From a193e06e42059e0d13c6ea5007e3278b414d7b57 Mon Sep 17 00:00:00 2001 From: James3014 <185240413+James3014@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:37:46 +0800 Subject: [PATCH 1/2] test(cutover): repair cutover-progression fixture and chain cutover tests into npm test (#517) Co-Authored-By: Claude Opus 5.5 --- package.json | 4 +-- scripts/check-test-chain.mjs | 1 - src/cutover-progression.test.ts | 47 ++++++++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4d2e27d67..2b085083a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "test:node-pty-permissions": "node --import tsx --test src/node-pty-postinstall.test.ts", "test:physical-host-registry": "tsx src/physical-host-registry.test.ts", "start": "node dist/cli.js serve", - "test": "node scripts/check-test-chain.mjs && tsx src/local-agent-path.test.ts && npm run test:node-pty-permissions && tsx src/development-boundaries.test.ts && npm run test:physical-host-registry && npm run test:carrier-binding && tsx src/coordination-reader-loader.test.ts && tsx src/control-plane-ownership.test.ts && tsx src/control-plane-handoff.test.ts && tsx src/control-plane-continuation.test.ts && tsx src/deployment-convergence.test.ts && tsx src/capability-generation-convergence.test.ts && tsx src/control-plane-convergence.test.ts && tsx src/current-completion-matrix.test.ts && tsx src/control-plane-consumer.test.ts && tsx src/local-agent-cline-catalog.test.ts && tsx src/chat-swarm-contract.test.ts && tsx src/chat-swarm-store.test.ts && tsx src/chat-swarm-runtime-owner.test.ts && tsx src/chat-swarm-runtime-tools.test.ts && tsx src/local-agent-opencode-mcp-catalog.test.ts && tsx src/chat-swarm-coordinator.test.ts && tsx src/chat-swarm-peer-runtime.test.ts && tsx src/chat-swarm-tools.test.ts && tsx src/chat-swarm-lifecycle.test.ts && tsx src/chat-swarm-carrier.test.ts && tsx src/chat-swarm-continuation-domain.test.ts && tsx src/chat-swarm-continuation-store.test.ts && tsx src/chat-swarm-peer-admission.test.ts && tsx src/chat-swarm-task-ledger.test.ts && tsx src/git-worktrees.test.ts && tsx src/host-storage-retention.test.ts && tsx src/execution-protocol.test.ts && tsx src/durable-operations-ci.test.ts && tsx src/git-candidate.test.ts && tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions-ci.test.ts && tsx src/codex-goal-sessions-ci.test.ts && tsx src/local-agent-process-tree.test.ts && tsx src/mcp-sessions.test.ts && tsx src/cutover-state.test.ts && tsx src/cutover-activation.test.ts && tsx src/cutover-state-recovery-guard.test.ts && tsx src/cutover-build-ready.test.ts && tsx src/cutover-orchestration.test.ts && tsx src/mcp-cutover.test.ts && tsx src/cutover-restart.test.ts && tsx src/cutover-http.test.ts && tsx src/cutover-recovery.test.ts && tsx src/cutover-binding-repair.test.ts && tsx src/capability-manifest.test.ts && tsx src/server-shutdown.test.ts && tsx src/codex-runtime.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-opencode-catalog.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/herdr-socket-path.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-profile-source.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-toolchains.test.ts && tsx src/local-agent-idle-policy.test.ts && tsx src/local-agent-capacity.test.ts && tsx src/local-agent-execution-contract.test.ts && tsx src/local-agent-continuation.test.ts && tsx src/provider-scratch.test.ts && tsx src/git-integration-ci.test.ts && tsx src/repository-intelligence.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/conversation-isolation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server-ci.test.ts && tsx src/oauth-store.test.ts && tsx src/cli-ci.test.ts && tsx src/oauth-json-and-child-reap.test.ts && tsx src/local-agent-errors.test.ts && tsx src/provider-output-redaction.test.ts && tsx src/host-activation.test.ts && tsx src/host-operation-policy.test.ts && tsx src/host-operations.test.ts && tsx src/host-operations-http.test.ts && tsx src/host-operation-external.test.ts && node --import tsx --test src/issue-413-wave1.test.ts && node --import tsx --test src/work-resume-integration.test.ts && tsx src/batch-autonomy-grant.test.ts && node --import tsx --test src/db/migrations.test.ts && node --import tsx --test src/host-capability-snapshot.test.ts && node --import tsx --test src/pi-tools.test.ts && node --import tsx --test src/work-resume.test.ts", + "test": "node scripts/check-test-chain.mjs && tsx src/local-agent-path.test.ts && npm run test:node-pty-permissions && tsx src/development-boundaries.test.ts && npm run test:physical-host-registry && npm run test:carrier-binding && tsx src/coordination-reader-loader.test.ts && tsx src/control-plane-ownership.test.ts && tsx src/control-plane-handoff.test.ts && tsx src/control-plane-continuation.test.ts && tsx src/deployment-convergence.test.ts && tsx src/capability-generation-convergence.test.ts && tsx src/control-plane-convergence.test.ts && tsx src/current-completion-matrix.test.ts && tsx src/control-plane-consumer.test.ts && tsx src/local-agent-cline-catalog.test.ts && tsx src/chat-swarm-contract.test.ts && tsx src/chat-swarm-store.test.ts && tsx src/chat-swarm-runtime-owner.test.ts && tsx src/chat-swarm-runtime-tools.test.ts && tsx src/local-agent-opencode-mcp-catalog.test.ts && tsx src/chat-swarm-coordinator.test.ts && tsx src/chat-swarm-peer-runtime.test.ts && tsx src/chat-swarm-tools.test.ts && tsx src/chat-swarm-lifecycle.test.ts && tsx src/chat-swarm-carrier.test.ts && tsx src/chat-swarm-continuation-domain.test.ts && tsx src/chat-swarm-continuation-store.test.ts && tsx src/chat-swarm-peer-admission.test.ts && tsx src/chat-swarm-task-ledger.test.ts && tsx src/git-worktrees.test.ts && tsx src/host-storage-retention.test.ts && tsx src/execution-protocol.test.ts && tsx src/durable-operations-ci.test.ts && tsx src/git-candidate.test.ts && tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions-ci.test.ts && tsx src/codex-goal-sessions-ci.test.ts && tsx src/local-agent-process-tree.test.ts && tsx src/mcp-sessions.test.ts && tsx src/cutover-state.test.ts && tsx src/cutover-activation.test.ts && tsx src/cutover-state-recovery-guard.test.ts && tsx src/cutover-build-ready.test.ts && tsx src/cutover-orchestration.test.ts && tsx src/mcp-cutover.test.ts && tsx src/cutover-restart.test.ts && tsx src/cutover-http.test.ts && tsx src/cutover-recovery.test.ts && tsx src/cutover-binding-repair.test.ts && node --import tsx --test src/cutover-progression.test.ts && tsx src/capability-manifest.test.ts && tsx src/server-shutdown.test.ts && tsx src/codex-runtime.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-opencode-catalog.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/herdr-socket-path.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-profile-source.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-toolchains.test.ts && tsx src/local-agent-idle-policy.test.ts && tsx src/local-agent-capacity.test.ts && tsx src/local-agent-execution-contract.test.ts && tsx src/local-agent-continuation.test.ts && tsx src/provider-scratch.test.ts && tsx src/git-integration-ci.test.ts && tsx src/repository-intelligence.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/conversation-isolation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server-ci.test.ts && tsx src/oauth-store.test.ts && tsx src/cli-ci.test.ts && tsx src/oauth-json-and-child-reap.test.ts && tsx src/local-agent-errors.test.ts && tsx src/provider-output-redaction.test.ts && tsx src/host-activation.test.ts && tsx src/host-operation-policy.test.ts && tsx src/host-operations.test.ts && tsx src/host-operations-http.test.ts && tsx src/host-operation-external.test.ts && node --import tsx --test src/issue-413-wave1.test.ts && node --import tsx --test src/work-resume-integration.test.ts && tsx src/batch-autonomy-grant.test.ts && node --import tsx --test src/db/migrations.test.ts && node --import tsx --test src/host-capability-snapshot.test.ts && node --import tsx --test src/pi-tools.test.ts && node --import tsx --test src/work-resume.test.ts && node --test scripts/ops/m5-cutover/m5-cutover.test.mjs", "verify:nexus-core": "node scripts/nexus-core-verify.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", "test:carrier-binding": "tsx src/carrier-binding.test.ts", @@ -87,5 +87,5 @@ "optionalDependencies": { "node-pty": "1.1.0" }, - "//devspace-ds-1": "TODO(DS-1): test files excluded from npm test (src/cli.test.ts, src/server.test.ts via cli-ci/server-ci; src/cutover-progression.test.ts, src/issue344-minimal-direct-coding.test.ts, src/local-agent-herdr.test.ts, src/local-agent-p2-telemetry.test.ts) are listed with reasons in scripts/check-test-chain.mjs. Failure summaries: /home/claude/issues/devspace/DS-1-failures.md. Tracking GitHub issue IDs: TBD (not created from this session)." + "//devspace-ds-1": "TODO(DS-1): test files excluded from npm test (src/cli.test.ts, src/server.test.ts via cli-ci/server-ci; src/issue344-minimal-direct-coding.test.ts, src/local-agent-herdr.test.ts, src/local-agent-p2-telemetry.test.ts) are listed with reasons in scripts/check-test-chain.mjs. Failure summaries: /home/claude/issues/devspace/DS-1-failures.md. Tracking GitHub issue IDs: TBD (not created from this session)." } diff --git a/scripts/check-test-chain.mjs b/scripts/check-test-chain.mjs index c2ee2be9c..7cebf0b3e 100644 --- a/scripts/check-test-chain.mjs +++ b/scripts/check-test-chain.mjs @@ -12,7 +12,6 @@ const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")); // Details: /home/claude/issues/devspace/DS-1-failures.md const EXCLUDED = new Map([ ["src/cli.test.ts", "TODO(DS-1-F1): fails in npm test; see DS-1-failures.md"], - ["src/cutover-progression.test.ts", "TODO(DS-1-F2): fails in npm test; see DS-1-failures.md"], ["src/issue344-minimal-direct-coding.test.ts", "TODO(DS-1-F3): needs DEVSPACE_OAUTH_OWNER_TOKEN; see DS-1-failures.md"], ["src/local-agent-herdr.test.ts", "TODO(DS-1-F4): live canary with hard-coded /Users/james paths; see DS-1-failures.md"], ["src/local-agent-p2-telemetry.test.ts", "TODO(DS-1-F5): requires agy CLI; see DS-1-failures.md"], diff --git a/src/cutover-progression.test.ts b/src/cutover-progression.test.ts index d6a33fda0..5218db588 100644 --- a/src/cutover-progression.test.ts +++ b/src/cutover-progression.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { + CUTOVER_ACTIVATION_BINDING_SCHEMA, CutoverStateStore, type CutoverServerIdentity, type ExpectedCutoverIdentity, @@ -74,6 +75,28 @@ function createMockActuator(fail = false): SelfRestartActuator & { scheduledCoun return actuator; } +// Orchestrated restart is fenced on an exact activation binding (#377). The +// fixture records one after the restart decision so the original gates under +// test (build-ready, actuator) are reached instead of the earlier fence. +const BOUND_RELEASE_SHA256 = "e".repeat(64); +const boundReleasePath = (stateDir: string): string => join(stateDir, "releases", "release-target"); + +function bindActivation(stateDir: string, controller: McpCutoverController): void { + const record = controller.record(); + assert.ok(record); + const cutoverId = record.cutoverId; + new CutoverStateStore(stateDir).recordActivationBinding(cutoverId, { + schema: CUTOVER_ACTIVATION_BINDING_SCHEMA, + cutoverId, + sourceCommit: record.expectedNewIdentity.sourceCommit, + buildId: record.expectedNewIdentity.buildId, + releaseSha256: BOUND_RELEASE_SHA256, + releasePath: boundReleasePath(stateDir), + pointerPath: join(stateDir, "current-release.json"), + boundAt: new Date().toISOString(), + }); +} + function createOrchestratorFixture(stateDir: string, options?: { actuatorFail?: boolean; probe?: (expected: ExpectedCutoverIdentity) => BuildReadyProbeResult; @@ -101,6 +124,17 @@ function createOrchestratorFixture(stateDir: string, options?: { oldOrch: makeOrch(oldController), replacementOrch: makeOrch(replacementController), mismatchedOrch: makeOrch(mismatchedController), + // Successor that reports the activation it was started under (health + // build.activation_cutover_id), as the real replacement process does. + activatedReplacementFor: (cutoverId: string) => { + const controller = new McpCutoverController(store, { + ...replacementIdentity, + releaseSha256: BOUND_RELEASE_SHA256, + releasePath: boundReleasePath(stateDir), + activationCutoverId: cutoverId, + }); + return { controller, orch: makeOrch(controller) }; + }, }; } @@ -155,6 +189,7 @@ test("devspace#263 G4: end-to-end self-cutover progression canary (advance-drive verifiedBy: "build-pipeline", verifiedAt: new Date().toISOString(), }); + bindActivation(stateDir, f.oldController); // Step 6: cutover_advance marks restart and invokes actuator -> ADVANCED / AWAITING_RECONNECT const step6 = await f.oldOrch.advance(); @@ -170,17 +205,18 @@ test("devspace#263 G4: end-to-end self-cutover progression canary (advance-drive assert.equal(f.actuator.scheduledCount, 1, "must never schedule duplicate restart"); // Step 8: Client reconnects to replacement instance (reconcile-only mode) - assert.equal(f.replacementController.mode(), "reconcile-only"); - const step8 = await f.replacementOrch.advance({ + const successor = f.activatedReplacementFor(started.cutoverId); + assert.equal(successor.controller.mode(), "reconcile-only"); + const step8 = await successor.orch.advance({ callerContinuity: { caller: "reconnected-agent" }, }); assert.equal(step8.status, "TERMINAL"); assert.equal(step8.cutover_phase, "closed"); assert.equal(step8.reconciliation_required, false); - assert.equal(f.replacementController.mode(), "normal"); + assert.equal(successor.controller.mode(), "normal"); // Step 9: Post-close advance returns TERMINAL - const final = await f.replacementOrch.advance(); + const final = await successor.orch.advance(); assert.equal(final.status, "TERMINAL"); } finally { rmSync(stateDir, { recursive: true, force: true }); @@ -278,6 +314,7 @@ test("devspace#263 G3: build-ready missing or probe failure blocks with BUILD_NO verifiedBy: "attester", verifiedAt: new Date().toISOString(), }); + bindActivation(stateDir, f.oldController); const probeFailed = await f.oldOrch.advance(); assert.equal(probeFailed.status, "BLOCKED"); assert.equal(probeFailed.blocker, "BUILD_NOT_READY"); @@ -298,6 +335,7 @@ test("devspace#263 G3: replacement identity mismatch fails closed with RECONCILI verifiedBy: "attester", verifiedAt: new Date().toISOString(), }); + bindActivation(stateDir, f.oldController); await f.oldOrch.advance(); // Replacement boots with unexpected source/build @@ -320,6 +358,7 @@ test("devspace#263 G3: actuator failure is durable and never retried blindly", a verifiedBy: "attester", verifiedAt: new Date().toISOString(), }); + bindActivation(stateDir, f.oldController); const failed = await f.oldOrch.advance(); assert.equal(failed.status, "BLOCKED"); From cbd5bce86ebc1a9d002ac2336e48bab6b6c3bcc9 Mon Sep 17 00:00:00 2001 From: James3014 <185240413+James3014@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:04:15 +0800 Subject: [PATCH 2/2] test(ops): pass HOME to the no-env sidecar test and surface raw stderr on failure (#517) The Core verify container runs npm test as a uid where os.homedir() can throw without HOME, so the sidecar crashed before printing its JSON usage error. Co-Authored-By: Claude Opus 5.5 --- scripts/ops/m5-cutover/m5-cutover.test.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/ops/m5-cutover/m5-cutover.test.mjs b/scripts/ops/m5-cutover/m5-cutover.test.mjs index c88dc4812..78f2ffdf4 100644 --- a/scripts/ops/m5-cutover/m5-cutover.test.mjs +++ b/scripts/ops/m5-cutover/m5-cutover.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; @@ -25,10 +26,12 @@ function record(overrides = {}) { const select = (file, liveIsOld, liveIsTarget) => selectMode({ file, contract, liveIsOld, liveIsTarget }); test("sidecar without env exits 1 with a usage error and no side effects", () => { - const env = { PATH: process.env.PATH }; + // HOME is set explicitly: CI containers may run as a uid with no passwd entry, where os.homedir() throws. + const env = { PATH: process.env.PATH, HOME: tmpdir() }; const result = spawnSync(process.execPath, [join(here, "m5-cutover-sidecar.mjs")], { env, encoding: "utf8" }); assert.equal(result.status, 1); - const failure = JSON.parse(result.stderr); + let failure; + try { failure = JSON.parse(result.stderr); } catch { assert.fail(`sidecar stderr is not the JSON usage failure:\n${result.stderr}`); } assert.equal(failure.ok, false); assert.match(failure.error, /PENDING_ID, CONTRACT_PATH, RECEIPT_PATH and ACTIVE_RELEASE_ROOT are all required/); });