diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index 0e924c9c9ee3..4be12d9a3c04 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -85,10 +85,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/commands/sessions-tail.ts: setSessionsTailFollowIntervalMsForTests", "src/commands/sessions.ts: testing", "src/commands/status.command.ts: resolvePairingRecoveryContext", - "src/commitments/extraction.ts: validateCommitmentCandidates", - "src/commitments/runtime.ts: configureCommitmentExtractionRuntime", - "src/commitments/runtime.ts: drainCommitmentExtractionQueue", - "src/commitments/runtime.ts: resetCommitmentExtractionRuntimeForTests", "src/cron/isolated-agent/delivery-dispatch.ts: getCompletedDirectCronDeliveriesCountForTests", "src/cron/isolated-agent/delivery-dispatch.ts: resetCompletedDirectCronDeliveriesForTests", "src/cron/schedule.ts: clearCronScheduleCacheForTest", @@ -100,24 +96,7 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/cron/service/timer.ts: executeJobCore", "src/cron/service/timer.ts: onTimer", "src/cron/session-reaper.ts: resetReaperThrottle", - "src/flows/doctor-health-contributions.ts: createDoctorHealthContribution", - "src/flows/doctor-health-contributions.ts: resolveDoctorHealthContributions", "src/logging/redact-internal.ts: withFullContextToolPayloadRedaction", - "src/sessions/session-lifecycle-admission.ts: runExclusiveSessionLifecycle", - "src/sessions/session-upstream-monitor.ts: runSessionUpstreamMonitorTick", - "src/sessions/user-turn-transcript.ts: persistUserTurnTranscript", - "src/tasks/detached-task-runtime.ts: resetDetachedTaskLifecycleRuntimeForTests", - "src/tasks/detached-task-runtime.ts: setDetachedTaskLifecycleRuntime", - "src/tasks/generated-media-task-activity.ts: resetGeneratedMediaTaskActivityForTests", - "src/tasks/task-flow-registry.store.ts: configureTaskFlowRegistryRuntime", - "src/tasks/task-flow-registry.ts: createFlowRecord", - "src/tasks/task-flow-registry.ts: resetTaskFlowRegistryForTests", - "src/tasks/task-registry.ts: maybeDeliverTaskStateChangeUpdate", - "src/tasks/task-registry.ts: resetTaskRegistryControlRuntimeForTests", - "src/tasks/task-registry.ts: resetTaskRegistryDeliveryRuntimeForTests", - "src/tasks/task-registry.ts: resetTaskRegistryForTests", - "src/tasks/task-registry.ts: setTaskRegistryControlRuntimeForTests", - "src/tasks/task-registry.ts: setTaskRegistryDeliveryRuntimeForTests", ]; // Platform-variant findings. Allowed when present; never required. diff --git a/scripts/e2e/commitments-safety-docker-client.ts b/scripts/e2e/commitments-safety-docker-client.ts index 67e7156d5a8e..5a86e2a9a9df 100644 --- a/scripts/e2e/commitments-safety-docker-client.ts +++ b/scripts/e2e/commitments-safety-docker-client.ts @@ -4,12 +4,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; +import { enqueueCommitmentExtraction } from "../../dist/commitments/runtime.js"; import { configureCommitmentExtractionRuntime, drainCommitmentExtractionQueue, - enqueueCommitmentExtraction, resetCommitmentExtractionRuntimeForTests, -} from "../../dist/commitments/runtime.js"; +} from "../../dist/commitments/runtime.test-support.js"; import { listCommitments, listDueCommitmentsForSession, diff --git a/scripts/e2e/commitments-safety-docker.sh b/scripts/e2e/commitments-safety-docker.sh index 71ff1d933785..713cb3a24ab5 100755 --- a/scripts/e2e/commitments-safety-docker.sh +++ b/scripts/e2e/commitments-safety-docker.sh @@ -23,6 +23,7 @@ set +e docker_e2e_run_with_harness \ --name "$CONTAINER_NAME" \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e OPENCLAW_COMMITMENTS_SAFETY_E2E=1 \ "$IMAGE_NAME" \ bash -lc 'set -euo pipefail; tsx scripts/e2e/commitments-safety-docker-client.ts' \ >"$RUN_LOG" 2>&1 diff --git a/src/agents/command/attempt-execution.cli.test.ts b/src/agents/command/attempt-execution.cli.test.ts index fa7b132bf418..8dc2ef6d3710 100644 --- a/src/agents/command/attempt-execution.cli.test.ts +++ b/src/agents/command/attempt-execution.cli.test.ts @@ -20,10 +20,8 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { createUserTurnTranscriptRecorder } from "../../sessions/user-turn-transcript.js"; import { createTestUserTurnTranscriptTarget } from "../../sessions/user-turn-transcript.test-support.js"; import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js"; -import { - registerGeneratedMediaTaskActivity, - resetGeneratedMediaTaskActivityForTests, -} from "../../tasks/generated-media-task-activity.js"; +import { registerGeneratedMediaTaskActivity } from "../../tasks/generated-media-task-activity.js"; +import { resetGeneratedMediaTaskActivityForTests } from "../../tasks/task-runtime.test-helpers.js"; import { captureEnv, setTestEnvValue } from "../../test-utils/env.js"; import { saveAuthProfileStore } from "../auth-profiles/store.js"; import type { EmbeddedAgentRunResult } from "../embedded-agent.js"; diff --git a/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts b/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts index 04854c43cfd3..0020ca5af06a 100644 --- a/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts +++ b/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts @@ -8,14 +8,14 @@ import { enqueueCommandInLane, markGatewayDraining } from "../../process/command import * as commandQueueModule from "../../process/command-queue.js"; import { resetCommandQueueStateForTest } from "../../process/command-queue.test-support.js"; import { createQueuedTaskRun as createQueuedTaskRunOrNull } from "../../tasks/task-executor.js"; -import { getTaskFlowById, resetTaskFlowRegistryForTests } from "../../tasks/task-flow-registry.js"; +import { getTaskFlowById } from "../../tasks/task-flow-registry.js"; +import { getTaskById, listTasksForOwnerKey } from "../../tasks/task-registry.js"; +import type { TaskRecord } from "../../tasks/task-registry.types.js"; import { - getTaskById, - listTasksForOwnerKey, + resetTaskFlowRegistryForTests, resetTaskRegistryForTests, setTaskRegistryDeliveryRuntimeForTests, -} from "../../tasks/task-registry.js"; -import type { TaskRecord } from "../../tasks/task-registry.types.js"; +} from "../../tasks/task-runtime.test-helpers.js"; import { withStateDirEnv } from "../../test-helpers/state-dir-env.js"; import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js"; import { resolveSessionLane } from "./lanes.js"; diff --git a/src/agents/embedded-agent-runner/run/attempt.async-tasks.test.ts b/src/agents/embedded-agent-runner/run/attempt.async-tasks.test.ts index 2a3ebdbcac59..92b58af09c8e 100644 --- a/src/agents/embedded-agent-runner/run/attempt.async-tasks.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.async-tasks.test.ts @@ -5,7 +5,7 @@ import { createRunningTaskRun, } from "../../../tasks/detached-task-runtime.js"; import type { TaskRecord } from "../../../tasks/runtime-internal.js"; -import { resetTaskRegistryForTests } from "../../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../../tasks/task-runtime.test-helpers.js"; import { requiresCompletionRequiredAsyncTaskWait, shouldWaitForCompletionRequiredAsyncTasks, diff --git a/src/agents/embedded-agent-runner/run/attempt.llm-boundary.cache-stability.test.ts b/src/agents/embedded-agent-runner/run/attempt.llm-boundary.cache-stability.test.ts index 5a99c17e1918..a47cebda873c 100644 --- a/src/agents/embedded-agent-runner/run/attempt.llm-boundary.cache-stability.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.llm-boundary.cache-stability.test.ts @@ -29,9 +29,9 @@ import type { Context, Model } from "../../../llm/types.js"; import { createUserTurnTranscriptRecorder, mergePreparedUserTurnMessageForRuntime, - persistUserTurnTranscript, type UserTurnInput, } from "../../../sessions/user-turn-transcript.js"; +import { persistUserTurnTranscript } from "../../../sessions/user-turn-transcript.test-support.js"; import { OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE, relocateCurrentRuntimeContextCarrierToTail, diff --git a/src/agents/subagent-orphan-recovery.restart-integration.test.ts b/src/agents/subagent-orphan-recovery.restart-integration.test.ts index 4f10fb920e74..36c061652c90 100644 --- a/src/agents/subagent-orphan-recovery.restart-integration.test.ts +++ b/src/agents/subagent-orphan-recovery.restart-integration.test.ts @@ -13,8 +13,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { setRuntimeConfigSnapshot } from "../config/config.js"; import { callGateway } from "../gateway/call.js"; import { createRunningTaskRun } from "../tasks/detached-task-runtime.js"; -import { resetTaskFlowRegistryForTests } from "../tasks/task-flow-registry.js"; -import { findTaskByRunId, resetTaskRegistryForTests } from "../tasks/task-registry.js"; +import { findTaskByRunId } from "../tasks/task-registry.js"; +import { + resetTaskFlowRegistryForTests, + resetTaskRegistryForTests, +} from "../tasks/task-runtime.test-helpers.js"; import { captureEnv } from "../test-utils/env.js"; import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js"; import { recoverOrphanedSubagentSessions } from "./subagent-orphan-recovery.js"; diff --git a/src/agents/subagent-registry.archive.e2e.test.ts b/src/agents/subagent-registry.archive.e2e.test.ts index 9be7bfaa0518..1e23d0d65aa8 100644 --- a/src/agents/subagent-registry.archive.e2e.test.ts +++ b/src/agents/subagent-registry.archive.e2e.test.ts @@ -6,11 +6,11 @@ import path from "node:path"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { callGateway } from "../gateway/call.js"; import { SUBAGENT_KILL_TASK_ERROR } from "../tasks/detached-task-runtime-contract.js"; +import { getDetachedTaskLifecycleRuntime } from "../tasks/detached-task-runtime.js"; import { - getDetachedTaskLifecycleRuntime, resetDetachedTaskLifecycleRuntimeForTests, setDetachedTaskLifecycleRuntime, -} from "../tasks/detached-task-runtime.js"; +} from "../tasks/task-runtime.test-helpers.js"; const taskRuntimeMocks = vi.hoisted(() => ({ finalizeTaskRunByRunId: vi.fn<(_params: unknown) => unknown[]>(() => [{}]), diff --git a/src/agents/subagent-registry.steer-restart.test.ts b/src/agents/subagent-registry.steer-restart.test.ts index 7c35108a6e36..132e56a1abd6 100644 --- a/src/agents/subagent-registry.steer-restart.test.ts +++ b/src/agents/subagent-registry.steer-restart.test.ts @@ -4,10 +4,8 @@ import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ContextEngine } from "../context-engine/types.js"; -import { - getDetachedTaskLifecycleRuntime, - setDetachedTaskLifecycleRuntime, -} from "../tasks/detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "../tasks/detached-task-runtime.js"; +import { setDetachedTaskLifecycleRuntime } from "../tasks/task-runtime.test-helpers.js"; import { findTaskByRunIdForStatus } from "../tasks/task-status-access.js"; const noop = () => {}; diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index 2782c94f121b..7fa4e4ed585b 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -23,11 +23,13 @@ import { findDetachedTaskRun, finalizeTaskRunByRunId, getDetachedTaskLifecycleRuntime, - resetDetachedTaskLifecycleRuntimeForTests, - setDetachedTaskLifecycleRuntime, } from "../tasks/detached-task-runtime.js"; -import { resetTaskFlowRegistryForTests } from "../tasks/task-flow-registry.js"; -import { resetTaskRegistryForTests } from "../tasks/task-registry.js"; +import { + resetDetachedTaskLifecycleRuntimeForTests, + resetTaskFlowRegistryForTests, + resetTaskRegistryForTests, + setDetachedTaskLifecycleRuntime, +} from "../tasks/task-runtime.test-helpers.js"; import { findTaskByRunIdForStatus } from "../tasks/task-status-access.js"; import { SUBAGENT_ENDED_REASON_COMPLETE, diff --git a/src/agents/tools/media-generate-background-shared.test.ts b/src/agents/tools/media-generate-background-shared.test.ts index 36b4e4ab69de..9402a621bb3e 100644 --- a/src/agents/tools/media-generate-background-shared.test.ts +++ b/src/agents/tools/media-generate-background-shared.test.ts @@ -2,7 +2,7 @@ // wake delivery, and direct media fallback behavior. import { beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions/types.js"; -import { resetGeneratedMediaTaskActivityForTests } from "../../tasks/generated-media-task-activity.js"; +import { resetGeneratedMediaTaskActivityForTests } from "../../tasks/task-runtime.test-helpers.js"; import { hasPendingGeneratedMediaTaskForSessionKey } from "../../tasks/task-status-access.js"; const subagentAnnounceDeliveryMocks = vi.hoisted(() => ({ diff --git a/src/auto-reply/reply/commands-acp.test.ts b/src/auto-reply/reply/commands-acp.test.ts index 86560e895296..8d8dc50eaab8 100644 --- a/src/auto-reply/reply/commands-acp.test.ts +++ b/src/auto-reply/reply/commands-acp.test.ts @@ -147,8 +147,8 @@ const { buildCommandTestParams } = await import("./commands-spawn.test-harness.j const { testing: acpManagerTesting } = await import("../../acp/control-plane/manager.js"); const { testing: acpResetTargetTesting, resolveEffectiveResetTargetSessionKey } = await import("./acp-reset-target.js"); -const { createTaskRecord, resetTaskRegistryForTests } = - await import("../../tasks/task-registry.js"); +const { createTaskRecord } = await import("../../tasks/task-registry.js"); +const { resetTaskRegistryForTests } = await import("../../tasks/task-runtime.test-helpers.js"); const { configureTaskRegistryRuntime } = await import("../../tasks/task-registry.store.js"); const { failTaskRunByRunId } = await import("../../tasks/task-executor.js"); diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index c81213bcf032..136ee52cf7af 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -20,7 +20,7 @@ import { createRunningTaskRun, failTaskRunByRunId, } from "../../tasks/task-executor.js"; -import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { buildStatusPluginsReply, buildStatusReply, buildStatusText } from "./commands-status.js"; import { diff --git a/src/auto-reply/reply/commands-subagents-info.test.ts b/src/auto-reply/reply/commands-subagents-info.test.ts index 69edc0698e1a..58e1714c6b1a 100644 --- a/src/auto-reply/reply/commands-subagents-info.test.ts +++ b/src/auto-reply/reply/commands-subagents-info.test.ts @@ -9,7 +9,8 @@ import { import type { SubagentRunRecord } from "../../agents/subagent-registry.types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { failTaskRunByRunId } from "../../tasks/task-executor.js"; -import { createTaskRecord, resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { createTaskRecord } from "../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; import type { ReplyPayload } from "../types.js"; import { handleSubagentsInfoAction } from "./commands-subagents/action-info.js"; import { diff --git a/src/auto-reply/reply/commands-tasks.test.ts b/src/auto-reply/reply/commands-tasks.test.ts index b1894b52e4b1..ba6e73a6bacc 100644 --- a/src/auto-reply/reply/commands-tasks.test.ts +++ b/src/auto-reply/reply/commands-tasks.test.ts @@ -7,7 +7,7 @@ import { createRunningTaskRun, failTaskRunByRunId, } from "../../tasks/task-executor.js"; -import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; import { handleTasksCommand } from "./commands-tasks.js"; import { baseCommandTestConfig, diff --git a/src/commands/flows.test.ts b/src/commands/flows.test.ts index 0f4f3f02f54c..83c41e15f5cc 100644 --- a/src/commands/flows.test.ts +++ b/src/commands/flows.test.ts @@ -2,16 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../runtime.js"; import { createRunningTaskRun as createRunningTaskRunOrNull } from "../tasks/task-executor.js"; -import { - createManagedTaskFlow as createManagedTaskFlowOrNull, - resetTaskFlowRegistryForTests, -} from "../tasks/task-flow-registry.js"; +import { createManagedTaskFlow as createManagedTaskFlowOrNull } from "../tasks/task-flow-registry.js"; import type { TaskFlowRecord } from "../tasks/task-flow-registry.types.js"; +import type { TaskRecord } from "../tasks/task-registry.types.js"; import { + resetTaskFlowRegistryForTests, resetTaskRegistryDeliveryRuntimeForTests, resetTaskRegistryForTests, -} from "../tasks/task-registry.js"; -import type { TaskRecord } from "../tasks/task-registry.types.js"; +} from "../tasks/task-runtime.test-helpers.js"; import { captureEnv } from "../test-utils/env.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { flowsCancelCommand, flowsListCommand, flowsShowCommand } from "./flows.js"; diff --git a/src/commands/tasks-json.test.ts b/src/commands/tasks-json.test.ts index 780a3c1ba631..a47b4c54c1db 100644 --- a/src/commands/tasks-json.test.ts +++ b/src/commands/tasks-json.test.ts @@ -1,18 +1,16 @@ // Tasks JSON tests cover structured task command output and managed task flow state. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../runtime.js"; -import { - createManagedTaskFlow as createManagedTaskFlowOrNull, - resetTaskFlowRegistryForTests, -} from "../tasks/task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "../tasks/task-flow-registry.store.js"; +import { createManagedTaskFlow as createManagedTaskFlowOrNull } from "../tasks/task-flow-registry.js"; import type { TaskFlowRecord } from "../tasks/task-flow-registry.types.js"; +import { createTaskRecord as createTaskRecordOrNull } from "../tasks/task-registry.js"; +import type { TaskRecord } from "../tasks/task-registry.types.js"; import { - createTaskRecord as createTaskRecordOrNull, + configureTaskFlowRegistryRuntime, + resetTaskFlowRegistryForTests, resetTaskRegistryDeliveryRuntimeForTests, resetTaskRegistryForTests, -} from "../tasks/task-registry.js"; -import type { TaskRecord } from "../tasks/task-registry.types.js"; +} from "../tasks/task-runtime.test-helpers.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { tasksAuditJsonCommand, tasksListJsonCommand } from "./tasks-json.js"; diff --git a/src/commands/tasks.test.ts b/src/commands/tasks.test.ts index f4e2192797ac..084f87c5c513 100644 --- a/src/commands/tasks.test.ts +++ b/src/commands/tasks.test.ts @@ -7,22 +7,22 @@ import type { SessionEntry } from "../config/sessions/types.js"; import { saveCronStore } from "../cron/store.js"; import type { RuntimeEnv } from "../runtime.js"; import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js"; -import { resetDetachedTaskLifecycleRuntimeForTests } from "../tasks/detached-task-runtime.js"; -import { - createManagedTaskFlow as createManagedTaskFlowOrNull, - resetTaskFlowRegistryForTests, -} from "../tasks/task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "../tasks/task-flow-registry.store.js"; +import { createManagedTaskFlow as createManagedTaskFlowOrNull } from "../tasks/task-flow-registry.js"; import type { TaskFlowRecord } from "../tasks/task-flow-registry.types.js"; import { createTaskRecord as createTaskRecordOrNull, getTaskById, reloadTaskRegistryFromStore, - resetTaskRegistryDeliveryRuntimeForTests, - resetTaskRegistryForTests, } from "../tasks/task-registry.js"; import * as taskRegistryMaintenance from "../tasks/task-registry.maintenance.js"; import type { TaskRecord } from "../tasks/task-registry.types.js"; +import { + configureTaskFlowRegistryRuntime, + resetDetachedTaskLifecycleRuntimeForTests, + resetTaskFlowRegistryForTests, + resetTaskRegistryDeliveryRuntimeForTests, + resetTaskRegistryForTests, +} from "../tasks/task-runtime.test-helpers.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import type { OpenClawTestState } from "../test-utils/openclaw-test-state.js"; import type { TaskSystemAuditCode, TaskSystemAuditSeverity } from "./tasks-audit-system.js"; diff --git a/src/commitments/commitments-full-chain.integration.test.ts b/src/commitments/commitments-full-chain.integration.test.ts index 7162b602ca91..899a386a2237 100644 --- a/src/commitments/commitments-full-chain.integration.test.ts +++ b/src/commitments/commitments-full-chain.integration.test.ts @@ -9,12 +9,12 @@ import { } from "../infra/heartbeat-runner.test-utils.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { withEnvAsync } from "../test-utils/env.js"; +import { enqueueCommitmentExtraction } from "./runtime.js"; import { configureCommitmentExtractionRuntime, drainCommitmentExtractionQueue, - enqueueCommitmentExtraction, resetCommitmentExtractionRuntimeForTests, -} from "./runtime.js"; +} from "./runtime.test-support.js"; import { readCommitmentsForTest } from "./store.test-utils.js"; import type { CommitmentExtractionBatchResult, CommitmentExtractionItem } from "./types.js"; diff --git a/src/commitments/extraction.test-support.ts b/src/commitments/extraction.test-support.ts new file mode 100644 index 000000000000..f6208923c15e --- /dev/null +++ b/src/commitments/extraction.test-support.ts @@ -0,0 +1,40 @@ +import type { OpenClawConfig } from "../config/config.js"; +import "./extraction.js"; +import type { + CommitmentCandidate, + CommitmentExtractionBatchResult, + CommitmentExtractionItem, +} from "./types.js"; + +type ValidatedCommitmentCandidate = { + item: CommitmentExtractionItem; + candidate: CommitmentCandidate; + earliestMs: number; + latestMs: number; + timezone: string; +}; + +type CommitmentExtractionTestApi = { + validateCommitmentCandidates(params: { + cfg?: OpenClawConfig; + items: CommitmentExtractionItem[]; + result: CommitmentExtractionBatchResult; + nowMs?: number; + }): ValidatedCommitmentCandidate[]; +}; + +function getTestApi(): CommitmentExtractionTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.commitmentExtractionTestApi") + ]; + if (!api) { + throw new Error("commitment extraction test API is unavailable"); + } + return api as CommitmentExtractionTestApi; +} + +export function validateCommitmentCandidates( + params: Parameters[0], +): ValidatedCommitmentCandidate[] { + return getTestApi().validateCommitmentCandidates(params); +} diff --git a/src/commitments/extraction.test.ts b/src/commitments/extraction.test.ts index 86989bef5886..78098ed97b62 100644 --- a/src/commitments/extraction.test.ts +++ b/src/commitments/extraction.test.ts @@ -10,8 +10,8 @@ import { buildCommitmentExtractionPrompt, parseCommitmentExtractionOutput, persistCommitmentExtractionResult, - validateCommitmentCandidates, } from "./extraction.js"; +import { validateCommitmentCandidates } from "./extraction.test-support.js"; import { readCommitmentsForTest } from "./store.test-utils.js"; import type { CommitmentCandidate, CommitmentExtractionItem } from "./types.js"; diff --git a/src/commitments/extraction.ts b/src/commitments/extraction.ts index 46959eb87132..273d4f70c04c 100644 --- a/src/commitments/extraction.ts +++ b/src/commitments/extraction.ts @@ -273,7 +273,7 @@ function resolveMinimumDueMs(params: { return params.nowMs + intervalMs; } -export function validateCommitmentCandidates(params: { +function validateCommitmentCandidates(params: { cfg?: OpenClawConfig; items: CommitmentExtractionItem[]; result: CommitmentExtractionBatchResult; @@ -370,3 +370,8 @@ export async function persistCommitmentExtractionResult(params: { } return created; } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.commitmentExtractionTestApi")] = + { validateCommitmentCandidates }; +} diff --git a/src/commitments/runtime.test-support.ts b/src/commitments/runtime.test-support.ts new file mode 100644 index 000000000000..b4ef2050b2ae --- /dev/null +++ b/src/commitments/runtime.test-support.ts @@ -0,0 +1,47 @@ +import type { OpenClawConfig } from "../config/config.js"; +import "./runtime.js"; +import type { CommitmentExtractionBatchResult, CommitmentExtractionItem } from "./types.js"; + +type TimerHandle = ReturnType; + +type CommitmentExtractionRuntime = { + extractBatch?: (params: { + cfg?: OpenClawConfig; + items: CommitmentExtractionItem[]; + }) => Promise; + resolveDefaultModel?: (params: { cfg: OpenClawConfig; agentId?: string }) => { + provider: string; + model: string; + }; + setTimer?: (callback: () => void, delayMs: number) => TimerHandle; + clearTimer?: (timer: TimerHandle) => void; + forceInTests?: boolean; +}; + +type CommitmentRuntimeTestApi = { + configureCommitmentExtractionRuntime(next: CommitmentExtractionRuntime): void; + drainCommitmentExtractionQueue(): Promise; + resetCommitmentExtractionRuntimeForTests(): void; +}; + +function getTestApi(): CommitmentRuntimeTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.commitmentRuntimeTestApi") + ]; + if (!api) { + throw new Error("commitment runtime test API is unavailable"); + } + return api as CommitmentRuntimeTestApi; +} + +export function configureCommitmentExtractionRuntime(next: CommitmentExtractionRuntime): void { + getTestApi().configureCommitmentExtractionRuntime(next); +} + +export async function drainCommitmentExtractionQueue(): Promise { + return await getTestApi().drainCommitmentExtractionQueue(); +} + +export function resetCommitmentExtractionRuntimeForTests(): void { + getTestApi().resetCommitmentExtractionRuntimeForTests(); +} diff --git a/src/commitments/runtime.test.ts b/src/commitments/runtime.test.ts index ad713570e135..f00e367ce1ec 100644 --- a/src/commitments/runtime.test.ts +++ b/src/commitments/runtime.test.ts @@ -6,12 +6,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { captureEnv, setTestEnvValue } from "../test-utils/env.js"; +import { enqueueCommitmentExtraction } from "./runtime.js"; import { configureCommitmentExtractionRuntime, drainCommitmentExtractionQueue, - enqueueCommitmentExtraction, resetCommitmentExtractionRuntimeForTests, -} from "./runtime.js"; +} from "./runtime.test-support.js"; import { readCommitmentsForTest, seedCommitmentsForTest } from "./store.test-utils.js"; import type { CommitmentExtractionBatchResult, CommitmentExtractionItem } from "./types.js"; diff --git a/src/commitments/runtime.ts b/src/commitments/runtime.ts index 6b03841731fc..55bbe789e712 100644 --- a/src/commitments/runtime.ts +++ b/src/commitments/runtime.ts @@ -91,12 +91,12 @@ function scheduleDrainSoon(debounceMs: number): void { } /** Installs runtime hooks for extraction tests or alternate batch extraction. */ -export function configureCommitmentExtractionRuntime(next: CommitmentExtractionRuntime): void { +function configureCommitmentExtractionRuntime(next: CommitmentExtractionRuntime): void { runtime = next; } /** Clears queued work, timers, and injected hooks for isolated tests. */ -export function resetCommitmentExtractionRuntimeForTests(): void { +function resetCommitmentExtractionRuntimeForTests(): void { if (timer) { clearTimer(timer); } @@ -299,7 +299,7 @@ function takeAgentBatch( } /** Drains queued extraction work in batches and returns processed item count. */ -export async function drainCommitmentExtractionQueue(): Promise { +async function drainCommitmentExtractionQueue(): Promise { if (draining) { return 0; } @@ -357,3 +357,15 @@ export async function drainCommitmentExtractionQueue(): Promise { draining = false; } } + +if ( + process.env.VITEST || + process.env.NODE_ENV === "test" || + process.env.OPENCLAW_COMMITMENTS_SAFETY_E2E === "1" +) { + (globalThis as Record)[Symbol.for("openclaw.commitmentRuntimeTestApi")] = { + configureCommitmentExtractionRuntime, + drainCommitmentExtractionQueue, + resetCommitmentExtractionRuntimeForTests, + }; +} diff --git a/src/cron/service/ops.test.ts b/src/cron/service/ops.test.ts index 1e7d419a7cbd..26135ca53740 100644 --- a/src/cron/service/ops.test.ts +++ b/src/cron/service/ops.test.ts @@ -4,11 +4,8 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { runOpenClawStateWriteTransaction } from "../../state/openclaw-state-db.js"; import * as taskExecutor from "../../tasks/task-executor.js"; -import { - findTaskByRunId, - listTaskRecordsUnsorted, - resetTaskRegistryForTests, -} from "../../tasks/task-registry.js"; +import { findTaskByRunId, listTaskRecordsUnsorted } from "../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; import { formatTaskStatusDetail } from "../../tasks/task-status.js"; import { withEnvAsync } from "../../test-utils/env.js"; import * as cronSchedule from "../schedule.js"; diff --git a/src/cron/service/task-runs.test.ts b/src/cron/service/task-runs.test.ts index f38d0d22b3f4..af73a343d1e0 100644 --- a/src/cron/service/task-runs.test.ts +++ b/src/cron/service/task-runs.test.ts @@ -1,14 +1,15 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { - getDetachedTaskLifecycleRuntime, - resetDetachedTaskLifecycleRuntimeForTests, - setDetachedTaskLifecycleRuntime, -} from "../../tasks/detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "../../tasks/detached-task-runtime.js"; import * as taskExecutor from "../../tasks/task-executor.js"; import { finalizeTaskRunByRunId } from "../../tasks/task-executor.js"; import * as taskRegistry from "../../tasks/task-registry.js"; -import { markTaskLostById, resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { markTaskLostById } from "../../tasks/task-registry.js"; import { listTaskRegistryRecordsByRuntimeSourceIdFromSqlite } from "../../tasks/task-registry.store.sqlite.js"; +import { + resetDetachedTaskLifecycleRuntimeForTests, + resetTaskRegistryForTests, + setDetachedTaskLifecycleRuntime, +} from "../../tasks/task-runtime.test-helpers.js"; import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { cronStoreKey } from "../store/key.js"; import { readCronTaskRunHistoryPage } from "../task-run-history.js"; diff --git a/src/cron/service/timer.backoff-config-readback.test.ts b/src/cron/service/timer.backoff-config-readback.test.ts index ab443f038b77..b1923728bc38 100644 --- a/src/cron/service/timer.backoff-config-readback.test.ts +++ b/src/cron/service/timer.backoff-config-readback.test.ts @@ -3,7 +3,7 @@ // computed in memory by applyJobResult. import path from "node:path"; import { describe, expect, it, vi } from "vitest"; -import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { setupCronServiceSuite, writeCronStoreSnapshot } from "../service.test-harness.js"; import { loadCronStore } from "../store.js"; diff --git a/src/cron/service/timer.regression.test.ts b/src/cron/service/timer.regression.test.ts index 03e2f2281e3f..99b2f8cf4ca3 100644 --- a/src/cron/service/timer.regression.test.ts +++ b/src/cron/service/timer.regression.test.ts @@ -13,13 +13,12 @@ import { import { HEARTBEAT_SKIP_LANES_BUSY, type HeartbeatRunResult } from "../../infra/heartbeat-wake.js"; import { enqueueCommandInLane } from "../../process/command-queue.js"; import { CommandLane } from "../../process/lanes.js"; +import { cancelTaskById, listTaskRecords } from "../../tasks/task-registry.js"; import { - cancelTaskById, - listTaskRecords, resetTaskRegistryControlRuntimeForTests, resetTaskRegistryForTests, setTaskRegistryControlRuntimeForTests, -} from "../../tasks/task-registry.js"; +} from "../../tasks/task-runtime.test-helpers.js"; import { advanceCronActiveJobGeneration, clearCronJobActive, diff --git a/src/cron/service/timer.test.ts b/src/cron/service/timer.test.ts index 68b9b902fb54..71b7c789b6c4 100644 --- a/src/cron/service/timer.test.ts +++ b/src/cron/service/timer.test.ts @@ -10,11 +10,8 @@ import { loadCronStore } from "../../cron/store.js"; import { cronStoreKey } from "../../cron/store/key.js"; import type { CronJob } from "../../cron/types.js"; import * as taskExecutor from "../../tasks/task-executor.js"; -import { - findTaskByRunId, - listTaskRecordsUnsorted, - resetTaskRegistryForTests, -} from "../../tasks/task-registry.js"; +import { findTaskByRunId, listTaskRecordsUnsorted } from "../../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../../tasks/task-runtime.test-helpers.js"; import { formatTaskStatusDetail } from "../../tasks/task-status.js"; const { logger, makeStorePath } = setupCronServiceSuite({ diff --git a/src/cron/task-run-history.test.ts b/src/cron/task-run-history.test.ts index 2bcc6302894c..9ad1ab9b8196 100644 --- a/src/cron/task-run-history.test.ts +++ b/src/cron/task-run-history.test.ts @@ -1,8 +1,8 @@ import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; -import { resetTaskRegistryForTests } from "../tasks/task-registry.js"; import { saveTaskRegistryStateToSqlite } from "../tasks/task-registry.store.sqlite.js"; import type { TaskRecord } from "../tasks/task-registry.types.js"; +import { resetTaskRegistryForTests } from "../tasks/task-runtime.test-helpers.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import type { CronRunLogEntry } from "./run-log-types.js"; import { CronService } from "./service.js"; diff --git a/src/flows/doctor-health-contributions.test-support.ts b/src/flows/doctor-health-contributions.test-support.ts new file mode 100644 index 000000000000..a430bd11eb34 --- /dev/null +++ b/src/flows/doctor-health-contributions.test-support.ts @@ -0,0 +1,57 @@ +import type { DoctorHealthFlowContext } from "./doctor-health-contributions.js"; +import "./doctor-health-contributions.js"; +import type { HealthCheckInput, RunnableHealthCheck } from "./health-check-runner-types.js"; +import type { HealthCheck } from "./health-checks.js"; +import type { FlowContribution } from "./types.js"; + +type DoctorContributionHealthCheck = + | (Omit & { + readonly id?: string; + readonly kind?: "core"; + readonly source?: string; + }) + | (Omit & { + readonly id?: string; + readonly kind?: "core"; + readonly source?: string; + }); + +type DoctorHealthContribution = FlowContribution & { + kind: "core"; + surface: "health"; + healthChecks: readonly HealthCheckInput[]; + healthCheckIds: readonly string[]; + run: (ctx: DoctorHealthFlowContext) => Promise; +}; + +type DoctorHealthContributionTestApi = { + createDoctorHealthContribution(params: { + id: string; + label: string; + healthCheckIds?: readonly string[]; + healthChecks?: DoctorContributionHealthCheck | readonly DoctorContributionHealthCheck[]; + hint?: string; + run?: (ctx: DoctorHealthFlowContext) => Promise; + }): DoctorHealthContribution; + resolveDoctorHealthContributions(): DoctorHealthContribution[]; +}; + +function getTestApi(): DoctorHealthContributionTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.doctorHealthContributionsTestApi") + ]; + if (!api) { + throw new Error("doctor health contributions test API is unavailable"); + } + return api as DoctorHealthContributionTestApi; +} + +export function createDoctorHealthContribution( + params: Parameters[0], +): DoctorHealthContribution { + return getTestApi().createDoctorHealthContribution(params); +} + +export function resolveDoctorHealthContributions(): DoctorHealthContribution[] { + return getTestApi().resolveDoctorHealthContributions(); +} diff --git a/src/flows/doctor-health-contributions.test.ts b/src/flows/doctor-health-contributions.test.ts index 345df27a1160..cb548d7d8831 100644 --- a/src/flows/doctor-health-contributions.test.ts +++ b/src/flows/doctor-health-contributions.test.ts @@ -7,11 +7,11 @@ import type { DoctorPrompter } from "../commands/doctor-prompter.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { CORE_HEALTH_CHECKS } from "./doctor-core-checks.js"; import "./doctor-tool-result-cap-advice.js"; +import { resolveDoctorContributionHealthChecks } from "./doctor-health-contributions.js"; import { createDoctorHealthContribution, - resolveDoctorContributionHealthChecks, resolveDoctorHealthContributions, -} from "./doctor-health-contributions.js"; +} from "./doctor-health-contributions.test-support.js"; import { runDoctorLintChecks } from "./doctor-lint-flow.js"; import type { HealthCheck, HealthFinding } from "./health-checks.js"; diff --git a/src/flows/doctor-health-contributions.ts b/src/flows/doctor-health-contributions.ts index adabac955c7a..57ec76cb123c 100644 --- a/src/flows/doctor-health-contributions.ts +++ b/src/flows/doctor-health-contributions.ts @@ -113,7 +113,7 @@ function shouldSkipLegacyUpdateDoctorConfigWrite(params: { env: NodeJS.ProcessEn return true; } -export function createDoctorHealthContribution(params: { +function createDoctorHealthContribution(params: { id: string; label: string; healthCheckIds?: readonly string[]; @@ -1529,7 +1529,7 @@ async function runSkillWorkshopToolPolicyHealth(ctx: DoctorHealthFlowContext): P await runCoreHealthFindingNote(ctx, "core/doctor/skill-workshop-tool-policy"); } -export function resolveDoctorHealthContributions(): DoctorHealthContribution[] { +function resolveDoctorHealthContributions(): DoctorHealthContribution[] { return [ createDoctorHealthContribution({ id: "doctor:gateway-config", @@ -2282,4 +2282,10 @@ export async function runDoctorHealthContributions(ctx: DoctorHealthFlowContext) await contribution.run(ctx); } } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[ + Symbol.for("openclaw.doctorHealthContributionsTestApi") + ] = { createDoctorHealthContribution, resolveDoctorHealthContributions }; +} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/gateway/server-methods/agent.events-and-subagents.test-utils.ts b/src/gateway/server-methods/agent.events-and-subagents.test-utils.ts index 93fc1e4cd5ff..dce0e9c186cd 100644 --- a/src/gateway/server-methods/agent.events-and-subagents.test-utils.ts +++ b/src/gateway/server-methods/agent.events-and-subagents.test-utils.ts @@ -13,11 +13,9 @@ import { resumeGatewaySuspend, } from "../../infra/gateway-suspend-coordinator.js"; import { resetGatewayWorkAdmission } from "../../process/gateway-work-admission.js"; -import { - getDetachedTaskLifecycleRuntime, - setDetachedTaskLifecycleRuntime, -} from "../../tasks/detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "../../tasks/detached-task-runtime.js"; import { findTaskByRunId } from "../../tasks/task-registry.js"; +import { setDetachedTaskLifecycleRuntime } from "../../tasks/task-runtime.test-helpers.js"; import { getAgentTestMocks, makeContext, diff --git a/src/gateway/server-methods/agent.sessions-and-models.test-utils.ts b/src/gateway/server-methods/agent.sessions-and-models.test-utils.ts index cbbbd3f3b7b9..f960fad4376e 100644 --- a/src/gateway/server-methods/agent.sessions-and-models.test-utils.ts +++ b/src/gateway/server-methods/agent.sessions-and-models.test-utils.ts @@ -9,16 +9,16 @@ import { resetSubagentRegistryForTests, testing as subagentRegistryTesting, } from "../../agents/subagent-registry.test-helpers.js"; -import { - getDetachedTaskLifecycleRuntime, - setDetachedTaskLifecycleRuntime, -} from "../../tasks/detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "../../tasks/detached-task-runtime.js"; import { findTaskByRunId, listTaskRecords, markTaskTerminalById, - resetTaskRegistryForTests, } from "../../tasks/task-registry.js"; +import { + resetTaskRegistryForTests, + setDetachedTaskLifecycleRuntime, +} from "../../tasks/task-runtime.test-helpers.js"; import { withTempDir } from "../../test-helpers/temp-dir.js"; import { getAgentTestMocks, diff --git a/src/gateway/server-methods/agent.test-harness.ts b/src/gateway/server-methods/agent.test-harness.ts index e69192abc700..6b10b892d22c 100644 --- a/src/gateway/server-methods/agent.test-harness.ts +++ b/src/gateway/server-methods/agent.test-harness.ts @@ -12,8 +12,10 @@ import type { SessionEntry } from "../../config/sessions.js"; import type { SessionTranscriptStats } from "../../config/sessions/session-accessor.js"; import { parseSqliteSessionFileMarker } from "../../config/sessions/sqlite-marker.js"; import { resetDiagnosticEventsForTest } from "../../infra/diagnostic-events.js"; -import { resetDetachedTaskLifecycleRuntimeForTests } from "../../tasks/detached-task-runtime.js"; -import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { + resetDetachedTaskLifecycleRuntimeForTests, + resetTaskRegistryForTests, +} from "../../tasks/task-runtime.test-helpers.js"; import { captureEnv, setTestEnvValue } from "../../test-utils/env.js"; import { agentHandlers } from "./agent.js"; import { suspendHandlers } from "./suspend.js"; diff --git a/src/gateway/server-methods/tasks.test.ts b/src/gateway/server-methods/tasks.test.ts index 2a76a6c19875..a0f9e1cfe7f3 100644 --- a/src/gateway/server-methods/tasks.test.ts +++ b/src/gateway/server-methods/tasks.test.ts @@ -13,14 +13,14 @@ import { markTaskTerminalById, recordTaskProgressByRunId, } from "../../tasks/runtime-internal.js"; +import { reloadTaskRegistryFromStore } from "../../tasks/task-registry.js"; +import { saveTaskRegistryStateToSqlite } from "../../tasks/task-registry.store.sqlite.js"; +import type { TaskRecord } from "../../tasks/task-registry.types.js"; import { - reloadTaskRegistryFromStore, resetTaskRegistryControlRuntimeForTests, resetTaskRegistryForTests, setTaskRegistryControlRuntimeForTests, -} from "../../tasks/task-registry.js"; -import { saveTaskRegistryStateToSqlite } from "../../tasks/task-registry.store.sqlite.js"; -import type { TaskRecord } from "../../tasks/task-registry.types.js"; +} from "../../tasks/task-runtime.test-helpers.js"; import { captureEnv, setTestEnvValue } from "../../test-utils/env.js"; import { tasksHandlers } from "./tasks.js"; import type { RespondFn } from "./types.js"; diff --git a/src/gateway/server-runtime-subscriptions.test.ts b/src/gateway/server-runtime-subscriptions.test.ts index e83aba96ff09..75c809cbd878 100644 --- a/src/gateway/server-runtime-subscriptions.test.ts +++ b/src/gateway/server-runtime-subscriptions.test.ts @@ -11,8 +11,9 @@ import { emitSessionTranscriptUpdate, type InternalSessionTranscriptUpdate, } from "../sessions/transcript-events.js"; -import { createTaskRecord, resetTaskRegistryForTests } from "../tasks/task-registry.js"; +import { createTaskRecord } from "../tasks/task-registry.js"; import { getTaskRegistryObservers } from "../tasks/task-registry.store.js"; +import { resetTaskRegistryForTests } from "../tasks/task-runtime.test-helpers.js"; import { installInMemoryTaskRegistryRuntime } from "../test-utils/task-registry-runtime.js"; import { createChatRunState, diff --git a/src/gateway/server.sessions.reset-cleanup.test.ts b/src/gateway/server.sessions.reset-cleanup.test.ts index 855175e0d675..76a767164e6c 100644 --- a/src/gateway/server.sessions.reset-cleanup.test.ts +++ b/src/gateway/server.sessions.reset-cleanup.test.ts @@ -16,9 +16,9 @@ import type { SessionAcpMeta } from "../config/sessions/types.js"; import { enqueueSystemEvent, peekSystemEvents } from "../infra/system-events.js"; import { beginSessionWorkAdmission, - runExclusiveSessionLifecycle, runExclusiveSessionLifecycleMutation, } from "../sessions/session-lifecycle-admission.js"; +import { runExclusiveSessionLifecycle } from "../sessions/session-lifecycle-admission.test-support.js"; import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import { embeddedRunMock, testState, writeSessionStore } from "./test-helpers.js"; import { diff --git a/src/infra/state-migrations.cron-run-logs.test.ts b/src/infra/state-migrations.cron-run-logs.test.ts index ea099780e073..f99a4e8b2131 100644 --- a/src/infra/state-migrations.cron-run-logs.test.ts +++ b/src/infra/state-migrations.cron-run-logs.test.ts @@ -8,7 +8,7 @@ import { closeOpenClawStateDatabaseForTest, openOpenClawStateDatabase, } from "../state/openclaw-state-db.js"; -import { resetTaskRegistryForTests } from "../tasks/task-registry.js"; +import { resetTaskRegistryForTests } from "../tasks/task-runtime.test-helpers.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; const CRON_RUN_LOG_TASK_IMPORT_MIGRATION_ID = "state:cron-run-logs-to-task-runs:v1"; diff --git a/src/plugins/runtime/runtime-task-test-harness.ts b/src/plugins/runtime/runtime-task-test-harness.ts index 6f828f6430e2..d735f69d32ec 100644 --- a/src/plugins/runtime/runtime-task-test-harness.ts +++ b/src/plugins/runtime/runtime-task-test-harness.ts @@ -1,7 +1,7 @@ // Runtime task test harness helpers build mocked plugin runtimes for task-flow tests. import { vi } from "vitest"; -import { resetDetachedTaskLifecycleRuntimeForTests } from "../../tasks/detached-task-runtime.js"; import { + resetDetachedTaskLifecycleRuntimeForTests, resetTaskFlowRegistryForTests, resetTaskRegistryControlRuntimeForTests, resetTaskRegistryDeliveryRuntimeForTests, diff --git a/src/plugins/runtime/runtime-tasks.test.ts b/src/plugins/runtime/runtime-tasks.test.ts index 0ff42d1369d4..d449e8fbf914 100644 --- a/src/plugins/runtime/runtime-tasks.test.ts +++ b/src/plugins/runtime/runtime-tasks.test.ts @@ -1,9 +1,7 @@ // Runtime task tests cover plugin task runtime registration, invocation, and cleanup. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - getDetachedTaskLifecycleRuntime, - setDetachedTaskLifecycleRuntime, -} from "../../tasks/detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "../../tasks/detached-task-runtime.js"; +import { setDetachedTaskLifecycleRuntime } from "../../tasks/task-runtime.test-helpers.js"; import { getRuntimeTaskMocks, installRuntimeTaskDeliveryMock, diff --git a/src/sessions/session-lifecycle-admission.test-support.ts b/src/sessions/session-lifecycle-admission.test-support.ts new file mode 100644 index 000000000000..d5db870d4927 --- /dev/null +++ b/src/sessions/session-lifecycle-admission.test-support.ts @@ -0,0 +1,24 @@ +import "./session-lifecycle-admission.js"; + +type RunExclusiveSessionLifecycleParams = { + scope: string; + identities: Iterable; + signal?: AbortSignal; + run: () => Promise; +}; + +type SessionLifecycleAdmissionTestApi = { + runExclusiveSessionLifecycle(params: RunExclusiveSessionLifecycleParams): Promise; +}; + +function getTestApi(): SessionLifecycleAdmissionTestApi { + return (globalThis as Record)[ + Symbol.for("openclaw.sessionLifecycleAdmissionTestApi") + ] as SessionLifecycleAdmissionTestApi; +} + +export async function runExclusiveSessionLifecycle( + params: RunExclusiveSessionLifecycleParams, +): Promise { + return await getTestApi().runExclusiveSessionLifecycle(params); +} diff --git a/src/sessions/session-lifecycle-admission.ts b/src/sessions/session-lifecycle-admission.ts index c8a7bfeb3fe0..3bc51bcefe6a 100644 --- a/src/sessions/session-lifecycle-admission.ts +++ b/src/sessions/session-lifecycle-admission.ts @@ -168,7 +168,7 @@ async function waitForNormalizedSessionLifecycleMutationIdle( } } -export async function runExclusiveSessionLifecycle(params: { +async function runExclusiveSessionLifecycle(params: { scope: string; identities: Iterable; signal?: AbortSignal; @@ -524,3 +524,9 @@ export async function interruptSessionWorkAdmissions(params: { } } } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[ + Symbol.for("openclaw.sessionLifecycleAdmissionTestApi") + ] = { runExclusiveSessionLifecycle }; +} diff --git a/src/sessions/session-upstream-monitor.test-support.ts b/src/sessions/session-upstream-monitor.test-support.ts new file mode 100644 index 000000000000..35c189ad5716 --- /dev/null +++ b/src/sessions/session-upstream-monitor.test-support.ts @@ -0,0 +1,42 @@ +import type { isEmbeddedAgentRunActive } from "../agents/embedded-agent.js"; +import type { loadSessionEntry } from "../config/sessions/session-accessor.js"; +import type { SessionEntry } from "../config/sessions/types.js"; +import type { SessionCatalogProvider, SessionUpstreamProbe } from "../plugins/session-catalog.js"; +import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js"; +import "./session-upstream-monitor.js"; + +type SessionUpstreamMonitorOptions = OpenClawStateDatabaseOptions & { + providers?: readonly SessionCatalogProvider[]; + now?: () => number; + loadEntry?: typeof loadSessionEntry; + isRunActive?: typeof isEmbeddedAgentRunActive; + loadOwnRecentUserTexts?: (params: { + entry: SessionEntry; + probe: Omit; + }) => Promise; +}; + +type SessionUpstreamMissingCounter = { + count: number; + linkUpdatedAt: number; +}; + +type SessionUpstreamMonitorTestApi = { + runSessionUpstreamMonitorTick( + options?: SessionUpstreamMonitorOptions, + missingCounts?: Map, + ): Promise; +}; + +function getTestApi(): SessionUpstreamMonitorTestApi { + return (globalThis as Record)[ + Symbol.for("openclaw.sessionUpstreamMonitorTestApi") + ] as SessionUpstreamMonitorTestApi; +} + +export async function runSessionUpstreamMonitorTick( + options: SessionUpstreamMonitorOptions = {}, + missingCounts: Map = new Map(), +): Promise { + await getTestApi().runSessionUpstreamMonitorTick(options, missingCounts); +} diff --git a/src/sessions/session-upstream-monitor.test.ts b/src/sessions/session-upstream-monitor.test.ts index 27afad368fc7..aa683ae3909d 100644 --- a/src/sessions/session-upstream-monitor.test.ts +++ b/src/sessions/session-upstream-monitor.test.ts @@ -15,7 +15,7 @@ import { readSessionUpstreamLink, upsertSessionUpstreamLink, } from "./session-upstream-links.js"; -import { runSessionUpstreamMonitorTick } from "./session-upstream-monitor.js"; +import { runSessionUpstreamMonitorTick } from "./session-upstream-monitor.test-support.js"; const tempDirs: string[] = []; const watcherSessionKey = "agent:main:main"; diff --git a/src/sessions/session-upstream-monitor.ts b/src/sessions/session-upstream-monitor.ts index 0f6c1be4e7b0..40a9cfd502ac 100644 --- a/src/sessions/session-upstream-monitor.ts +++ b/src/sessions/session-upstream-monitor.ts @@ -131,7 +131,7 @@ async function probeProvenanceUnchanged( ); } -export async function runSessionUpstreamMonitorTick( +async function runSessionUpstreamMonitorTick( options: SessionUpstreamMonitorOptions = {}, missingCounts: Map = new Map(), ): Promise { @@ -374,3 +374,9 @@ export function startSessionUpstreamMonitor( }, }; } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[ + Symbol.for("openclaw.sessionUpstreamMonitorTestApi") + ] = { runSessionUpstreamMonitorTick }; +} diff --git a/src/sessions/user-turn-transcript.test-support.ts b/src/sessions/user-turn-transcript.test-support.ts index c7ee1401faa0..d56b092fcaba 100644 --- a/src/sessions/user-turn-transcript.test-support.ts +++ b/src/sessions/user-turn-transcript.test-support.ts @@ -1,4 +1,27 @@ -import type { UserTurnTranscriptTarget } from "./user-turn-transcript.types.js"; +import "./user-turn-transcript.js"; +import type { + PersistUserTurnTranscriptParams, + UserTurnTranscriptPersistResult, + UserTurnTranscriptTarget, +} from "./user-turn-transcript.types.js"; + +type UserTurnTranscriptTestApi = { + persistUserTurnTranscript( + params: PersistUserTurnTranscriptParams, + ): Promise; +}; + +function getTestApi(): UserTurnTranscriptTestApi { + return (globalThis as Record)[ + Symbol.for("openclaw.userTurnTranscriptTestApi") + ] as UserTurnTranscriptTestApi; +} + +export async function persistUserTurnTranscript( + params: PersistUserTurnTranscriptParams, +): Promise { + return await getTestApi().persistUserTurnTranscript(params); +} /** Creates a store-backed transcript target for tests that do not own runtime session setup. */ export function createTestUserTurnTranscriptTarget( diff --git a/src/sessions/user-turn-transcript.test.ts b/src/sessions/user-turn-transcript.test.ts index 58555c2784d2..132074a5a46b 100644 --- a/src/sessions/user-turn-transcript.test.ts +++ b/src/sessions/user-turn-transcript.test.ts @@ -16,10 +16,10 @@ import { buildPersistedUserTurnMediaInputsFromFields, createUserTurnTranscriptRecorder, mergePreparedUserTurnMessageForRuntime, - persistUserTurnTranscript, resolvePersistedUserTurnText, type UserTurnInput, } from "./user-turn-transcript.js"; +import { persistUserTurnTranscript } from "./user-turn-transcript.test-support.js"; describe("user turn transcript persistence", () => { const tempDirs: string[] = []; diff --git a/src/sessions/user-turn-transcript.ts b/src/sessions/user-turn-transcript.ts index 6b799dd6ccca..09434206d280 100644 --- a/src/sessions/user-turn-transcript.ts +++ b/src/sessions/user-turn-transcript.ts @@ -411,7 +411,7 @@ export function preparePersistedUserTurnMessageForTranscriptWrite( // Store-backed persistence resolves the current session transcript file lazily // so callers can pass a session entry/store without knowing the final path. -export async function persistUserTurnTranscript( +async function persistUserTurnTranscript( params: PersistUserTurnTranscriptParams, ): Promise { const message = resolvePersistedUserTurnMessage(params); @@ -710,3 +710,9 @@ export function createUserTurnTranscriptRecorder( }), }; } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.userTurnTranscriptTestApi")] = { + persistUserTurnTranscript, + }; +} diff --git a/src/tasks/detached-task-runtime.test-support.ts b/src/tasks/detached-task-runtime.test-support.ts new file mode 100644 index 000000000000..05647a3db075 --- /dev/null +++ b/src/tasks/detached-task-runtime.test-support.ts @@ -0,0 +1,13 @@ +import type { DetachedTaskLifecycleRuntime } from "./detached-task-runtime-contract.js"; +import { + clearDetachedTaskLifecycleRuntimeRegistration, + registerDetachedTaskLifecycleRuntime, +} from "./detached-task-runtime-state.js"; + +export function setDetachedTaskLifecycleRuntime(runtime: DetachedTaskLifecycleRuntime): void { + registerDetachedTaskLifecycleRuntime("__test__", runtime); +} + +export function resetDetachedTaskLifecycleRuntimeForTests(): void { + clearDetachedTaskLifecycleRuntimeRegistration(); +} diff --git a/src/tasks/detached-task-runtime.test.ts b/src/tasks/detached-task-runtime.test.ts index 2a4013611f84..6be1ba62f78a 100644 --- a/src/tasks/detached-task-runtime.test.ts +++ b/src/tasks/detached-task-runtime.test.ts @@ -13,13 +13,15 @@ import { finalizeTaskRunByRunId, getDetachedTaskLifecycleRuntime, recordTaskRunProgressByRunId, - resetDetachedTaskLifecycleRuntimeForTests, - setDetachedTaskLifecycleRuntime, setDetachedTaskDeliveryStatusByRunId, startTaskRunByRunId, tryRecoverTaskBeforeMarkLost, } from "./detached-task-runtime.js"; import type { TaskRecord } from "./task-registry.types.js"; +import { + resetDetachedTaskLifecycleRuntimeForTests, + setDetachedTaskLifecycleRuntime, +} from "./task-runtime.test-helpers.js"; const { mockFindTaskByRunIdForStatus, mockListTasksForSessionKeyForStatus, mockLogWarn } = vi.hoisted(() => ({ diff --git a/src/tasks/detached-task-runtime.ts b/src/tasks/detached-task-runtime.ts index e7d31a544857..74e13b1a20ea 100644 --- a/src/tasks/detached-task-runtime.ts +++ b/src/tasks/detached-task-runtime.ts @@ -8,11 +8,7 @@ import type { DetachedTaskFinalizeParams, DetachedTaskLifecycleRuntime, } from "./detached-task-runtime-contract.js"; -import { - clearDetachedTaskLifecycleRuntimeRegistration, - getRegisteredDetachedTaskLifecycleRuntime, - registerDetachedTaskLifecycleRuntime, -} from "./detached-task-runtime-state.js"; +import { getRegisteredDetachedTaskLifecycleRuntime } from "./detached-task-runtime-state.js"; import { cancelTaskById as cancelDetachedTaskRunByIdInCore } from "./runtime-internal.js"; import { completeTaskRunByRunId as completeTaskRunByRunIdFromExecutor, @@ -74,14 +70,6 @@ export function getDetachedTaskLifecycleRuntime(): DetachedTaskLifecycleRuntime return getRegisteredDetachedTaskLifecycleRuntime() ?? DEFAULT_DETACHED_TASK_LIFECYCLE_RUNTIME; } -export function setDetachedTaskLifecycleRuntime(runtime: DetachedTaskLifecycleRuntime): void { - registerDetachedTaskLifecycleRuntime("__test__", runtime); -} - -export function resetDetachedTaskLifecycleRuntimeForTests(): void { - clearDetachedTaskLifecycleRuntimeRegistration(); -} - export function createQueuedTaskRun( ...args: Parameters ): ReturnType { diff --git a/src/tasks/generated-media-task-activity.test-support.ts b/src/tasks/generated-media-task-activity.test-support.ts new file mode 100644 index 000000000000..8d61adc4b0b8 --- /dev/null +++ b/src/tasks/generated-media-task-activity.test-support.ts @@ -0,0 +1,19 @@ +import "./generated-media-task-activity.js"; + +type GeneratedMediaTaskActivityTestApi = { + resetGeneratedMediaTaskActivityForTests(): void; +}; + +function getTestApi(): GeneratedMediaTaskActivityTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.generatedMediaTaskActivityTestApi") + ]; + if (!api) { + throw new Error("generated media task activity test API is unavailable"); + } + return api as GeneratedMediaTaskActivityTestApi; +} + +export function resetGeneratedMediaTaskActivityForTests(): void { + getTestApi().resetGeneratedMediaTaskActivityForTests(); +} diff --git a/src/tasks/generated-media-task-activity.ts b/src/tasks/generated-media-task-activity.ts index 1b71d878c859..ed6032c63c80 100644 --- a/src/tasks/generated-media-task-activity.ts +++ b/src/tasks/generated-media-task-activity.ts @@ -54,7 +54,13 @@ export function getLatestGeneratedMediaTaskAdmissionIdForSessionKey( return getLatestGeneratedMediaTaskAdmissions().get(sessionKey); } -export function resetGeneratedMediaTaskActivityForTests(): void { +function resetGeneratedMediaTaskActivityForTests(): void { getActiveGeneratedMediaTasks().clear(); getLatestGeneratedMediaTaskAdmissions().clear(); } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[ + Symbol.for("openclaw.generatedMediaTaskActivityTestApi") + ] = { resetGeneratedMediaTaskActivityForTests }; +} diff --git a/src/tasks/task-executor.test.ts b/src/tasks/task-executor.test.ts index c245b4e49db1..fbdeb5afb21d 100644 --- a/src/tasks/task-executor.test.ts +++ b/src/tasks/task-executor.test.ts @@ -5,11 +5,7 @@ import { resetSystemEventsForTest } from "../infra/system-events.js"; import { withStateDirEnv } from "../test-helpers/state-dir-env.js"; import { captureEnv } from "../test-utils/env.js"; import { SUBAGENT_KILL_TASK_ERROR } from "./detached-task-runtime-contract.js"; -import { - getDetachedTaskLifecycleRuntime, - resetDetachedTaskLifecycleRuntimeForTests, - setDetachedTaskLifecycleRuntime, -} from "./detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "./detached-task-runtime.js"; import { cancelFlowById, cancelFlowByIdForOwner, @@ -27,22 +23,26 @@ import { createManagedTaskFlow as createManagedTaskFlowOrNull, getTaskFlowById, listTaskFlowRecords, - resetTaskFlowRegistryForTests, } from "./task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; import { - setTaskRegistryDeliveryRuntimeForTests, getTaskById, findTaskByRunId, listTasksForFlowId, markTaskTerminalById, +} from "./task-registry.js"; +import type { TaskRecord } from "./task-registry.types.js"; +import { + configureTaskFlowRegistryRuntime, + resetDetachedTaskLifecycleRuntimeForTests, resetTaskRegistryControlRuntimeForTests, resetTaskRegistryDeliveryRuntimeForTests, resetTaskRegistryForTests, + resetTaskFlowRegistryForTests, + setDetachedTaskLifecycleRuntime, setTaskRegistryControlRuntimeForTests, -} from "./task-registry.js"; -import type { TaskRecord } from "./task-registry.types.js"; + setTaskRegistryDeliveryRuntimeForTests, +} from "./task-runtime.test-helpers.js"; const ORIGINAL_ENV = captureEnv(["OPENCLAW_STATE_DIR"]); diff --git a/src/tasks/task-flow-owner-access.test.ts b/src/tasks/task-flow-owner-access.test.ts index 9720fe19891b..f324f6761088 100644 --- a/src/tasks/task-flow-owner-access.test.ts +++ b/src/tasks/task-flow-owner-access.test.ts @@ -6,12 +6,12 @@ import { listTaskFlowsForOwner, resolveTaskFlowForLookupTokenForOwner, } from "./task-flow-owner-access.js"; -import { - createManagedTaskFlow as createManagedTaskFlowOrNull, - resetTaskFlowRegistryForTests, -} from "./task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; +import { createManagedTaskFlow as createManagedTaskFlowOrNull } from "./task-flow-registry.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; +import { + configureTaskFlowRegistryRuntime, + resetTaskFlowRegistryForTests, +} from "./task-runtime.test-helpers.js"; function createManagedTaskFlow( params: Parameters[0], diff --git a/src/tasks/task-flow-registry.audit.test.ts b/src/tasks/task-flow-registry.audit.test.ts index c1aa1cbbded8..fbfbd446d8c1 100644 --- a/src/tasks/task-flow-registry.audit.test.ts +++ b/src/tasks/task-flow-registry.audit.test.ts @@ -15,16 +15,16 @@ import { import { createManagedTaskFlow as createManagedTaskFlowOrNull, requestFlowCancel, - resetTaskFlowRegistryForTests, setFlowWaiting, } from "./task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; +import type { TaskRecord } from "./task-registry.types.js"; import { + configureTaskFlowRegistryRuntime, resetTaskRegistryDeliveryRuntimeForTests, resetTaskRegistryForTests, -} from "./task-registry.js"; -import type { TaskRecord } from "./task-registry.types.js"; + resetTaskFlowRegistryForTests, +} from "./task-runtime.test-helpers.js"; const ORIGINAL_ENV = captureEnv(["OPENCLAW_STATE_DIR"]); diff --git a/src/tasks/task-flow-registry.maintenance.test.ts b/src/tasks/task-flow-registry.maintenance.test.ts index 020d09a6d199..f530b78687d5 100644 --- a/src/tasks/task-flow-registry.maintenance.test.ts +++ b/src/tasks/task-flow-registry.maintenance.test.ts @@ -5,12 +5,10 @@ import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { SUBAGENT_KILL_TASK_ERROR } from "./detached-task-runtime-contract.js"; import { createRunningTaskRun as createRunningTaskRunOrNull } from "./task-executor.js"; import { - createFlowRecord as createFlowRecordOrNull, createManagedTaskFlow as createManagedTaskFlowOrNull, getTaskFlowById, listTaskFlowRecords, requestFlowCancel, - resetTaskFlowRegistryForTests, } from "./task-flow-registry.js"; import { getInspectableTaskFlowAuditSummary, @@ -18,12 +16,14 @@ import { runTaskFlowRegistryMaintenance, } from "./task-flow-registry.maintenance.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; +import { finalizeTaskRunByRunId } from "./task-registry.js"; +import type { TaskRecord } from "./task-registry.types.js"; import { - finalizeTaskRunByRunId, + createFlowRecord as createFlowRecordOrNull, resetTaskRegistryDeliveryRuntimeForTests, resetTaskRegistryForTests, -} from "./task-registry.js"; -import type { TaskRecord } from "./task-registry.types.js"; + resetTaskFlowRegistryForTests, +} from "./task-runtime.test-helpers.js"; const ORIGINAL_ENV = captureEnv(["OPENCLAW_STATE_DIR"]); diff --git a/src/tasks/task-flow-registry.store.test-support.ts b/src/tasks/task-flow-registry.store.test-support.ts new file mode 100644 index 000000000000..1d3da1304aaa --- /dev/null +++ b/src/tasks/task-flow-registry.store.test-support.ts @@ -0,0 +1,35 @@ +import type { TaskFlowRegistryObserverEvent } from "./task-flow-registry.store.js"; +import type { TaskFlowRegistryStoreSnapshot } from "./task-flow-registry.store.types.js"; +import "./task-flow-registry.store.js"; +import type { TaskFlowRecord } from "./task-flow-registry.types.js"; + +type TaskFlowRegistryStore = { + loadSnapshot: () => TaskFlowRegistryStoreSnapshot; + saveSnapshot: (snapshot: TaskFlowRegistryStoreSnapshot) => void; + upsertFlow?: (flow: TaskFlowRecord) => void; + deleteFlow?: (flowId: string) => void; + close?: () => void; +}; + +type TaskFlowRegistryStoreTestApi = { + configureTaskFlowRegistryRuntime(params: { + store?: TaskFlowRegistryStore; + observers?: { onEvent?: (event: TaskFlowRegistryObserverEvent) => void } | null; + }): void; +}; + +function getTestApi(): TaskFlowRegistryStoreTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.taskFlowRegistryStoreTestApi") + ]; + if (!api) { + throw new Error("task flow registry store test API is unavailable"); + } + return api as TaskFlowRegistryStoreTestApi; +} + +export function configureTaskFlowRegistryRuntime( + params: Parameters[0], +): void { + getTestApi().configureTaskFlowRegistryRuntime(params); +} diff --git a/src/tasks/task-flow-registry.store.test.ts b/src/tasks/task-flow-registry.store.test.ts index a7b8ab6fe938..eefad3b4a9da 100644 --- a/src/tasks/task-flow-registry.store.test.ts +++ b/src/tasks/task-flow-registry.store.test.ts @@ -11,10 +11,8 @@ import { createManagedTaskFlow as createManagedTaskFlowOrNull, getTaskFlowById, requestFlowCancel, - resetTaskFlowRegistryForTests, setFlowWaiting, } from "./task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; import { loadTaskFlowRegistryStateFromSqlite, saveTaskFlowRegistryStateToSqlite, @@ -25,6 +23,10 @@ import { type TaskFlowRecord, } from "./task-flow-registry.types.js"; import { parseTaskNotifyPolicy } from "./task-registry.types.js"; +import { + configureTaskFlowRegistryRuntime, + resetTaskFlowRegistryForTests, +} from "./task-runtime.test-helpers.js"; function createManagedTaskFlow( params: Parameters[0], diff --git a/src/tasks/task-flow-registry.store.ts b/src/tasks/task-flow-registry.store.ts index c6ad9ee65789..1ee8a1b6331e 100644 --- a/src/tasks/task-flow-registry.store.ts +++ b/src/tasks/task-flow-registry.store.ts @@ -57,7 +57,7 @@ export function getTaskFlowRegistryObservers(): TaskFlowRegistryObservers | null return configuredFlowRegistryObservers; } -export function configureTaskFlowRegistryRuntime(params: { +function configureTaskFlowRegistryRuntime(params: { store?: TaskFlowRegistryStore; observers?: TaskFlowRegistryObservers | null; }) { @@ -74,3 +74,9 @@ export function resetTaskFlowRegistryRuntimeForTests() { configuredFlowRegistryStore = defaultFlowRegistryStore; configuredFlowRegistryObservers = null; } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[ + Symbol.for("openclaw.taskFlowRegistryStoreTestApi") + ] = { configureTaskFlowRegistryRuntime }; +} diff --git a/src/tasks/task-flow-registry.test-support.ts b/src/tasks/task-flow-registry.test-support.ts new file mode 100644 index 000000000000..c8e7faad4dee --- /dev/null +++ b/src/tasks/task-flow-registry.test-support.ts @@ -0,0 +1,51 @@ +import type { + JsonValue, + TaskFlowRecord, + TaskFlowStatus, + TaskFlowSyncMode, +} from "./task-flow-registry.types.js"; +import "./task-flow-registry.js"; +import type { TaskNotifyPolicy } from "./task-registry.types.js"; + +type CreateFlowRecordParams = { + ownerKey: string; + requesterOrigin?: TaskFlowRecord["requesterOrigin"]; + status?: TaskFlowStatus; + notifyPolicy?: TaskNotifyPolicy; + goal: string; + currentStep?: string | null; + blockedTaskId?: string | null; + blockedSummary?: string | null; + stateJson?: JsonValue | null; + waitJson?: JsonValue | null; + cancelRequestedAt?: number | null; + createdAt?: number; + updatedAt?: number; + endedAt?: number | null; + syncMode?: TaskFlowSyncMode; + controllerId?: string | null; + revision?: number; +}; + +type TaskFlowRegistryTestApi = { + createFlowRecord(params: CreateFlowRecordParams): TaskFlowRecord | null; + resetTaskFlowRegistryForTests(opts?: { persist?: boolean }): void; +}; + +function getTestApi(): TaskFlowRegistryTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.taskFlowRegistryTestApi") + ]; + if (!api) { + throw new Error("task flow registry test API is unavailable"); + } + return api as TaskFlowRegistryTestApi; +} + +export function createFlowRecord(params: CreateFlowRecordParams): TaskFlowRecord | null { + return getTestApi().createFlowRecord(params); +} + +export function resetTaskFlowRegistryForTests(opts?: { persist?: boolean }): void { + getTestApi().resetTaskFlowRegistryForTests(opts); +} diff --git a/src/tasks/task-flow-registry.test.ts b/src/tasks/task-flow-registry.test.ts index c3c572e77b60..f4964bcada48 100644 --- a/src/tasks/task-flow-registry.test.ts +++ b/src/tasks/task-flow-registry.test.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { - createFlowRecord as createFlowRecordOrNull, createTaskFlowForTask as createTaskFlowForTaskOrNull, createManagedTaskFlow as createManagedTaskFlowOrNull, deleteTaskFlowRecordById, @@ -12,14 +11,17 @@ import { listTaskFlowRecords, requestFlowCancel, reloadTaskFlowRegistryFromStore, - resetTaskFlowRegistryForTests, resumeFlow, setFlowWaiting, syncFlowFromTaskResult, updateFlowRecordByIdExpectedRevision, } from "./task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; +import { + configureTaskFlowRegistryRuntime, + createFlowRecord as createFlowRecordOrNull, + resetTaskFlowRegistryForTests, +} from "./task-runtime.test-helpers.js"; function createFlowRecord(params: Parameters[0]): TaskFlowRecord { const flow = createFlowRecordOrNull(params); diff --git a/src/tasks/task-flow-registry.ts b/src/tasks/task-flow-registry.ts index 575e934bc360..426cdc215186 100644 --- a/src/tasks/task-flow-registry.ts +++ b/src/tasks/task-flow-registry.ts @@ -468,7 +468,7 @@ function writeFlowRecord(next: TaskFlowRecord, previous?: TaskFlowRecord): TaskF return cloneFlowRecord(next); } -export function createFlowRecord(params: CreateFlowRecordParams): TaskFlowRecord | null { +function createFlowRecord(params: CreateFlowRecordParams): TaskFlowRecord | null { ensureTaskFlowRegistryReady(); const record = buildFlowRecord(params); return writeFlowRecord(record); @@ -812,7 +812,7 @@ export function deleteTaskFlowRecordById(flowId: string): boolean { return true; } -export function resetTaskFlowRegistryForTests(opts?: { persist?: boolean }) { +function resetTaskFlowRegistryForTests(opts?: { persist?: boolean }) { flows = new Map(); taskFlowRegistryRestoreState = { status: "uninitialized" }; resetTaskFlowRegistryRuntimeForTests(); @@ -821,4 +821,11 @@ export function resetTaskFlowRegistryForTests(opts?: { persist?: boolean }) { } getTaskFlowRegistryStore().close?.(); } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.taskFlowRegistryTestApi")] = { + createFlowRecord, + resetTaskFlowRegistryForTests, + }; +} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/tasks/task-owner-access.test.ts b/src/tasks/task-owner-access.test.ts index 9a310851a0a3..665eb7d3e0ed 100644 --- a/src/tasks/task-owner-access.test.ts +++ b/src/tasks/task-owner-access.test.ts @@ -8,11 +8,9 @@ import { getTaskByIdForOwner, resolveTaskForLookupTokenForOwner, } from "./task-owner-access.js"; -import { - createTaskRecord as createTaskRecordOrNull, - resetTaskRegistryForTests, -} from "./task-registry.js"; +import { createTaskRecord as createTaskRecordOrNull } from "./task-registry.js"; import type { TaskRecord } from "./task-registry.types.js"; +import { resetTaskRegistryForTests } from "./task-runtime.test-helpers.js"; const ORIGINAL_ENV = captureEnv(["OPENCLAW_STATE_DIR"]); diff --git a/src/tasks/task-registry.maintenance.issue-60299.test.ts b/src/tasks/task-registry.maintenance.issue-60299.test.ts index 02b93af570d8..7c413acb3da7 100644 --- a/src/tasks/task-registry.maintenance.issue-60299.test.ts +++ b/src/tasks/task-registry.maintenance.issue-60299.test.ts @@ -3,11 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { AcpSessionStoreEntry } from "../acp/runtime/session-meta.js"; import type { SessionEntry } from "../config/sessions.js"; import type { ParsedAgentSessionKey } from "../routing/session-key.js"; -import { - resetDetachedTaskLifecycleRuntimeForTests, - setDetachedTaskLifecycleRuntime, - getDetachedTaskLifecycleRuntime, -} from "./detached-task-runtime.js"; +import { getDetachedTaskLifecycleRuntime } from "./detached-task-runtime.js"; import { CRON_HISTORY_KEEP_PER_JOB, getInspectableActiveTaskRestartBlockers, @@ -20,6 +16,10 @@ import { stopTaskRegistryMaintenance, } from "./task-registry.maintenance.js"; import type { TaskRecord } from "./task-registry.types.js"; +import { + resetDetachedTaskLifecycleRuntimeForTests, + setDetachedTaskLifecycleRuntime, +} from "./task-runtime.test-helpers.js"; const GRACE_EXPIRED_MS = 10 * 60_000; diff --git a/src/tasks/task-registry.store.test.ts b/src/tasks/task-registry.store.test.ts index cf1af86fa924..d4db16617ee8 100644 --- a/src/tasks/task-registry.store.test.ts +++ b/src/tasks/task-registry.store.test.ts @@ -22,10 +22,7 @@ import { import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; import { captureEnv } from "../test-utils/env.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; -import { - createManagedTaskFlow as createManagedTaskFlowOrNull, - resetTaskFlowRegistryForTests, -} from "./task-flow-registry.js"; +import { createManagedTaskFlow as createManagedTaskFlowOrNull } from "./task-flow-registry.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; import { createTaskRecord as createTaskRecordOrNull, @@ -34,9 +31,7 @@ import { getTaskById, listFreshTasksForOwnerKey, markTaskTerminalById, - maybeDeliverTaskStateChangeUpdate, reloadTaskRegistryFromStore, - resetTaskRegistryForTests, updateTaskNotifyPolicyById, } from "./task-registry.js"; import { @@ -56,6 +51,11 @@ import { parseTaskScopeKind, parseTaskStatus, } from "./task-registry.types.js"; +import { + maybeDeliverTaskStateChangeUpdate, + resetTaskFlowRegistryForTests, + resetTaskRegistryForTests, +} from "./task-runtime.test-helpers.js"; const ORIGINAL_ENV = captureEnv(["OPENCLAW_STATE_DIR"]); diff --git a/src/tasks/task-registry.test-support.ts b/src/tasks/task-registry.test-support.ts new file mode 100644 index 000000000000..4719cafd745f --- /dev/null +++ b/src/tasks/task-registry.test-support.ts @@ -0,0 +1,57 @@ +import type { TaskRegistryControlRuntime } from "./task-registry-control.types.js"; +import type { TaskEventRecord, TaskRecord } from "./task-registry.types.js"; +import "./task-registry.js"; + +type TaskRegistryDeliveryRuntime = Pick< + typeof import("./task-registry-delivery-runtime.js"), + "sendMessage" +>; + +type TaskRegistryTestApi = { + maybeDeliverTaskStateChangeUpdate( + taskId: string, + latestEvent?: TaskEventRecord, + ): Promise; + resetTaskRegistryForTests(opts?: { persist?: boolean }): void; + resetTaskRegistryDeliveryRuntimeForTests(): void; + setTaskRegistryDeliveryRuntimeForTests(runtime: TaskRegistryDeliveryRuntime): void; + resetTaskRegistryControlRuntimeForTests(): void; + setTaskRegistryControlRuntimeForTests(runtime: TaskRegistryControlRuntime): void; +}; + +function getTestApi(): TaskRegistryTestApi { + const api = (globalThis as Record)[ + Symbol.for("openclaw.taskRegistryTestApi") + ]; + if (!api) { + throw new Error("task registry test API is unavailable"); + } + return api as TaskRegistryTestApi; +} + +export async function maybeDeliverTaskStateChangeUpdate( + taskId: string, + latestEvent?: TaskEventRecord, +): Promise { + return await getTestApi().maybeDeliverTaskStateChangeUpdate(taskId, latestEvent); +} + +export function resetTaskRegistryForTests(opts?: { persist?: boolean }): void { + getTestApi().resetTaskRegistryForTests(opts); +} + +export function resetTaskRegistryDeliveryRuntimeForTests(): void { + getTestApi().resetTaskRegistryDeliveryRuntimeForTests(); +} + +export function setTaskRegistryDeliveryRuntimeForTests(runtime: TaskRegistryDeliveryRuntime): void { + getTestApi().setTaskRegistryDeliveryRuntimeForTests(runtime); +} + +export function resetTaskRegistryControlRuntimeForTests(): void { + getTestApi().resetTaskRegistryControlRuntimeForTests(); +} + +export function setTaskRegistryControlRuntimeForTests(runtime: TaskRegistryControlRuntime): void { + getTestApi().setTaskRegistryControlRuntimeForTests(runtime); +} diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts index 81fade65067e..25e4e4750d8f 100644 --- a/src/tasks/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -32,9 +32,7 @@ import { createManagedTaskFlow as createManagedTaskFlowOrNull, getTaskFlowById, requestFlowCancel, - resetTaskFlowRegistryForTests, } from "./task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; import type { TaskFlowRecord } from "./task-flow-registry.types.js"; import { cancelTaskById, @@ -49,18 +47,12 @@ import { listTasksForRelatedSessionKey, listTaskRecords, linkTaskToFlowById, - maybeDeliverTaskStateChangeUpdate, maybeDeliverTaskTerminalUpdate, markTaskRunningByRunId, markTaskTerminalById, recordTaskProgressByRunId, reloadTaskRegistryFromStore, - resetTaskRegistryControlRuntimeForTests, - resetTaskRegistryDeliveryRuntimeForTests, - resetTaskRegistryForTests, resolveTaskForLookupToken, - setTaskRegistryControlRuntimeForTests, - setTaskRegistryDeliveryRuntimeForTests, updateTaskNotifyPolicyById, } from "./task-registry.js"; import { @@ -80,6 +72,16 @@ import { import { configureTaskRegistryRuntime } from "./task-registry.store.js"; import { summarizeTaskRecords } from "./task-registry.summary.js"; import type { TaskDeliveryState, TaskRecord } from "./task-registry.types.js"; +import { + configureTaskFlowRegistryRuntime, + maybeDeliverTaskStateChangeUpdate, + resetTaskFlowRegistryForTests, + resetTaskRegistryControlRuntimeForTests, + resetTaskRegistryDeliveryRuntimeForTests, + resetTaskRegistryForTests, + setTaskRegistryControlRuntimeForTests, + setTaskRegistryDeliveryRuntimeForTests, +} from "./task-runtime.test-helpers.js"; const DEFAULT_TASK_RETENTION_MS = 7 * 24 * 60 * 60_000; const LOST_TASK_RETENTION_MS = 24 * 60 * 60_000; diff --git a/src/tasks/task-registry.ts b/src/tasks/task-registry.ts index 4b62b0ea6c32..e98a4bebac81 100644 --- a/src/tasks/task-registry.ts +++ b/src/tasks/task-registry.ts @@ -1607,7 +1607,7 @@ async function maybeDeliverTaskTerminalUpdateUnderAdmission( } } -export async function maybeDeliverTaskStateChangeUpdate( +async function maybeDeliverTaskStateChangeUpdate( taskId: string, latestEvent?: TaskEventRecord, ): Promise { @@ -2735,7 +2735,7 @@ export function deleteTaskRecordById(taskId: string): boolean { return true; } -export function resetTaskRegistryForTests(opts?: { persist?: boolean }) { +function resetTaskRegistryForTests(opts?: { persist?: boolean }) { clearTaskRegistryMemory(); taskRegistryRestoreState = { status: "uninitialized" }; resetTaskRegistryRuntimeForTests(); @@ -2754,31 +2754,42 @@ export function resetTaskRegistryForTests(opts?: { persist?: boolean }) { getTaskRegistryStore().close?.(); } -export function resetTaskRegistryDeliveryRuntimeForTests() { +function resetTaskRegistryDeliveryRuntimeForTests() { (globalThis as TaskRegistryGlobalWithRuntimeOverrides)[ TASK_REGISTRY_DELIVERY_RUNTIME_OVERRIDE_KEY ] = null; deliveryRuntimeLoader.clear(); } -export function setTaskRegistryDeliveryRuntimeForTests(runtime: TaskRegistryDeliveryRuntime): void { +function setTaskRegistryDeliveryRuntimeForTests(runtime: TaskRegistryDeliveryRuntime): void { (globalThis as TaskRegistryGlobalWithRuntimeOverrides)[ TASK_REGISTRY_DELIVERY_RUNTIME_OVERRIDE_KEY ] = runtime; deliveryRuntimeLoader.clear(); } -export function resetTaskRegistryControlRuntimeForTests() { +function resetTaskRegistryControlRuntimeForTests() { (globalThis as TaskRegistryGlobalWithRuntimeOverrides)[ TASK_REGISTRY_CONTROL_RUNTIME_OVERRIDE_KEY ] = null; controlRuntimeLoader.clear(); } -export function setTaskRegistryControlRuntimeForTests(runtime: TaskRegistryControlRuntime): void { +function setTaskRegistryControlRuntimeForTests(runtime: TaskRegistryControlRuntime): void { (globalThis as TaskRegistryGlobalWithRuntimeOverrides)[ TASK_REGISTRY_CONTROL_RUNTIME_OVERRIDE_KEY ] = runtime; controlRuntimeLoader.clear(); } + +if (process.env.VITEST || process.env.NODE_ENV === "test") { + (globalThis as Record)[Symbol.for("openclaw.taskRegistryTestApi")] = { + maybeDeliverTaskStateChangeUpdate, + resetTaskRegistryControlRuntimeForTests, + resetTaskRegistryDeliveryRuntimeForTests, + resetTaskRegistryForTests, + setTaskRegistryControlRuntimeForTests, + setTaskRegistryDeliveryRuntimeForTests, + }; +} /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/tasks/task-runtime.test-helpers.ts b/src/tasks/task-runtime.test-helpers.ts index 456e50d2292c..9fcb233f3c6c 100644 --- a/src/tasks/task-runtime.test-helpers.ts +++ b/src/tasks/task-runtime.test-helpers.ts @@ -1,9 +1,19 @@ // Test-only access to task registry reset and runtime-injection hooks. -export { resetTaskFlowRegistryForTests } from "./task-flow-registry.js"; export { + resetDetachedTaskLifecycleRuntimeForTests, + setDetachedTaskLifecycleRuntime, +} from "./detached-task-runtime.test-support.js"; +export { resetGeneratedMediaTaskActivityForTests } from "./generated-media-task-activity.test-support.js"; +export { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.test-support.js"; +export { + createFlowRecord, + resetTaskFlowRegistryForTests, +} from "./task-flow-registry.test-support.js"; +export { + maybeDeliverTaskStateChangeUpdate, resetTaskRegistryControlRuntimeForTests, resetTaskRegistryDeliveryRuntimeForTests, resetTaskRegistryForTests, setTaskRegistryControlRuntimeForTests, setTaskRegistryDeliveryRuntimeForTests, -} from "./task-registry.js"; +} from "./task-registry.test-support.js"; diff --git a/src/tasks/task-status-access.test.ts b/src/tasks/task-status-access.test.ts index 8c56cc02a773..1e387aea12cc 100644 --- a/src/tasks/task-status-access.test.ts +++ b/src/tasks/task-status-access.test.ts @@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { clearGeneratedMediaTaskActivity, registerGeneratedMediaTaskActivity, - resetGeneratedMediaTaskActivityForTests, } from "./generated-media-task-activity.js"; +import { resetGeneratedMediaTaskActivityForTests } from "./task-runtime.test-helpers.js"; import { getGeneratedMediaTaskIdsForSessionKey, hasNewGeneratedMediaTaskForSessionKey, diff --git a/test/helpers/acp-manager-task-state.ts b/test/helpers/acp-manager-task-state.ts index 49de20c067fc..9721b3a79776 100644 --- a/test/helpers/acp-manager-task-state.ts +++ b/test/helpers/acp-manager-task-state.ts @@ -1,7 +1,10 @@ // ACP manager task state helper resets task flow state for ACP tests. -import { resetTaskFlowRegistryForTests } from "../../src/tasks/task-flow-registry.js"; -import { configureTaskFlowRegistryRuntime } from "../../src/tasks/task-flow-registry.store.js"; -import { findTaskByRunId, resetTaskRegistryForTests } from "../../src/tasks/task-registry.js"; +import { findTaskByRunId } from "../../src/tasks/task-registry.js"; +import { + configureTaskFlowRegistryRuntime, + resetTaskFlowRegistryForTests, + resetTaskRegistryForTests, +} from "../../src/tasks/task-runtime.test-helpers.js"; import { withTempDir } from "../../src/test-helpers/temp-dir.js"; import { captureEnv, setTestEnvValue } from "../../src/test-utils/env.js"; import { installInMemoryTaskRegistryRuntime } from "../../src/test-utils/task-registry-runtime.js"; diff --git a/test/scripts/docker-e2e-clients.test.ts b/test/scripts/docker-e2e-clients.test.ts index 40d27035c9a4..9b2f15bf2166 100644 --- a/test/scripts/docker-e2e-clients.test.ts +++ b/test/scripts/docker-e2e-clients.test.ts @@ -11,6 +11,7 @@ describe("Docker E2E client scripts", () => { const source = readScript("scripts/e2e/commitments-safety-docker-client.ts"); expect(source).toContain("../../dist/commitments/runtime.js"); + expect(source).toContain("../../dist/commitments/runtime.test-support.js"); expect(source).toContain("../../dist/commitments/store.js"); expect(source).toContain("verifyQueueCap()"); expect(source).toContain("verifyExtractionStoresTypedMetadataOnly()"); diff --git a/tsdown.config.ts b/tsdown.config.ts index 8bfec4995fbc..d81f9aa3e068 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -323,6 +323,7 @@ function buildDockerE2eHarnessEntries(): Record { "auto-reply/reply/commands-system-agent": "src/auto-reply/reply/commands-system-agent.ts", "cli/run-main": "src/cli/run-main.ts", "commitments/runtime": "src/commitments/runtime.ts", + "commitments/runtime.test-support": "src/commitments/runtime.test-support.ts", "commitments/store": "src/commitments/store.ts", "config/config": "src/config/config.ts", "system-agent/system-agent": "src/system-agent/system-agent.ts",