From d035dba6c768117c673a6326252f1e1bc9182a56 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Tue, 11 Aug 2026 15:06:46 -0700 Subject: [PATCH] chore: scope SQLite session lifecycle CI (#121917) * oc-358: scope SQLite lifecycle CI * chore(ci): cover shared session lifecycle owner --- .github/workflows/ci.yml | 63 +++++++++++---- docs/ci.md | 19 ++--- scripts/lib/ci-changed-node-test-plan.mts | 46 +++++++++++ .../scripts/ci-changed-node-test-plan.test.ts | 41 ++++++++++ test/scripts/ci-workflow-guards.test.ts | 76 ++++++++++++++++--- test/scripts/vitest-e2e-global-setup.test.ts | 8 ++ test/vitest/vitest.e2e.global-setup.ts | 10 ++- 7 files changed, 230 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bd7529c91a9..0eb240614d4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,7 @@ jobs: frozen_target: ${{ steps.manifest.outputs.frozen_target }} run_qa_smoke_ci: ${{ steps.manifest.outputs.run_qa_smoke_ci }} run_prompt_snapshots: ${{ steps.manifest.outputs.run_prompt_snapshots }} + run_sqlite_session_lifecycle: ${{ steps.manifest.outputs.run_sqlite_session_lifecycle }} checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }} run_plugin_contracts_shards: ${{ steps.manifest.outputs.run_plugin_contracts_shards }} plugin_contracts_matrix: ${{ steps.manifest.outputs.plugin_contracts_matrix }} @@ -681,7 +682,20 @@ jobs: eventName !== "pull_request" || typeof changedNodeTestPlan.hasPromptSnapshotAffectingChange !== "function" || changedNodeTestPlan.hasPromptSnapshotAffectingChange(changedPaths); - const runBuildArtifacts = runNodeFull && changedScopeHasBuildImpact; + const supportsSqliteSessionLifecycleProof = existsSync( + "test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts", + ); + const changedScopeHasSqliteSessionLifecycleImpact = + changedPaths === null || + eventName === "workflow_dispatch" || + typeof changedNodeTestPlan.hasSqliteSessionLifecycleAffectingChange !== "function" || + changedNodeTestPlan.hasSqliteSessionLifecycleAffectingChange(changedPaths); + const runSqliteSessionLifecycle = + runNodeFull && + supportsSqliteSessionLifecycleProof && + changedScopeHasSqliteSessionLifecycleImpact; + const runBuildArtifacts = + runNodeFull && (changedScopeHasBuildImpact || runSqliteSessionLifecycle); const runQaSmokeCi = runNodeFull && changedScopeHasQaImpact && @@ -752,6 +766,7 @@ jobs: compatibility_target: compatibilityTarget, run_qa_smoke_ci: runQaSmokeCi, run_prompt_snapshots: runNodeFull && changedScopeHasPromptSnapshotImpact, + run_sqlite_session_lifecycle: runSqliteSessionLifecycle, checks_fast_core_matrix: createMatrix(checksFastCoreTasks), run_plugin_contracts_shards: runPluginContractShards, plugin_contracts_matrix: createMatrix( @@ -1437,6 +1452,38 @@ jobs: path: .local/gateway-watch-regression/ retention-days: 7 + sqlite-session-lifecycle: + permissions: + contents: read + name: check-sqlite-session-lifecycle + needs: [preflight, build-artifacts] + if: ${{ !cancelled() && always() && needs.preflight.outputs.run_sqlite_session_lifecycle == 'true' && needs.build-artifacts.result == 'success' }} + runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }} + timeout-minutes: 20 + steps: + - *linux_node_checkout_step + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + sticky-disk: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'true' || 'false' }} + use-actions-cache: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw') && 'false' || 'true' }} + + - name: Download exact-run built runtime + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: dist-runtime-build + path: .artifacts/dist-runtime-build + + - name: Extract built runtime + run: tar --extract --file .artifacts/dist-runtime-build/dist-runtime-build.tar.zst --use-compress-program unzstd + + - name: Verify SQLite session lifecycle + env: + OPENCLAW_E2E_USE_PREBUILT_DIST: "1" + OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "660000" + run: node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts + native-i18n: permissions: contents: read @@ -2545,9 +2592,6 @@ jobs: - check_name: check-plugin-sdk-api-baseline group: plugin-sdk-api-baseline runner: blacksmith-4vcpu-ubuntu-2404 - - check_name: check-sqlite-session-flip-proof - group: sqlite-session-flip-proof - runner: blacksmith-8vcpu-ubuntu-2404 - check_name: check-additional-extension-package-boundary group: extension-package-boundary # Light-run critical-path pole: cold runs spend ~100s in nine @@ -2774,15 +2818,6 @@ jobs: plugin-sdk-api-baseline) run_check "plugin-sdk:api:check" pnpm run plugin-sdk:api:check ;; - sqlite-session-flip-proof) - if [ ! -f test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts ]; then - echo "[skip] SQLite sessions/transcripts flip proof is not present in this checkout" - else - # This lane owns a 420-second E2E case plus cold hosted-runner setup; - # keep the global watchdog strict and scope compatibility headroom here. - run_check "sqlite sessions/transcripts flip proof" env OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=660000 node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts - fi - ;; extension-package-boundary) run_check "test:extensions:package-boundary:compile" pnpm run test:extensions:package-boundary:compile run_check "test:extensions:package-boundary:canary" pnpm run test:extensions:package-boundary:canary @@ -3751,6 +3786,7 @@ jobs: - security-fast - pnpm-store-warmup - build-artifacts + - sqlite-session-lifecycle - native-i18n - checks-ui - checks-ui-e2e @@ -3784,6 +3820,7 @@ jobs: SELECTED_RESULTS: | pnpm-store-warmup=${{ needs.pnpm-store-warmup.result }} build-artifacts=${{ needs.build-artifacts.result }} + sqlite-session-lifecycle=${{ needs.sqlite-session-lifecycle.result }} native-i18n=${{ needs.native-i18n.result }} checks-ui=${{ needs.checks-ui.result }} checks-ui-e2e=${{ needs.checks-ui-e2e.result }} diff --git a/docs/ci.md b/docs/ci.md index 9397917a931d..7ed2e9e57fcb 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -102,6 +102,7 @@ Separate iOS and macOS Periphery workflows enforce a zero-findings dead-code pol - **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph. - **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed. - **TUI PTY** splits by proof ownership. The dedicated `core-runtime-tui-pty` Node shard owns the full real-backend suite against the exact-head built CLI. The `build-artifacts` job keeps only a local model roundtrip and a real Gateway connection canary, so every artifact boundary proves the built launcher without duplicating the full serial suite inside the build job. +- **SQLite session lifecycle** runs the built-CLI migration, restart, compaction, cleanup, and session RPC proof only when the diff touches its direct storage/session owners or a reachable session path in the embedded runner. The dedicated `check-sqlite-session-lifecycle` job downloads the exact runtime produced by `build-artifacts`; manual and release dispatches always select it when the target contains the proof. - **CI routing-only edits, the small set of core-test fixtures the fast task runs directly, and narrow plugin contract helper edits** use a fast Node-only manifest path: `preflight`, `security-fast`, and only the fast lanes the change touches — a single `checks-fast-core` CI-routing task, the two plugin contract shards, or both. That path skips build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices. - **Windows Node checks** are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes. @@ -182,15 +183,15 @@ for commands and recovery. ## Runners -| Runner | Jobs | -| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ubuntu-24.04` | `security-fast`, manual CI dispatch and non-canonical repository fallbacks, pull-request retries of both UI E2E jobs, the QA Smoke aggregate, CodeQL security and quality scans, workflow-sanity, labeler, auto-response, the standalone Docs workflow, and the whole Install Smoke workflow | -| `blacksmith-4vcpu-ubuntu-2404` | `preflight`, `pnpm-store-warmup`, `native-i18n`, `checks-fast-core` except QA Smoke CI, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-*` lanes except `check-lint`, selected `check-additional-*` shards, `check-docs`, and `skills-python` | -| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, first-attempt same-repo pull requests and pushes for the serial Chromium/Vite `checks-ui-e2e` lane (three Control UI shards plus one browser extension shard), boundary/extension-heavy `check-additional-*` shards, and `android` | -| `blacksmith-16vcpu-ubuntu-2404` | Automatic QA Smoke CI shards, first-attempt same-repo pull requests and pushes for `checks-ui-e2e-real-gateway`, `build-artifacts` in CI and Testbox, and `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved) | -| `blacksmith-8vcpu-windows-2025` | `checks-windows` | -| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | -| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` | +| Runner | Jobs | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ubuntu-24.04` | `security-fast`, manual CI dispatch and non-canonical repository fallbacks, pull-request retries of both UI E2E jobs, the QA Smoke aggregate, CodeQL security and quality scans, workflow-sanity, labeler, auto-response, the standalone Docs workflow, and the whole Install Smoke workflow | +| `blacksmith-4vcpu-ubuntu-2404` | `preflight`, `pnpm-store-warmup`, `native-i18n`, `checks-fast-core` except QA Smoke CI, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-*` lanes except `check-lint`, selected `check-additional-*` shards, `check-docs`, and `skills-python` | +| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, first-attempt same-repo pull requests and pushes for the serial Chromium/Vite `checks-ui-e2e` lane (three Control UI shards plus one browser extension shard), boundary/extension-heavy `check-additional-*` shards, `check-sqlite-session-lifecycle`, and `android` | +| `blacksmith-16vcpu-ubuntu-2404` | Automatic QA Smoke CI shards, first-attempt same-repo pull requests and pushes for `checks-ui-e2e-real-gateway`, `build-artifacts` in CI and Testbox, and `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved) | +| `blacksmith-8vcpu-windows-2025` | `checks-windows` | +| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | +| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` | ## Runner registration budget diff --git a/scripts/lib/ci-changed-node-test-plan.mts b/scripts/lib/ci-changed-node-test-plan.mts index 3348585b6b15..89860e30d00f 100644 --- a/scripts/lib/ci-changed-node-test-plan.mts +++ b/scripts/lib/ci-changed-node-test-plan.mts @@ -148,6 +148,52 @@ export function hasPromptSnapshotAffectingChange(changedPaths: string[], options return hasImportGraphImpactOnTargets(sourcePaths, [PROMPT_SNAPSHOT_ENTRY], cwd); } +// The lifecycle proof crosses dynamic Gateway method registration, doctor +// migrations, shared session coordination, the public session SDK, and the +// built CLI. Keep those owners on the direct surface; use the import graph only +// inside the embedded-runner neighborhood, whose session reachability is not +// apparent from filenames. +const SQLITE_SESSION_LIFECYCLE_PREFIX_RE = + /^(?:src\/(?:agents\/(?:sessions\/|[^/]*(?:session|transcript|compaction)[^/]*)|commands\/doctor-session-|config\/sessions\/|gateway\/(?:agent-turn\/agent-session-persist|server-chat\.(?:load-gateway-session-row|persist-session-lifecycle)|server-methods\/sessions|server\.sessions|session-|sessions-)|plugin-sdk\/session-|sessions\/|state\/openclaw-agent-(?:db|schema))|\.github\/actions\/setup-node-env\/)/u; +const SQLITE_SESSION_LIFECYCLE_EXACT_RE = + /^(?:src\/config\/sessions\.ts|test\/helpers\/(?:openclaw-test-instance|sqlite-sessions-transcripts-flip-proof(?:-assertions)?)\.ts|test\/scripts\/(?:sqlite-sessions-transcripts-flip-proof(?:\.built-cli)?\.e2e\.test|vitest-e2e-global-setup\.test)\.ts|test\/vitest\/vitest\.e2e\.(?:config|global-setup)\.ts|scripts\/lib\/ci-changed-node-test-plan\.mts|\.github\/workflows\/ci\.yml|openclaw\.mjs|package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml)$/u; +const SQLITE_SESSION_LIFECYCLE_ENTRY = + "test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts"; +const SQLITE_SESSION_LIFECYCLE_IMPORT_CANDIDATE_RE = /^src\/agents\/embedded-agent-runner\/run\//u; + +/** + * True when a changed path touches a SQLite session lifecycle owner or reaches + * the proof from the embedded-runner neighborhood. + */ +export function hasSqliteSessionLifecycleAffectingChange( + changedPaths: string[], + options: CwdOptions = {}, +) { + const cwd = options.cwd ?? process.cwd(); + if ( + changedPaths.some( + (changedPath) => + (!isTestFileTarget(changedPath) && SQLITE_SESSION_LIFECYCLE_PREFIX_RE.test(changedPath)) || + SQLITE_SESSION_LIFECYCLE_EXACT_RE.test(changedPath), + ) + ) { + return true; + } + const sourcePaths = changedPaths.filter( + (changedPath) => + SQLITE_SESSION_LIFECYCLE_IMPORT_CANDIDATE_RE.test(changedPath) && + !isTestFileTarget(changedPath), + ); + // Deleted sources cannot be graphed; fail safe to running the lifecycle proof. + if (sourcePaths.some((changedPath) => !existsSync(path.join(cwd, changedPath)))) { + return true; + } + if (sourcePaths.length === 0) { + return false; + } + return hasImportGraphImpactOnTargets(sourcePaths, [SQLITE_SESSION_LIFECYCLE_ENTRY], cwd); +} + function createBoundaryShard() { // Boundary tests scan the source tree (including test files) and build // their own fixtures; they do not consume the built dist artifact. When the diff --git a/test/scripts/ci-changed-node-test-plan.test.ts b/test/scripts/ci-changed-node-test-plan.test.ts index 46c7db7ccb1d..7acdc9084cf5 100644 --- a/test/scripts/ci-changed-node-test-plan.test.ts +++ b/test/scripts/ci-changed-node-test-plan.test.ts @@ -7,6 +7,7 @@ import { hasBuildArtifactAffectingChange, hasPromptSnapshotAffectingChange, hasQaSmokeAffectingChange, + hasSqliteSessionLifecycleAffectingChange, } from "../../scripts/lib/ci-changed-node-test-plan.mts"; import { hasImportGraphImpactOnTargets } from "../../scripts/test-projects.test-support.mts"; import { listGitTrackedFiles } from "../../src/test-utils/repo-files.js"; @@ -135,6 +136,46 @@ describe("CI changed Node test plan", () => { expect(hasPromptSnapshotAffectingChange(["src/infra/definitely-deleted-module.ts"])).toBe(true); }); + it("classifies SQLite session lifecycle impact by owner and import graph", () => { + expect( + hasSqliteSessionLifecycleAffectingChange([ + "src/agents/embedded-agent-runner/run/attempt-session-runtime-prepare.ts", + ]), + ).toBe(true); + expect( + hasSqliteSessionLifecycleAffectingChange(["src/gateway/server-methods/sessions.ts"]), + ).toBe(true); + expect( + hasSqliteSessionLifecycleAffectingChange(["src/sessions/session-lifecycle-admission.ts"]), + ).toBe(true); + expect(hasSqliteSessionLifecycleAffectingChange(["src/config/sessions.ts"])).toBe(true); + expect( + hasSqliteSessionLifecycleAffectingChange([ + "test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts", + ]), + ).toBe(true); + expect( + hasSqliteSessionLifecycleAffectingChange(["src/media-understanding/provider-id.ts"]), + ).toBe(false); + expect(hasSqliteSessionLifecycleAffectingChange(["src/agents/model-auth.ts"])).toBe(false); + expect(hasSqliteSessionLifecycleAffectingChange(["extensions/discord/src/index.ts"])).toBe( + false, + ); + expect( + hasSqliteSessionLifecycleAffectingChange([ + "src/config/sessions/session-registry-maintenance.test.ts", + ]), + ).toBe(false); + expect( + hasSqliteSessionLifecycleAffectingChange(["src/infra/definitely-deleted-module.ts"]), + ).toBe(false); + expect( + hasSqliteSessionLifecycleAffectingChange([ + "src/agents/embedded-agent-runner/run/deleted-session-runtime.ts", + ]), + ).toBe(true); + }); + it("fails safe to the full plan for broad changes", () => { expect(createChangedNodeTestShards(["package.json"])).toBeNull(); }); diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 22f88c6d4e2c..6a89bec10e22 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -248,19 +248,33 @@ function runCiManifestFixture(options: { ? `throw new Error("planner import failure");\n` : ` export const createChangedNodeTestShards = (changedPaths) => - changedPaths.includes("src/focused.ts") + changedPaths.includes("src/focused.ts") || + changedPaths.includes("test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts") ? [{ checkName: "changed-node-plan", configs: [], requiresDist: false, runner: "ubuntu-24.04", shardName: "changed-node-plan", - targets: ["src/focused.test.ts"], + targets: changedPaths.includes("src/focused.ts") + ? ["src/focused.test.ts"] + : ["test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts"], }] : null; + export const hasBuildArtifactAffectingChange = (changedPaths) => + !changedPaths.includes("test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts"); + export const hasSqliteSessionLifecycleAffectingChange = (changedPaths) => + changedPaths.includes("src/sqlite-session-owner.ts") || + changedPaths.includes("test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts"); `, "utf8", ); + const sqliteLifecycleProof = path.join( + root, + "test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts", + ); + mkdirSync(path.dirname(sqliteLifecycleProof), { recursive: true }); + writeFileSync(sqliteLifecycleProof, "export {};\n"); writeFileSync( path.join(scriptsDir, "channel-contract-test-plan.mts"), `export const createChannelContractTestShards = () => [{ checkName: "channel-contracts" }];\n`, @@ -2866,6 +2880,7 @@ NODE "control-ui-i18n", "native-i18n", "qa-smoke-ci-profile", + "sqlite-session-lifecycle", ]); const hostedRetryJobs = new Set(["checks-ui-e2e", "checks-ui-e2e-real-gateway"]); for (const { jobName, stepWith } of stickyConsumers) { @@ -5151,6 +5166,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" expect(current.outputs.run_native_i18n).toBe("true"); expect(current.outputs.run_openclawkit_tests).toBe("true"); expect(current.outputs.run_qa_smoke_ci).toBe("true"); + expect(current.outputs.run_sqlite_session_lifecycle).toBe("true"); expect(current.outputs.run_channel_contracts_shards).toBe("true"); expect(current.outputs.run_protocol_event_coverage).toBe("true"); expect(current.outputs.run_format_check).toBe("true"); @@ -5233,6 +5249,25 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" }), ]); expect(changedPullRequest.outputs.run_checks_node_core_dist).toBe("true"); + expect(changedPullRequest.outputs.run_sqlite_session_lifecycle).toBe("false"); + + const sqliteLifecyclePullRequest = runCiManifestFixture({ + bundledPlanner: true, + changedPaths: ["src/sqlite-session-owner.ts"], + eventName: "pull_request", + }); + expect(sqliteLifecyclePullRequest.status, sqliteLifecyclePullRequest.output).toBe(0); + expect(sqliteLifecyclePullRequest.outputs.run_sqlite_session_lifecycle).toBe("true"); + expect(sqliteLifecyclePullRequest.outputs.run_build_artifacts).toBe("true"); + + const sqliteLifecycleTestPullRequest = runCiManifestFixture({ + bundledPlanner: true, + changedPaths: ["test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts"], + eventName: "pull_request", + }); + expect(sqliteLifecycleTestPullRequest.status, sqliteLifecycleTestPullRequest.output).toBe(0); + expect(sqliteLifecycleTestPullRequest.outputs.run_sqlite_session_lifecycle).toBe("true"); + expect(sqliteLifecycleTestPullRequest.outputs.run_build_artifacts).toBe("true"); const plannerImportFailure = runCiManifestFixture({ bundledPlanner: true, @@ -5821,18 +5856,40 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" expect(proofStep.run).toContain("Selected target predates"); }); - it("scopes cold-runner watchdog headroom to the SQLite flip proof", () => { + it("runs the scoped SQLite lifecycle proof against the exact built artifact", () => { const workflow = readCiWorkflow(); const additionalJob = workflow.jobs["check-additional-shard"]; - const runStep = additionalJob.steps.find( + const additionalRunStep = additionalJob.steps.find( (step: WorkflowStep) => step.name === "Run additional check shard", ); - - expect(runStep.run).toContain("sqlite-session-flip-proof)"); - expect(runStep.run).toContain( - 'run_check "sqlite sessions/transcripts flip proof" env OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=660000 node scripts/run-vitest.mjs run', + const lifecycleJob = workflow.jobs["sqlite-session-lifecycle"]; + const downloadStep = lifecycleJob.steps.find( + (step: WorkflowStep) => step.name === "Download exact-run built runtime", ); - expect(runStep.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBeUndefined(); + const extractStep = lifecycleJob.steps.find( + (step: WorkflowStep) => step.name === "Extract built runtime", + ); + const proofStep = lifecycleJob.steps.find( + (step: WorkflowStep) => step.name === "Verify SQLite session lifecycle", + ); + + expect(additionalJob.strategy.matrix.include).not.toContainEqual( + expect.objectContaining({ group: "sqlite-session-flip-proof" }), + ); + expect(additionalRunStep.run).not.toContain("sqlite-session-flip-proof)"); + expect(lifecycleJob.needs).toEqual(["preflight", "build-artifacts"]); + expect(lifecycleJob.if).toContain( + "needs.preflight.outputs.run_sqlite_session_lifecycle == 'true'", + ); + expect(downloadStep.uses).toBe(DOWNLOAD_ARTIFACT_V8); + expect(downloadStep.with.name).toBe("dist-runtime-build"); + expect(extractStep.run).toContain("dist-runtime-build.tar.zst"); + expect(proofStep.env.OPENCLAW_E2E_USE_PREBUILT_DIST).toBe("1"); + expect(proofStep.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBe("660000"); + expect(proofStep.run).toContain( + "test/scripts/sqlite-sessions-transcripts-flip-proof.built-cli.e2e.test.ts", + ); + expect(workflow.jobs["ci-gate"].needs).toContain("sqlite-session-lifecycle"); }); it("restores the dist build cache before building and saves only cache misses", () => { @@ -6058,6 +6115,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" const selectedJobs = [ "pnpm-store-warmup", "build-artifacts", + "sqlite-session-lifecycle", "native-i18n", "checks-ui", "checks-ui-e2e", diff --git a/test/scripts/vitest-e2e-global-setup.test.ts b/test/scripts/vitest-e2e-global-setup.test.ts index f6e8e147bc7e..33df8cfca545 100644 --- a/test/scripts/vitest-e2e-global-setup.test.ts +++ b/test/scripts/vitest-e2e-global-setup.test.ts @@ -61,6 +61,14 @@ describe("vitest E2E global setup", () => { ); }); + it("uses an exact-run prebuilt dist without rebuilding it", async () => { + const runCommand = vi.fn(); + + await runE2eGlobalSetup(runCommand, { OPENCLAW_E2E_USE_PREBUILT_DIST: "1" }); + + expect(runCommand).not.toHaveBeenCalled(); + }); + posixIt("forwards output and SIGTERM through the runner process group", async () => { const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-setup-group-")); const fixturePath = path.join(fixtureDir, "build-fixture.mjs"); diff --git a/test/vitest/vitest.e2e.global-setup.ts b/test/vitest/vitest.e2e.global-setup.ts index 32fb03d6bd89..6ca8391c1c97 100644 --- a/test/vitest/vitest.e2e.global-setup.ts +++ b/test/vitest/vitest.e2e.global-setup.ts @@ -28,19 +28,25 @@ export function runE2eSetupCommand(args: string[], env: NodeJS.ProcessEnv): Prom export async function runE2eGlobalSetup( runCommand: SetupCommandRunner = runE2eSetupCommand, + env: NodeJS.ProcessEnv = process.env, ): Promise { + // Exact-run CI artifact consumers already have the complete built surface. + // Rebuilding here would discard that provenance and duplicate the slow step. + if (env.OPENCLAW_E2E_USE_PREBUILT_DIST === "1") { + return; + } const commands = [ { args: ["scripts/run-node.mjs", "--version"], env: { - ...process.env, + ...env, OPENCLAW_BUILD_PRIVATE_QA: "1", OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "0", }, }, { args: ["--import", "tsx", "scripts/tsdown-build.mts", "--config", "tsdown.ai.config.ts"], - env: process.env, + env, }, ]; for (const { args, env } of commands) {