test: stabilize node 26 full-suite edge cases

This commit is contained in:
Peter Steinberger
2026-05-08 16:52:18 +01:00
parent 7cc0b21e4d
commit bbd6d9e254
7 changed files with 34 additions and 20 deletions

View File

@@ -21,6 +21,11 @@ export async function writeExternalFileWithinOutputRoot(params: {
rootDir,
path: outputPath,
write: params.write,
}).catch((err: unknown) => {
if (err instanceof Error && /file not found/i.test(err.message)) {
throw new Error("output directory changed while writing file");
}
throw err;
});
return result.path;
}

View File

@@ -1680,7 +1680,8 @@ describe("processDiscordMessage draft streaming", () => {
await runProcessDiscordMessage(ctx);
expect(draftStream.update).toHaveBeenCalledWith("Clawing...\n🧩 First\n🧩 Second\n🧩 Third");
expect(draftStream.update).toHaveBeenNthCalledWith(1, "Clawing...\n🧩 First\n🧩 Second");
expect(draftStream.update).toHaveBeenNthCalledWith(2, "🧩 First\n🧩 Second\n🧩 Third");
});
it("skips empty apply_patch starts and renders the patch summary", async () => {

View File

@@ -320,9 +320,7 @@ describe("createTeamsReplyStreamController", () => {
expect(ctrl.shouldSuppressDefaultToolProgressMessages()).toBe(true);
expect(ctrl.shouldStreamPreviewToolProgress()).toBe(true);
expect(streamInstances[0]?.sendInformativeUpdate).toHaveBeenLastCalledWith(
"Working\n- tool: exec",
);
expect(streamInstances[0]?.sendInformativeUpdate).toHaveBeenLastCalledWith("- tool: exec");
});
it("suppresses Teams default progress messages without stream lines when tool progress is disabled", async () => {

View File

@@ -11,16 +11,6 @@ let resetSessionWriteLockStateForTest: typeof import("./session-write-lock.js").
let resolveSessionLockMaxHoldFromTimeout: typeof import("./session-write-lock.js").resolveSessionLockMaxHoldFromTimeout;
let resolveSessionWriteLockAcquireTimeoutMs: typeof import("./session-write-lock.js").resolveSessionWriteLockAcquireTimeoutMs;
vi.mock("../shared/pid-alive.js", async () => {
const original =
await vi.importActual<typeof import("../shared/pid-alive.js")>("../shared/pid-alive.js");
return {
...original,
// Keep liveness checks real; only pin process start time for PID recycle coverage.
getProcessStartTime: (pid: number) => (pid === process.pid ? FAKE_STARTTIME : null),
};
});
async function expectLockRemovedOnlyAfterFinalRelease(params: {
lockPath: string;
firstLock: { release: () => Promise<void> };
@@ -142,6 +132,12 @@ describe("acquireSessionWriteLock", () => {
resetSessionWriteLockStateForTest();
vi.clearAllMocks();
});
function pinCurrentProcessStartTimeForTest(): void {
__testing.setProcessStartTimeResolverForTest((pid) =>
pid === process.pid ? FAKE_STARTTIME : null,
);
}
it("reuses locks across symlinked session paths", async () => {
await withSymlinkedSessionPaths(
async ({ sessionReal, sessionLink, realLockPath, linkLockPath }) => {
@@ -418,6 +414,7 @@ describe("acquireSessionWriteLock", () => {
});
it("cleans untracked current-process .jsonl lock files with matching starttime", async () => {
pinCurrentProcessStartTimeForTest();
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lock-"));
const sessionsDir = path.join(root, "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
@@ -490,6 +487,7 @@ describe("acquireSessionWriteLock", () => {
return;
}
await withTempSessionLockFile(async ({ sessionFile, lockPath }) => {
pinCurrentProcessStartTimeForTest();
// Write a lock with a live PID (current process) but a wrong starttime,
// simulating PID recycling: the PID is alive but belongs to a different
// process than the one that created the lock.
@@ -511,6 +509,7 @@ describe("acquireSessionWriteLock", () => {
it("reclaims untracked current-process lock files with matching starttime", async () => {
await withTempSessionLockFile(async ({ sessionFile, lockPath }) => {
pinCurrentProcessStartTimeForTest();
await writeCurrentProcessLock(lockPath, { starttime: FAKE_STARTTIME });
await expectCurrentPidOwnsLock({ sessionFile, timeoutMs: 500 });
@@ -526,6 +525,7 @@ describe("acquireSessionWriteLock", () => {
});
it("does not reclaim active in-process lock files with matching starttime", async () => {
pinCurrentProcessStartTimeForTest();
await expectActiveInProcessLockIsNotReclaimed({ legacyStarttime: FAKE_STARTTIME });
});

View File

@@ -61,6 +61,7 @@ type LockInspectionDetails = Pick<
>;
const SESSION_LOCKS = createFileLockManager("openclaw.session-write-lock");
let resolveProcessStartTimeForLock = getProcessStartTime;
function isFileLockError(error: unknown, code: string): boolean {
return (error as { code?: unknown } | null)?.code === code;
@@ -312,7 +313,7 @@ function inspectLockPayload(
const pidRecycled =
pidAlive && pid !== null && storedStarttime !== null
? (() => {
const currentStarttime = getProcessStartTime(pid);
const currentStarttime = resolveProcessStartTimeForLock(pid);
return currentStarttime !== null && currentStarttime !== storedStarttime;
})()
: false;
@@ -419,7 +420,7 @@ function shouldTreatAsOrphanSelfLock(params: {
return params.reclaimLockWithoutStarttime;
}
const currentStarttime = getProcessStartTime(process.pid);
const currentStarttime = resolveProcessStartTimeForLock(process.pid);
return currentStarttime !== null && currentStarttime === storedStarttime;
}
@@ -543,7 +544,7 @@ export async function acquireSessionWriteLock(params: {
metadata: { maxHoldMs },
payload: () => {
const createdAt = new Date().toISOString();
const starttime = getProcessStartTime(process.pid);
const starttime = resolveProcessStartTimeForLock(process.pid);
const lockPayload: LockFilePayload = { pid: process.pid, createdAt };
if (starttime !== null) {
lockPayload.starttime = starttime;
@@ -591,6 +592,9 @@ export const __testing = {
handleTerminationSignal,
releaseAllLocksSync,
runLockWatchdogCheck,
setProcessStartTimeResolverForTest(resolver: ((pid: number) => number | null) | null): void {
resolveProcessStartTimeForLock = resolver ?? getProcessStartTime;
},
};
export async function drainSessionWriteLockStateForTest(): Promise<void> {
@@ -603,4 +607,5 @@ export function resetSessionWriteLockStateForTest(): void {
releaseAllLocksSync();
stopWatchdogTimer();
unregisterCleanupHandlers();
resolveProcessStartTimeForLock = getProcessStartTime;
}

View File

@@ -69,7 +69,7 @@ function extractProviderFromModelRef(value: string): string | null {
}
function hasConfiguredEmbeddedHarnessRuntime(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
return collectConfiguredAgentHarnessRuntimes(cfg, env).length > 0;
return collectConfiguredAgentHarnessRuntimes(cfg, env, { includeEnvRuntime: false }).length > 0;
}
function resolveAgentHarnessOwnerPluginIds(
@@ -641,7 +641,9 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: {
}
}
for (const runtime of collectConfiguredAgentHarnessRuntimes(params.config, params.env)) {
for (const runtime of collectConfiguredAgentHarnessRuntimes(params.config, params.env, {
includeEnvRuntime: false,
})) {
const pluginIds = resolveAgentHarnessOwnerPluginIds(params.registry, runtime);
for (const pluginId of pluginIds) {
changes.push({

View File

@@ -629,7 +629,10 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
rootConfig: activationSourceConfig,
};
const requiredAgentHarnessRuntimes = new Set(
collectConfiguredAgentHarnessRuntimes(activationSourceConfig, params.env),
collectConfiguredAgentHarnessRuntimes(activationSourceConfig, params.env, {
includeEnvRuntime: false,
includeLegacyAgentRuntimes: false,
}),
);
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const manifestLookup = createManifestRegistryLookup(params.manifestRegistry);