diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index b947e443d4fb..e359bf9e735f 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -36,15 +36,11 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/agents/modes/interactive/theme/theme.ts: setTheme", "src/agents/openai-completions-compat.ts: resolveOpenAICompletionsCompatDefaults", "src/agents/openai-transport-stream.ts: testing", - "src/agents/run-wait.ts: testing", "src/agents/session-model-auto-revert.ts: reconcileAgentPatchedSessionModel", "src/agents/session-suspension.ts: testing", - "src/agents/session-write-lock.ts: resetSessionWriteLockStateForTest", - "src/agents/session-write-lock.ts: testing", "src/agents/sessions/tools/bash.ts: resolveBashTimeoutMs", "src/agents/tool-search.ts: testing", "src/agents/tool-search.ts: ToolSearchCatalogSession", - "src/agents/tools/agent-step.ts: testing", "src/agents/tools/image-generate-tool.ts: resolveImageGenerationModelConfigForTool", "src/agents/tools/image-tool.ts: resolveImageModelConfigForTool", "src/agents/tools/image-tool.ts: testing", @@ -54,7 +50,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/agents/tools/video-generate-tool.ts: resolveVideoGenerationModelConfigForTool", "src/agents/tools/web-fetch.ts: sanitizeWebFetchUrl", "src/agents/utils/tools-manager.ts: testing", - "src/agents/worktrees/run-lease.ts: testing", "src/auto-reply/reply/abort.ts: testing", "src/auto-reply/reply/acp-reset-target.ts: testing", "src/auto-reply/reply/agent-runner-context-recovery.ts: computeContextAwareReserveTokensFloor", diff --git a/src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts b/src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts index 7f0deff78018..861160454d76 100644 --- a/src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts @@ -22,10 +22,8 @@ import { SessionWriteLockStaleError, SessionWriteLockTimeoutError, } from "../../session-write-lock-error.js"; -import { - acquireSessionWriteLock, - resetSessionWriteLockStateForTest, -} from "../../session-write-lock.js"; +import { acquireSessionWriteLock } from "../../session-write-lock.js"; +import { resetSessionWriteLockStateForTest } from "../../session-write-lock.test-support.js"; import { SessionManager } from "../../sessions/session-manager.js"; import { acquireEmbeddedAttemptSessionFileOwner, diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index fcd8dfa9e8a9..4116a2b33b8e 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -42,7 +42,7 @@ import { testing as embeddedRunsTesting, setActiveEmbeddedRun, } from "./embedded-agent-runner/runs.js"; -import { testing as agentStepTesting } from "./tools/agent-step.js"; +import { testing as agentStepTesting } from "./tools/agent-step.test-support.js"; import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js"; import { createSessionsListTool } from "./tools/sessions-list-tool.js"; import { testing as sessionsResolutionTesting } from "./tools/sessions-resolution.js"; diff --git a/src/agents/run-wait.test.ts b/src/agents/run-wait.test.ts index a56df9875e4e..869627a8fec3 100644 --- a/src/agents/run-wait.test.ts +++ b/src/agents/run-wait.test.ts @@ -15,7 +15,6 @@ vi.mock("../gateway/call.js", () => ({ })); import { - testing, isRecoverableAgentWaitError, readLatestAssistantReply, readLatestAssistantReplySnapshot, @@ -77,9 +76,6 @@ function expectAgentWaitRequest( describe("readLatestAssistantReply", () => { beforeEach(() => { callGatewayMock.mockReset(); - testing.setDepsForTest({ - callGateway: async (opts) => await callGatewayMock(opts), - }); }); it("returns the most recent assistant message when compaction markers trail history", async () => { @@ -279,9 +275,6 @@ describe("readLatestAssistantReply", () => { describe("waitForAgentRun", () => { beforeEach(() => { callGatewayMock.mockReset(); - testing.setDepsForTest({ - callGateway: async (opts) => await callGatewayMock(opts), - }); }); it("maps gateway timeouts to timeout status", async () => { @@ -465,9 +458,6 @@ describe("waitForAgentRun", () => { describe("waitForAgentRunAndReadUpdatedAssistantReply", () => { beforeEach(() => { callGatewayMock.mockReset(); - testing.setDepsForTest({ - callGateway: async (opts) => await callGatewayMock(opts), - }); }); it("returns undefined when the latest assistant fingerprint matches the baseline", async () => { @@ -934,9 +924,6 @@ describe("waitForAgentRunAndReadUpdatedAssistantReply", () => { describe("waitForAgentRunsToDrain", () => { beforeEach(() => { callGatewayMock.mockReset(); - testing.setDepsForTest({ - callGateway: async (opts) => await callGatewayMock(opts), - }); }); it("waits across rounds until descendant runs stop changing", async () => { diff --git a/src/agents/run-wait.ts b/src/agents/run-wait.ts index 21b319b03f7d..2fed7aa51f5c 100644 --- a/src/agents/run-wait.ts +++ b/src/agents/run-wait.ts @@ -34,14 +34,6 @@ import { extractAssistantText, stripToolMessages } from "./tools/chat-history-te type GatewayCaller = typeof callGateway; -const defaultRunWaitDeps = { - callGateway, -}; - -let runWaitDeps: { - callGateway: GatewayCaller; -} = defaultRunWaitDeps; - function resolveRunWaitTimeoutMs(value: number | undefined): number { return clampTimerTimeoutMs(parseFiniteNumber(value) ?? 1) ?? 1; } @@ -339,7 +331,7 @@ export async function readLatestAssistantReplySnapshot(params: { stopAtTranscriptArtifact?: boolean; callGateway?: GatewayCaller; }): Promise { - const history = await (params.callGateway ?? runWaitDeps.callGateway)<{ + const history = await (params.callGateway ?? callGateway)<{ messages: Array; }>({ method: "chat.history", @@ -374,7 +366,7 @@ export async function waitForAgentRun(params: { }): Promise { const timeoutMs = resolveRunWaitTimeoutMs(params.timeoutMs); try { - const wait = await (params.callGateway ?? runWaitDeps.callGateway)({ + const wait = await (params.callGateway ?? callGateway)({ method: "agent.wait", params: { runId: params.runId, @@ -469,15 +461,3 @@ export async function waitForAgentRunsToDrain(params: { deadlineAtMs, }; } - -/** Test-only dependency injection for gateway calls. */ -export const testing = { - setDepsForTest(overrides?: Partial<{ callGateway: GatewayCaller }>) { - runWaitDeps = overrides - ? { - ...defaultRunWaitDeps, - ...overrides, - } - : defaultRunWaitDeps; - }, -}; diff --git a/src/agents/session-write-lock.test-support.ts b/src/agents/session-write-lock.test-support.ts new file mode 100644 index 000000000000..e70cdf563ea1 --- /dev/null +++ b/src/agents/session-write-lock.test-support.ts @@ -0,0 +1,44 @@ +import type { SessionLockInspection } from "./session-write-lock.js"; +import "./session-write-lock.js"; + +type CleanupSignal = "SIGINT" | "SIGTERM" | "SIGQUIT" | "SIGABRT"; +type LockFilePayload = { + pid?: number; + createdAt?: string; + starttime?: number; + maxHoldMs?: number; +}; +type LockInspectionDetails = Pick< + SessionLockInspection, + "pid" | "pidAlive" | "createdAt" | "ageMs" | "stale" | "staleReasons" +>; + +type SessionWriteLockTestApi = { + resetSessionWriteLockStateForTest(): void; + testing: { + cleanupSignals: CleanupSignal[]; + handleTerminationSignal(signal: CleanupSignal): void; + inspectLockPayloadForTest( + payload: LockFilePayload | null, + staleMs: number, + nowMs: number, + opts?: { respectMaxHold?: boolean }, + ): LockInspectionDetails; + releaseAllLocksSync(): void; + runLockWatchdogCheck(nowMs?: number): Promise; + resolveRemainingAcquireTimeoutMs(timeoutMs: number, startedAtMs: number, nowMs: number): number; + setProcessStartTimeResolverForTest(resolver: ((pid: number) => number | null) | null): void; + }; +}; + +function getTestApi(): SessionWriteLockTestApi { + return (globalThis as Record)[ + Symbol.for("openclaw.sessionWriteLockTestApi") + ] as SessionWriteLockTestApi; +} + +export function resetSessionWriteLockStateForTest(): void { + getTestApi().resetSessionWriteLockStateForTest(); +} + +export const testing = getTestApi().testing; diff --git a/src/agents/session-write-lock.test.ts b/src/agents/session-write-lock.test.ts index 11a9258d68a1..b6d137008e9d 100644 --- a/src/agents/session-write-lock.test.ts +++ b/src/agents/session-write-lock.test.ts @@ -10,10 +10,10 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { SessionWriteLockStaleError } from "./session-write-lock-error.js"; const FAKE_STARTTIME = 12345; -let testing: typeof import("./session-write-lock.js").testing; +let testing: typeof import("./session-write-lock.test-support.js").testing; let acquireSessionWriteLock: typeof import("./session-write-lock.js").acquireSessionWriteLock; let cleanStaleLockFiles: typeof import("./session-write-lock.js").cleanStaleLockFiles; -let resetSessionWriteLockStateForTest: typeof import("./session-write-lock.js").resetSessionWriteLockStateForTest; +let resetSessionWriteLockStateForTest: typeof import("./session-write-lock.test-support.js").resetSessionWriteLockStateForTest; let resolveSessionLockMaxHoldFromTimeout: typeof import("./session-write-lock.js").resolveSessionLockMaxHoldFromTimeout; let resolveSessionWriteLockAcquireTimeoutMs: typeof import("./session-write-lock.js").resolveSessionWriteLockAcquireTimeoutMs; let resolveSessionWriteLockOptions: typeof import("./session-write-lock.js").resolveSessionWriteLockOptions; @@ -163,14 +163,14 @@ async function expectActiveInProcessLockIsNotReclaimed(params?: { describe("acquireSessionWriteLock", () => { beforeAll(async () => { ({ - testing, acquireSessionWriteLock, cleanStaleLockFiles, - resetSessionWriteLockStateForTest, resolveSessionLockMaxHoldFromTimeout, resolveSessionWriteLockAcquireTimeoutMs, resolveSessionWriteLockOptions, } = await import("./session-write-lock.js")); + ({ testing, resetSessionWriteLockStateForTest } = + await import("./session-write-lock.test-support.js")); }); afterEach(() => { diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index 77424d583898..92f7989627ba 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -1077,7 +1077,7 @@ export async function acquireSessionWriteLock(params: { } } -export const testing = { +const testing = { cleanupSignals: [...CLEANUP_SIGNALS], handleTerminationSignal, inspectLockPayloadForTest: inspectLockPayload, @@ -1095,10 +1095,17 @@ export async function drainSessionWriteLockStateForTest(): Promise { unregisterCleanupHandlers(); } -export function resetSessionWriteLockStateForTest(): void { +function resetSessionWriteLockStateForTest(): void { releaseAllLocksSync(); stopWatchdogTimer(); unregisterCleanupHandlers(); resolveProcessStartTimeForLock = getProcessStartTime; } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.sessionWriteLockTestApi")] = { + resetSessionWriteLockStateForTest, + testing, + }; +} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/agents/tools/agent-step.test-support.ts b/src/agents/tools/agent-step.test-support.ts new file mode 100644 index 000000000000..0114aa940a16 --- /dev/null +++ b/src/agents/tools/agent-step.test-support.ts @@ -0,0 +1,23 @@ +import type { callGateway } from "../../gateway/call.js"; +import "./agent-step.js"; + +type AgentCommandRunner = typeof import("../../commands/agent.js").agentCommandFromIngress; +type AgentStepTesting = { + setDepsForTest( + overrides?: Partial<{ + agentCommandFromIngress: AgentCommandRunner; + callGateway: typeof callGateway; + }>, + ): void; +}; +type AgentStepTestApi = { + testing: AgentStepTesting; +}; + +function getTestApi(): AgentStepTestApi { + return (globalThis as Record)[ + Symbol.for("openclaw.agentStepTestApi") + ] as AgentStepTestApi; +} + +export const testing = getTestApi().testing; diff --git a/src/agents/tools/agent-step.test.ts b/src/agents/tools/agent-step.test.ts index a604926b3f05..09842e939e50 100644 --- a/src/agents/tools/agent-step.test.ts +++ b/src/agents/tools/agent-step.test.ts @@ -2,7 +2,8 @@ // MCP runtime retirement after completed nested turns. import { afterEach, describe, expect, it, vi } from "vitest"; import type { CallGatewayOptions } from "../../gateway/call.js"; -import { runAgentStep, testing } from "./agent-step.js"; +import { runAgentStep } from "./agent-step.js"; +import { testing } from "./agent-step.test-support.js"; const runWaitMocks = vi.hoisted(() => ({ waitForAgentRunAndReadUpdatedAssistantReply: vi.fn(), diff --git a/src/agents/tools/agent-step.ts b/src/agents/tools/agent-step.ts index 39bfddfae54a..c859689c66d0 100644 --- a/src/agents/tools/agent-step.ts +++ b/src/agents/tools/agent-step.ts @@ -137,7 +137,7 @@ export async function runAgentStep(params: { } /** Test-only dependency overrides for gateway and in-process command execution. */ -export const testing = { +const testing = { setDepsForTest( overrides?: Partial<{ agentCommandFromIngress: AgentCommandRunner; @@ -152,3 +152,9 @@ export const testing = { : defaultAgentStepDeps; }, }; + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.agentStepTestApi")] = { + testing, + }; +} diff --git a/src/agents/worktrees/run-lease.test-support.ts b/src/agents/worktrees/run-lease.test-support.ts new file mode 100644 index 000000000000..e165882175fb --- /dev/null +++ b/src/agents/worktrees/run-lease.test-support.ts @@ -0,0 +1,24 @@ +import type { unlockWorktree } from "./git-lock.js"; +import type { releaseWorktreeRunLeaseRow } from "./registry.js"; +import "./run-lease.js"; + +type WorktreeRunLeaseTesting = { + setProcessStartTimeResolverForTest(resolver: ((pid: number) => number | null) | null): void; + setDeadPidResolverForTest(resolver: ((pid: number) => boolean) | null): void; + setReleaseRowImplForTest(impl: typeof releaseWorktreeRunLeaseRow | null): void; + setUnlockImplForTest(impl: typeof unlockWorktree | null): void; + drainPendingCleanupsForTest(): Promise; + resetForTest(): void; +}; + +type WorktreeRunLeaseTestApi = { + testing: WorktreeRunLeaseTesting; +}; + +function getTestApi(): WorktreeRunLeaseTestApi { + return (globalThis as Record)[ + Symbol.for("openclaw.worktreeRunLeaseTestApi") + ] as WorktreeRunLeaseTestApi; +} + +export const testing = getTestApi().testing; diff --git a/src/agents/worktrees/run-lease.test.ts b/src/agents/worktrees/run-lease.test.ts index a37bfda4fa70..8bd363601f13 100644 --- a/src/agents/worktrees/run-lease.test.ts +++ b/src/agents/worktrees/run-lease.test.ts @@ -13,13 +13,13 @@ import { releaseWorktreeRunLeaseRow, } from "./registry.js"; import { - testing as runLeaseTesting, abortWorktreeRemoval, acquireWorktreeRunLease, claimWorktreeRemoval, hasLiveWorktreeRunLease, resolveWorktreeIdForPath, } from "./run-lease.js"; +import { testing as runLeaseTesting } from "./run-lease.test-support.js"; import { ManagedWorktreeService } from "./service.js"; const execFileAsync = promisify(execFile); diff --git a/src/agents/worktrees/run-lease.ts b/src/agents/worktrees/run-lease.ts index 0e8c2fc07046..6dd0556698f3 100644 --- a/src/agents/worktrees/run-lease.ts +++ b/src/agents/worktrees/run-lease.ts @@ -321,7 +321,7 @@ export function hasLiveWorktreeRunLease(env: NodeJS.ProcessEnv, worktreeId: stri return hasLiveWorktreeRunLeaseRow(env, worktreeId, ownerChecks); } -export const testing = { +const testing = { setProcessStartTimeResolverForTest(resolver: ((pid: number) => number | null) | null): void { resolveSelfStartTime = resolver ?? getFileLockProcessStartTime; ownerChecks = { ...ownerChecks, getProcessStartTime: resolver ?? undefined }; @@ -348,3 +348,9 @@ export const testing = { unlockWorktreeImpl = unlockWorktree; }, }; + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.worktreeRunLeaseTestApi")] = { + testing, + }; +} diff --git a/src/agents/worktrees/service.remove-lease.test.ts b/src/agents/worktrees/service.remove-lease.test.ts index 65e72cf2f5f7..23187384cd61 100644 --- a/src/agents/worktrees/service.remove-lease.test.ts +++ b/src/agents/worktrees/service.remove-lease.test.ts @@ -6,7 +6,8 @@ import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import { getRegistryWorktree } from "./registry.js"; -import { acquireWorktreeRunLease, testing as runLeaseTesting } from "./run-lease.js"; +import { acquireWorktreeRunLease } from "./run-lease.js"; +import { testing as runLeaseTesting } from "./run-lease.test-support.js"; import { IDLE_GC_MS, ManagedWorktreeService } from "./service.js"; const execFileAsync = promisify(execFile); diff --git a/src/cron/isolated-agent/subagent-followup.test.ts b/src/cron/isolated-agent/subagent-followup.test.ts index fa1e598be170..1dfdf25e313c 100644 --- a/src/cron/isolated-agent/subagent-followup.test.ts +++ b/src/cron/isolated-agent/subagent-followup.test.ts @@ -31,8 +31,7 @@ vi.mock("../../gateway/call.js", () => ({ })); const { listDescendantRunsForRequester } = await import("../../agents/subagent-registry-read.js"); -const { testing: runWaitTesting, readLatestAssistantReply } = - await import("../../agents/run-wait.js"); +const { readLatestAssistantReply } = await import("../../agents/run-wait.js"); const { callGateway } = await import("../../gateway/call.js"); async function resolveAfterAdvancingTimers(promise: Promise, advanceMs = 100): Promise { @@ -285,14 +284,10 @@ describe("waitForDescendantSubagentSummary", () => { vi.mocked(listDescendantRunsForRequester).mockReturnValue([]); vi.mocked(readLatestAssistantReply).mockResolvedValue(undefined); vi.mocked(callGateway).mockResolvedValue({ status: "ok" }); - runWaitTesting.setDepsForTest({ - callGateway: ((opts) => vi.mocked(callGateway)(opts as never)) as typeof callGateway, - }); }); afterEach(() => { vi.useRealTimers(); - runWaitTesting.setDepsForTest(); }); it("returns initialReply immediately when no active descendants and observedActiveDescendants=false", async () => { diff --git a/src/gateway/server.sessions-send.test.ts b/src/gateway/server.sessions-send.test.ts index 206dfa9a4d2b..45d481c42d72 100644 --- a/src/gateway/server.sessions-send.test.ts +++ b/src/gateway/server.sessions-send.test.ts @@ -4,7 +4,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi, type Mock } from "vitest"; -import { testing as agentStepTesting } from "../agents/tools/agent-step.js"; +import { testing as agentStepTesting } from "../agents/tools/agent-step.test-support.js"; import { runSessionsSendA2AFlow } from "../agents/tools/sessions-send-tool.a2a.js"; import { loadSessionEntry, diff --git a/src/test-utils/session-state-cleanup.test.ts b/src/test-utils/session-state-cleanup.test.ts index 9fc40b095130..b19b98aa47bf 100644 --- a/src/test-utils/session-state-cleanup.test.ts +++ b/src/test-utils/session-state-cleanup.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { resetSessionWriteLockStateForTest } from "../agents/session-write-lock.js"; +import { resetSessionWriteLockStateForTest } from "../agents/session-write-lock.test-support.js"; import { runExclusiveSessionStoreWrite } from "../config/sessions/store-writer.js"; import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import { resetFileLockStateForTest } from "../infra/file-lock.js"; diff --git a/test/setup-openclaw-runtime.ts b/test/setup-openclaw-runtime.ts index 4deb28a8ce8b..66c59911d406 100644 --- a/test/setup-openclaw-runtime.ts +++ b/test/setup-openclaw-runtime.ts @@ -32,7 +32,7 @@ type WorkerCleanupHelpers = { resetContextWindowCacheForTest: typeof import("../src/agents/context-runtime-state.js").resetContextWindowCacheForTest; resetFileLockStateForTest: typeof import("../src/infra/file-lock.js").resetFileLockStateForTest; resetModelsJsonReadyCacheForTest: typeof import("../src/agents/models-config-state.test-support.js").resetModelsJsonReadyCacheForTest; - resetSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").resetSessionWriteLockStateForTest; + resetSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.test-support.js").resetSessionWriteLockStateForTest; }; type ReplyToModeResolver = NonNullable< @@ -79,6 +79,9 @@ function loadWorkerCleanupHelpers(): Promise { vi.importActual( "../src/agents/session-write-lock.js", ), + vi.importActual( + "../src/agents/session-write-lock.test-support.js", + ), vi.importActual( "../src/config/sessions/store-cache.js", ), @@ -91,6 +94,7 @@ function loadWorkerCleanupHelpers(): Promise { contextRuntimeState, modelsConfigState, sessionWriteLock, + sessionWriteLockTestSupport, sessionStoreCache, sessionStoreWriterState, fileLock, @@ -103,7 +107,8 @@ function loadWorkerCleanupHelpers(): Promise { resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest, resetFileLockStateForTest: fileLock.resetFileLockStateForTest, resetModelsJsonReadyCacheForTest: modelsConfigState.resetModelsJsonReadyCacheForTest, - resetSessionWriteLockStateForTest: sessionWriteLock.resetSessionWriteLockStateForTest, + resetSessionWriteLockStateForTest: + sessionWriteLockTestSupport.resetSessionWriteLockStateForTest, }), ); return globalState[WORKER_CLEANUP_HELPERS];