From 63dd032b7f78d483b1ff134adb27e611f64a3b06 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 17:30:03 -0400 Subject: [PATCH] test(integration): cover exact adoption recovery TASK-f40126ee-2485-45c8-a67b-4279e4e8704a --- docs/TEST_INTEGRITY.md | 41 +++++ src/integration/adoption.mjs | 3 +- test/fleet/integration-adoption.test.mjs | 199 +++++++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 test/fleet/integration-adoption.test.mjs diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 2e4dd8a..c332e14 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -3143,3 +3143,44 @@ test_integrity_note: did_relax_any_assertion: false if_true_explain_spec_basis: "" ``` + +# Exact-source adoption (2026-10-03) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-integration-adoption-private-ref-cas + given: "A clean disposable Git repository, a base commit, and an owner-bound private-ref adoption" + when: "The adoption finalizes and a stale compare-and-swap attempts to alter its candidate ref" + then: "Real Git private archive and candidate refs are created atomically, while the stale write refuses" + - id: SCN-integration-adoption-journal-and-binding + given: "A prepared adoption journal record" + when: "A second preparation or lifecycle request supplies a different candidate binding" + then: "The journal revision compare-and-swap and immutable binding checks refuse without changing the prepared record" + - id: SCN-integration-adoption-finalize-recovery + given: "A real private-ref transaction completed before its journal write" + when: "Reconciliation is run and then replayed" + then: "One durable finalized recovery is recorded and the replay is consistent without another mutation" + - id: SCN-integration-adoption-rollback-recovery + given: "A finalized candidate whose private ref was removed before its rollback journal write, or whose candidate ref diverged" + when: "Rollback is requested" + then: "The completed rollback is recovered once and divergent candidate bytes refuse" + - id: SCN-integration-adoption-private-ref-intake + given: "A finalized owner-bound private candidate ref" + when: "Native qualification is requested" + then: "The normal integration queue records the exact private ref and commit in ordinary testing state" + observable_outcomes: + - "Git ref values, structured refusal codes, durable journal state, and native integration-request fields" + determinism_controls: + - "Disposable local Git repositories, temporary SQLite-backed control-plane state, fixed fixture bytes, and no network or provider" + anti_cheat_rationale: + prevents: + - "Fixture-only CAS implementations that do not exercise Git update-ref" + - "Mutable lifecycle bindings or overwriteable adoption journal records" + - "Claiming crash recovery without preserving actual ref effects" + - "Treating a private candidate as an ordinary specialist branch" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/src/integration/adoption.mjs b/src/integration/adoption.mjs index cac0c5e..7c0ed67 100644 --- a/src/integration/adoption.mjs +++ b/src/integration/adoption.mjs @@ -82,7 +82,8 @@ function exactAuthority(authority, binding) { function exactInput(record, input) { const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest', 'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef']; - const mismatches = fields.filter((field) => input[field] !== undefined && input[field] !== record[field]); + const mismatches = fields.filter((field) => input[field] !== undefined + && input[field] !== (field === 'adoptionId' ? record.id : record[field])); if (input.provenance !== undefined && JSON.stringify(input.provenance) !== JSON.stringify(record.provenance)) mismatches.push('provenance'); if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', { code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches }, diff --git a/test/fleet/integration-adoption.test.mjs b/test/fleet/integration-adoption.test.mjs new file mode 100644 index 0000000..7363ef7 --- /dev/null +++ b/test/fleet/integration-adoption.test.mjs @@ -0,0 +1,199 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import test from 'node:test'; +import { openControlPlane } from '../../src/control-plane/service.mjs'; +import { analyzeRepository } from '../../src/kernel/analyze.mjs'; +import { proposeDomains } from '../../src/kernel/domains.mjs'; +import { inspectRepository } from '../../src/kernel/git.mjs'; +import { installProject } from '../../src/kernel/install.mjs'; +import { IntegrationService } from '../../src/integration/service.mjs'; + +const sha = value => execFileSync('git', ['-C', value, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); +const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex'); + +function fixture(name) { + const root = mkdtempSync(join(tmpdir(), `${name}-`)); + execFileSync('git', ['init', '-b', 'main', root]); + execFileSync('git', ['-C', root, 'config', 'user.email', 'torch-test@example.invalid']); + execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'package.json'), `${JSON.stringify({ name, scripts: {} })}\n`); + writeFileSync(join(root, 'app.mjs'), 'export const value = 1;\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture base']); + const repository = inspectRepository(root); + const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) }); + proposal.domains.push({ + id: 'review-area', title: 'Review Area', kind: 'development', + scope: ['review fixtures'], not_scope: ['application source'], owned_paths: ['review/**'], + shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'claude', evidence: ['app.mjs'], + }); + proposal.checks = []; + proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00.000Z', reviewedBy: 'fixture-owner', notes: [] }; + const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) }; + installProject({ repository, proposal, env, projectId: name }); + execFileSync('git', ['-C', root, 'add', '.torch']); + execFileSync('git', ['-C', root, 'commit', '-m', 'install fixture control plane']); + const base = sha(root); + execFileSync('git', ['-C', root, 'checkout', '-b', 'candidate']); + writeFileSync(join(root, 'candidate.mjs'), 'export const candidate = true;\n'); + execFileSync('git', ['-C', root, 'add', 'candidate.mjs']); + execFileSync('git', ['-C', root, 'commit', '-m', 'candidate contribution']); + const candidate = sha(root); + execFileSync('git', ['-C', root, 'checkout', 'main']); + const control = openControlPlane({ repositoryRoot: root, env }); + const service = new IntegrationService({ repositoryRoot: root, controlPlane: control, checkService: { exactPasses: () => false } }); + const contributor = proposal.domains[0].id; + return { root, control, service, contributor, destination: 'review-area', base, candidate }; +} + +function authority(context, { action, adoptionId }) { + const provenance = ['fixture:owner-approved', `candidate:${context.candidate}`]; + const binding = { + schema: 'torch.dev/integration-adoption-authority/v1alpha1', action, adoptionId, + candidateSha: context.candidate, baseTargetSha: context.base, + provenanceManifestDigest: digest(provenance), contributorArea: context.contributor, + destinationArea: context.destination, targetBranch: 'main', + candidateRef: `refs/torch/integration-candidates/${adoptionId}`, + expectedOldRef: 'refs/heads/main', adapter: { + name: context.service.adoption.refAdapter.name, id: context.service.adoption.refAdapter.id, + }, + }; + const pending = context.control.requestApproval({ + requester: context.contributor, approver: 'owner', task: 'TASK-f40126ee', + title: `Fixture ${action} authority`, summary: 'Hermetic exact-source adoption scenario.', + }); + const approved = context.control.decideApproval({ + approvalId: pending.id, decidedBy: 'owner', decision: 'approved', expectedRevision: pending.revision, + note: JSON.stringify(binding), + }); + return { + approvalId: approved.id, approvalRevision: approved.revision, provenance, + provenanceManifestDigest: binding.provenanceManifestDigest, adapter: binding.adapter, + }; +} + +function input(context, action, adoptionId, extra = {}) { + return { + actorId: 'owner', adoptionId, candidateSha: context.candidate, baseTargetSha: context.base, + contributorArea: context.contributor, destinationArea: context.destination, + targetBranch: 'main', candidateRef: `refs/torch/integration-candidates/${adoptionId}`, + expectedOldRef: 'refs/heads/main', ...authority(context, { action, adoptionId }), ...extra, + }; +} + +function prepare(context, adoptionId = 'fixture-adoption') { + const preparedInput = input(context, 'prepare', adoptionId); + const plan = context.service.planAdoption({ ...preparedInput, action: 'prepare' }); + assert.deepEqual(plan.blockers, [], `preparation must begin from an exact clean fixture: ${JSON.stringify(plan.blockers)}`); + return context.service.prepareAdoption(preparedInput); +} + +function lifecycleInput(context, action, adoptionId, extra = {}) { + const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId }); + return { actorId: 'owner', adoptionId, approvalId, approvalRevision, adapter, ...extra }; +} + +test('SCN-integration-adoption-private-ref-cas: a real temporary Git transaction atomically creates private archive and candidate refs, and stale expected refs refuse', () => { + const context = fixture('torch-adoption-cas'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const refs = context.service.adoption.refAdapter.readRefs([ + 'refs/heads/main', finalized.candidateRef, finalized.archiveRef, + ]); + assert.deepEqual(refs, { + 'refs/heads/main': context.base, + [finalized.candidateRef]: context.candidate, + [finalized.archiveRef]: context.base, + }); + assert.throws(() => context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: finalized.candidateRef, sha: context.base }], + writes: [{ ref: finalized.candidateRef, sha: null }], + }), (error) => error.code === 'ADOPTION_REF_CAS_FAILED'); + assert.equal(context.service.adoption.getPrepared(adoptionId).state, 'finalized'); + context.control.close(); +}); + +test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cannot overwrite the journal and lifecycle input cannot alter its immutable candidate binding', () => { + const context = fixture('torch-adoption-journal'); + const adoptionId = 'fixture-adoption'; + const prepared = prepare(context, adoptionId); + assert.throws(() => context.service.prepareAdoption(input(context, 'prepare', adoptionId)), + (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE'); + assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })), + (error) => error.code === 'ADOPTION_IMMUTABLE_BINDING_STALE'); + const { mutationPerformed, ...durablePrepared } = prepared; + assert.equal(mutationPerformed, true); + assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared); + context.control.close(); +}); + +test('SCN-integration-adoption-finalize-recovery: a crash after real ref CAS but before the journal transition reconciles once and never duplicates refs', () => { + const context = fixture('torch-adoption-finalize-recovery'); + const adoptionId = 'fixture-adoption'; + const prepared = prepare(context, adoptionId); + context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [ + { ref: prepared.expectedOldRef, sha: prepared.baseTargetSha }, + { ref: prepared.candidateRef, sha: null }, + { ref: prepared.archiveRef, sha: null }, + ], + writes: [ + { ref: prepared.archiveRef, sha: prepared.baseTargetSha }, + { ref: prepared.candidateRef, sha: prepared.candidateSha }, + ], + }); + const recovered = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId)); + assert.equal(recovered.state, 'recovered-finalized'); + assert.equal(recovered.record.recovery, 'private-ref-cas-completed-before-journal'); + const replay = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId)); + assert.equal(replay.state, 'consistent'); + assert.equal(replay.mutationPerformed, false); + context.control.close(); +}); + +test('SCN-integration-adoption-rollback-recovery: a journal-missing rollback completes without a second deletion, while divergent candidate bytes refuse', () => { + const context = fixture('torch-adoption-rollback-recovery'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + assert.equal(finalized.state, 'finalized'); + context.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: finalized.candidateRef, sha: finalized.candidateSha }], + writes: [{ ref: finalized.candidateRef, sha: null }], + }); + const recovered = context.service.rollbackAdoption(lifecycleInput(context, 'rollback', adoptionId)); + assert.equal(recovered.state, 'rolled-back'); + assert.equal(recovered.recovery, 'private-ref-rollback-completed-before-journal'); + + const divergent = fixture('torch-adoption-rollback-divergence'); + const divergentId = 'fixture-adoption'; + prepare(divergent, divergentId); + const divergentFinal = divergent.service.finalizeAdoption(lifecycleInput(divergent, 'finalize', divergentId)); + divergent.service.adoption.refAdapter.compareAndSwapRefs({ + expected: [{ ref: divergentFinal.candidateRef, sha: divergentFinal.candidateSha }], + writes: [{ ref: divergentFinal.candidateRef, sha: divergent.base }], + }); + assert.throws(() => divergent.service.rollbackAdoption(lifecycleInput(divergent, 'rollback', divergentId)), + (error) => error.code === 'ADOPTION_ROLLBACK_REF_DIVERGED'); + context.control.close(); + divergent.control.close(); +}); + +test('SCN-integration-adoption-private-ref-intake: finalized private refs enter the ordinary native integration queue, not a specialist branch shortcut', () => { + const context = fixture('torch-adoption-native-intake'); + const adoptionId = 'fixture-adoption'; + prepare(context, adoptionId); + const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId)); + const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId)); + assert.equal(requested.state, 'requested'); + assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef); + assert.equal(requested.integrationRequest.sourceCommit, context.candidate); + assert.equal(requested.integrationRequest.state, 'testing'); + assert.equal(requested.integrationRequest.targetBranch, 'main'); + context.control.close(); +});