Josh Lehman 9cb0fa58c2
fix: restore protocol outputs and stabilize Windows path CI (#44266)
* fix(ci): restore protocol outputs and stabilize Windows path test

Regenerate the Swift protocol models so protocol:check stops failing on main.
Align the session target test helper with the sync production realpath behavior so Windows does not compare runneradmin and RUNNER~1 spellings for the same file.

Regeneration-Prompt: |
  Investigate the failing checks from merged PR #34485 and confirm whether they still affect current main before changing code. Keep the fix tight: do not alter runtime behavior beyond what is required to clear the reproduced CI regressions. Commit the generated Swift protocol outputs for the PushTestResult transport field because protocol:check was failing from stale generated files on main. Also fix the Windows-only session target test by making its helper use the same synchronous realpath behavior as production discovery, so path spelling differences like runneradmin versus RUNNER~1 do not cause a false assertion failure.

* fix(ci): align session target realpath behavior on Windows

Use native realpath for sync session target discovery so it matches the async path on Windows, and update the session target test helper to assert against the same canonical path form.

Regeneration-Prompt: |
  After opening the follow-up PR for the CI regressions from merged PR #34485, inspect the new failing Windows shard instead of assuming the first fix covered every case. Keep scope limited to the session target path mismatch exposed by CI. Fix the inconsistency at the source by making sync session target discovery use the same native realpath canonicalization as the async discovery path on Windows, then update the test helper to match that shared behavior and verify the touched file with targeted tests and file-scoped lint/format checks.

* test: make merge config fixtures satisfy provider type

After rebasing the PR onto current origin/main, the merge helper test fixtures no longer satisfied ProviderConfig because the anthropic provider examples were missing required provider and model fields. Add a shared fully-typed model fixture and explicit anthropic baseUrl values so the test keeps full type coverage under tsgo.

Regeneration-Prompt: |
  Rebase the PR branch for #44266 onto the current origin/main because the failing CI error only reproduced on the merge ref. Re-run the type-check path and inspect src/agents/models-config.merge.test.ts at the exact compiler lines instead of weakening types globally. Keep the fix test-only: make the anthropic ProviderConfig fixtures structurally valid by supplying the required baseUrl and full model definition fields, and keep the shared fixture typed so tsgo accepts it without unknown casts.

* fix: align Windows session store test expectations
2026-03-12 10:55:29 -07:00

345 lines
11 KiB
TypeScript

import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { listAgentIds, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import {
resolveAgentSessionDirsFromAgentsDir,
resolveAgentSessionDirsFromAgentsDirSync,
} from "../../agents/session-dirs.js";
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
import { resolveStateDir } from "../paths.js";
import type { OpenClawConfig } from "../types.openclaw.js";
import { resolveAgentsDirFromSessionStorePath, resolveStorePath } from "./paths.js";
export type SessionStoreSelectionOptions = {
store?: string;
agent?: string;
allAgents?: boolean;
};
export type SessionStoreTarget = {
agentId: string;
storePath: string;
};
const NON_FATAL_DISCOVERY_ERROR_CODES = new Set([
"EACCES",
"ELOOP",
"ENOENT",
"ENOTDIR",
"EPERM",
"ESTALE",
]);
function dedupeTargetsByStorePath(targets: SessionStoreTarget[]): SessionStoreTarget[] {
const deduped = new Map<string, SessionStoreTarget>();
for (const target of targets) {
if (!deduped.has(target.storePath)) {
deduped.set(target.storePath, target);
}
}
return [...deduped.values()];
}
function shouldSkipDiscoveryError(err: unknown): boolean {
const code = (err as NodeJS.ErrnoException | undefined)?.code;
return typeof code === "string" && NON_FATAL_DISCOVERY_ERROR_CODES.has(code);
}
function isWithinRoot(realPath: string, realRoot: string): boolean {
return realPath === realRoot || realPath.startsWith(`${realRoot}${path.sep}`);
}
function shouldSkipDiscoveredAgentDirName(dirName: string, agentId: string): boolean {
// Avoid collapsing arbitrary directory names like "###" into the default main agent.
// Human-friendly names like "Retired Agent" are still allowed because they normalize to
// a non-default stable id and preserve the intended retired-store discovery behavior.
return agentId === DEFAULT_AGENT_ID && dirName.trim().toLowerCase() !== DEFAULT_AGENT_ID;
}
function resolveValidatedDiscoveredStorePathSync(params: {
sessionsDir: string;
agentsRoot: string;
realAgentsRoot?: string;
}): string | undefined {
const storePath = path.join(params.sessionsDir, "sessions.json");
try {
const stat = fsSync.lstatSync(storePath);
if (stat.isSymbolicLink() || !stat.isFile()) {
return undefined;
}
const realStorePath = fsSync.realpathSync.native(storePath);
const realAgentsRoot = params.realAgentsRoot ?? fsSync.realpathSync.native(params.agentsRoot);
return isWithinRoot(realStorePath, realAgentsRoot) ? realStorePath : undefined;
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
return undefined;
}
throw err;
}
}
async function resolveValidatedDiscoveredStorePath(params: {
sessionsDir: string;
agentsRoot: string;
realAgentsRoot?: string;
}): Promise<string | undefined> {
const storePath = path.join(params.sessionsDir, "sessions.json");
try {
const stat = await fs.lstat(storePath);
if (stat.isSymbolicLink() || !stat.isFile()) {
return undefined;
}
const realStorePath = await fs.realpath(storePath);
const realAgentsRoot = params.realAgentsRoot ?? (await fs.realpath(params.agentsRoot));
return isWithinRoot(realStorePath, realAgentsRoot) ? realStorePath : undefined;
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
return undefined;
}
throw err;
}
}
function resolveSessionStoreDiscoveryState(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): {
configuredTargets: SessionStoreTarget[];
agentsRoots: string[];
} {
const configuredTargets = resolveSessionStoreTargets(cfg, { allAgents: true }, { env });
const agentsRoots = new Set<string>();
for (const target of configuredTargets) {
const agentsDir = resolveAgentsDirFromSessionStorePath(target.storePath);
if (agentsDir) {
agentsRoots.add(agentsDir);
}
}
agentsRoots.add(path.join(resolveStateDir(env), "agents"));
return {
configuredTargets,
agentsRoots: [...agentsRoots],
};
}
function toDiscoveredSessionStoreTarget(
sessionsDir: string,
storePath: string,
): SessionStoreTarget | undefined {
const dirName = path.basename(path.dirname(sessionsDir));
const agentId = normalizeAgentId(dirName);
if (shouldSkipDiscoveredAgentDirName(dirName, agentId)) {
return undefined;
}
return {
agentId,
// Keep the actual on-disk store path so retired/manual agent dirs remain discoverable
// even if their directory name no longer round-trips through normalizeAgentId().
storePath,
};
}
export function resolveAllAgentSessionStoreTargetsSync(
cfg: OpenClawConfig,
params: { env?: NodeJS.ProcessEnv } = {},
): SessionStoreTarget[] {
const env = params.env ?? process.env;
const { configuredTargets, agentsRoots } = resolveSessionStoreDiscoveryState(cfg, env);
const realAgentsRoots = new Map<string, string>();
const getRealAgentsRoot = (agentsRoot: string): string | undefined => {
const cached = realAgentsRoots.get(agentsRoot);
if (cached !== undefined) {
return cached;
}
try {
const realAgentsRoot = fsSync.realpathSync.native(agentsRoot);
realAgentsRoots.set(agentsRoot, realAgentsRoot);
return realAgentsRoot;
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
return undefined;
}
throw err;
}
};
const validatedConfiguredTargets = configuredTargets.flatMap((target) => {
const agentsRoot = resolveAgentsDirFromSessionStorePath(target.storePath);
if (!agentsRoot) {
return [target];
}
const realAgentsRoot = getRealAgentsRoot(agentsRoot);
if (!realAgentsRoot) {
return [];
}
const validatedStorePath = resolveValidatedDiscoveredStorePathSync({
sessionsDir: path.dirname(target.storePath),
agentsRoot,
realAgentsRoot,
});
return validatedStorePath ? [{ ...target, storePath: validatedStorePath }] : [];
});
const discoveredTargets = agentsRoots.flatMap((agentsDir) => {
try {
const realAgentsRoot = getRealAgentsRoot(agentsDir);
if (!realAgentsRoot) {
return [];
}
return resolveAgentSessionDirsFromAgentsDirSync(agentsDir).flatMap((sessionsDir) => {
const validatedStorePath = resolveValidatedDiscoveredStorePathSync({
sessionsDir,
agentsRoot: agentsDir,
realAgentsRoot,
});
const target = validatedStorePath
? toDiscoveredSessionStoreTarget(sessionsDir, validatedStorePath)
: undefined;
return target ? [target] : [];
});
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
return [];
}
throw err;
}
});
return dedupeTargetsByStorePath([...validatedConfiguredTargets, ...discoveredTargets]);
}
export async function resolveAllAgentSessionStoreTargets(
cfg: OpenClawConfig,
params: { env?: NodeJS.ProcessEnv } = {},
): Promise<SessionStoreTarget[]> {
const env = params.env ?? process.env;
const { configuredTargets, agentsRoots } = resolveSessionStoreDiscoveryState(cfg, env);
const realAgentsRoots = new Map<string, string>();
const getRealAgentsRoot = async (agentsRoot: string): Promise<string | undefined> => {
const cached = realAgentsRoots.get(agentsRoot);
if (cached !== undefined) {
return cached;
}
try {
const realAgentsRoot = await fs.realpath(agentsRoot);
realAgentsRoots.set(agentsRoot, realAgentsRoot);
return realAgentsRoot;
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
return undefined;
}
throw err;
}
};
const validatedConfiguredTargets = (
await Promise.all(
configuredTargets.map(async (target) => {
const agentsRoot = resolveAgentsDirFromSessionStorePath(target.storePath);
if (!agentsRoot) {
return target;
}
const realAgentsRoot = await getRealAgentsRoot(agentsRoot);
if (!realAgentsRoot) {
return undefined;
}
const validatedStorePath = await resolveValidatedDiscoveredStorePath({
sessionsDir: path.dirname(target.storePath),
agentsRoot,
realAgentsRoot,
});
return validatedStorePath ? { ...target, storePath: validatedStorePath } : undefined;
}),
)
).filter((target): target is SessionStoreTarget => Boolean(target));
const discoveredTargets = (
await Promise.all(
agentsRoots.map(async (agentsDir) => {
try {
const realAgentsRoot = await getRealAgentsRoot(agentsDir);
if (!realAgentsRoot) {
return [];
}
const sessionsDirs = await resolveAgentSessionDirsFromAgentsDir(agentsDir);
return (
await Promise.all(
sessionsDirs.map(async (sessionsDir) => {
const validatedStorePath = await resolveValidatedDiscoveredStorePath({
sessionsDir,
agentsRoot: agentsDir,
realAgentsRoot,
});
return validatedStorePath
? toDiscoveredSessionStoreTarget(sessionsDir, validatedStorePath)
: undefined;
}),
)
).filter((target): target is SessionStoreTarget => Boolean(target));
} catch (err) {
if (shouldSkipDiscoveryError(err)) {
return [];
}
throw err;
}
}),
)
).flat();
return dedupeTargetsByStorePath([...validatedConfiguredTargets, ...discoveredTargets]);
}
export function resolveSessionStoreTargets(
cfg: OpenClawConfig,
opts: SessionStoreSelectionOptions,
params: { env?: NodeJS.ProcessEnv } = {},
): SessionStoreTarget[] {
const env = params.env ?? process.env;
const defaultAgentId = resolveDefaultAgentId(cfg);
const hasAgent = Boolean(opts.agent?.trim());
const allAgents = opts.allAgents === true;
if (hasAgent && allAgents) {
throw new Error("--agent and --all-agents cannot be used together");
}
if (opts.store && (hasAgent || allAgents)) {
throw new Error("--store cannot be combined with --agent or --all-agents");
}
if (opts.store) {
return [
{
agentId: defaultAgentId,
storePath: resolveStorePath(opts.store, { agentId: defaultAgentId, env }),
},
];
}
if (allAgents) {
const targets = listAgentIds(cfg).map((agentId) => ({
agentId,
storePath: resolveStorePath(cfg.session?.store, { agentId, env }),
}));
return dedupeTargetsByStorePath(targets);
}
if (hasAgent) {
const knownAgents = listAgentIds(cfg);
const requested = normalizeAgentId(opts.agent ?? "");
if (!knownAgents.includes(requested)) {
throw new Error(
`Unknown agent id "${opts.agent}". Use "openclaw agents list" to see configured agents.`,
);
}
return [
{
agentId: requested,
storePath: resolveStorePath(cfg.session?.store, { agentId: requested, env }),
},
];
}
return [
{
agentId: defaultAgentId,
storePath: resolveStorePath(cfg.session?.store, { agentId: defaultAgentId, env }),
},
];
}