mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
TASK-f40126ee: compose d179 adoption candidate
Port the owner-authorized four-path adoption delta from the original Work lineage (2c59634,8f894e9,97973f0,8ea3bef,63dd032,530afb3,beb99bc,004d9e7) plus caller-revision correction7533d864. Preserve unrelated B2/check/convergence history outside this isolated composition.
This commit is contained in:
1 parent
d179d5bc2f
commit
4a5f3d0c94
4 files changed
+1085
-1
No files matched your search
@@ -2883,6 +2883,45 @@ test_integrity_note:
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
# Candidate source B2 frozen-input and fence boundaries (2026-10-01)
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-candidate-bootstrap-one-shot
|
||||
given: "An issued, nonce-bound native admission"
|
||||
when: "It is opened, replayed, expired, foreign, or malformed"
|
||||
then: "Only one bounded private bootstrap opens; every invalid case refuses before child start and exposes no authority handle"
|
||||
- id: SCN-candidate-input-consumption
|
||||
given: "A frozen project-local source snapshot"
|
||||
when: "The original source changes before the fixed Node child reads its cwd"
|
||||
then: "The child observes the captured bytes; snapshot drift refuses"
|
||||
- id: SCN-candidate-execution-boundary
|
||||
given: "A fixed command and explicit bounded environment"
|
||||
when: "Ambient PATH/HOME/cache fallback or output overflow is attempted"
|
||||
then: "Execution refuses or remains non-success"
|
||||
- id: SCN-candidate-fence-closure
|
||||
given: "A private PID1 namespace observation"
|
||||
when: "Descendant closure is absent or uncertain"
|
||||
then: "The outcome is UNKNOWN/INCOMPLETE, never terminal PASS"
|
||||
observable_outcomes:
|
||||
- "No DB, ControlPlane, writer, receipt adapter, or authority FD reaches the child"
|
||||
- "Product input links and special nodes refuse; frozen product S stays under 64MiB/file and 512MiB total"
|
||||
- "Artifact output remains M/artifactManifest; receipt-adapter A is absent and unclaimed"
|
||||
determinism_controls:
|
||||
- "Temporary source trees, fixed Node executable, explicit empty ambient environment, bounded streams, and injected observations"
|
||||
- "No live policy registration, installation, network, cache, or provider process"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "Reading mutated live source instead of captured bytes"
|
||||
- "Replaying a private admission or relabeling fixture output as a receipt"
|
||||
- "Treating PGID emptiness, zero exit, or Chromium metadata as descendant/binary proof"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
# Console attention advisory cohorts (2026-10-01)
|
||||
|
||||
```yaml
|
||||
@@ -2932,6 +2971,47 @@ test_integrity_note:
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
# Candidate source B2 authenticated-attempt spec correction (2026-10-01)
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: spec_correction
|
||||
scenarios:
|
||||
- id: SCN-candidate-bootstrap-one-shot
|
||||
given: "A CheckService-issued opaque registered-plan handle"
|
||||
when: "A foreign, cloned, expired, replayed, or already-closed handle is used"
|
||||
then: "No FD3 admission opens; the only accepted acknowledgement is the exact FD4 binding frame and it closes the original linear handle"
|
||||
- id: SCN-candidate-input-consumption
|
||||
given: "A frozen, bounded product snapshot and a fixed Node E binding"
|
||||
when: "The original product bytes change before execution"
|
||||
then: "An actual spawned child reads the frozen bytes; post-capture drift refuses before spawn"
|
||||
- id: SCN-candidate-execution-boundary
|
||||
given: "An opaque attempt with fixed command, arguments, explicit environment, and output cap"
|
||||
when: "The child exits zero after exceeding the cap"
|
||||
then: "The parent records overflow as non-success without accepting an injected executor or caller-selected limit"
|
||||
- id: SCN-candidate-fence-closure
|
||||
given: "A closed opaque attempt"
|
||||
when: "The backend has not recorded namespace identity, FD closure, and descendant reaping"
|
||||
then: "Terminal proof is UNKNOWN; fabricated observations and cloned handles cannot create proof"
|
||||
observable_outcomes:
|
||||
- "Only the exact FD4 acknowledgement closes a one-shot opaque admission"
|
||||
- "Child stdout proves frozen-byte consumption, not same-process test reads"
|
||||
- "Zero exit plus output overflow is never success"
|
||||
- "Missing backend fence evidence is never TerminalProof"
|
||||
determinism_controls:
|
||||
- "Temporary source trees, fixed absolute Node runtime, explicit empty child environment, bounded streams"
|
||||
- "No injected executor, caller-provided command, root, environment, limit, or terminal observation"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "Arbitrary tuple issuance or a fabricated bootstrap literal"
|
||||
- "Boolean caller observations minting terminal proof"
|
||||
- "Cloned or reused handles reopening an admission"
|
||||
- "Same-process snapshot reads impersonating child consumption"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
# Compact Console Overview density (2026-10-01)
|
||||
|
||||
```yaml
|
||||
@@ -3063,3 +3143,103 @@ 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: "An existing unfinalized private candidate ref and a separate finalized owner-bound private candidate ref"
|
||||
when: "Direct private-ref intake and authenticated native qualification are requested"
|
||||
then: "Direct intake refuses without a queue record, while the authenticated path records the exact private ref and commit in ordinary testing state"
|
||||
- id: SCN-integration-adoption-approval-boundaries
|
||||
given: "An ordinary source-only approval, or an adoption whose actor, action, approval revision, or provenance differs from the approved immutable binding"
|
||||
when: "Preparation or a lifecycle transition is attempted"
|
||||
then: "The transition refuses with its structured boundary code and no integration queue record"
|
||||
- id: SCN-integration-adoption-request-replay
|
||||
given: "A finalized adoption that has created its one ordinary private-ref integration request"
|
||||
when: "Two independently opened authenticated API callers request it concurrently"
|
||||
then: "The existing transactional adoption journal serializes them, one caller refuses after the completed transition, and exactly one queue row remains"
|
||||
- id: SCN-integration-adoption-legacy-retries
|
||||
given: "An existing project database containing two historical ordinary-request retry rows with the same source branch and commit"
|
||||
when: "IntegrationService starts"
|
||||
then: "Both records remain readable; startup adds no global uniqueness constraint and does not reject historical evidence"
|
||||
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"
|
||||
- "Using an unrelated, source-only, stale, or mismatched approval as adoption authority"
|
||||
- "Duplicating a private-ref queue record through concurrent authenticated API requests or replay"
|
||||
- "Rejecting historical ordinary-request retry evidence with an unqualified schema constraint"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-integration-adoption-lifecycle-revision-cas
|
||||
given: "A prepared or finalized owner-bound adoption record with a known journal revision"
|
||||
when: "A caller supplies a stale or omitted expectedRecordRevision to finalize, reconcile, rollback, or request native qualification"
|
||||
then: "The operation refuses before private-ref, journal, queue, or audit effects; the original record and refs remain unchanged"
|
||||
observable_outcomes:
|
||||
- "ADOPTION_RECORD_REVISION_STALE is returned for stale or omitted lifecycle callers"
|
||||
- "Private candidate/archive refs, durable journal record, audit count, and integration queue remain unchanged for every rejected lifecycle call"
|
||||
determinism_controls:
|
||||
- "Disposable local Git repository and temporary SQLite-backed control-plane fixture; no network, provider, or live adoption"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "Issuing external ref or queue effects from a stale lifecycle read"
|
||||
- "Treating journal revision as an internal-only implementation detail"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: spec_correction
|
||||
scenarios:
|
||||
- id: SCN-integration-adoption-request-replay
|
||||
given: "Two authenticated callers carry the same finalized adoption revision into the serialized native-request transition"
|
||||
when: "One caller completes the transition and the other resumes with its now-stale expectedRecordRevision"
|
||||
then: "The second caller strictly receives ADOPTION_RECORD_REVISION_STALE while exactly one requested journal record and one queue row remain"
|
||||
observable_outcomes:
|
||||
- "The replay loser reports the stale journal-revision boundary, not a generic post-state refusal"
|
||||
- "Exactly one private-ref integration request and requested audit transition exist"
|
||||
determinism_controls:
|
||||
- "Two independently opened local API processes over disposable Git and SQLite fixture state"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "Accepting a stale lifecycle read after another caller has committed"
|
||||
- "Relaxing the exactly-one-request invariant after adding revision binding"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
@@ -0,0 +1,384 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
import { observeRuntimeTurn } from '../runtime/turn-guard.mjs';
|
||||
|
||||
const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1';
|
||||
const AUTHORITY_SCHEMA = 'torch.dev/integration-adoption-authority/v1alpha1';
|
||||
const ADAPTER_NAME = 'git-private-ref-cas-v1';
|
||||
const ACTIONS = new Set(['plan', 'prepare', 'finalize', 'reconcile', 'rollback', 'request']);
|
||||
const MUTATING_ACTIONS = new Set(['prepare', 'finalize', 'rollback', 'request']);
|
||||
const SHA = /^[0-9a-f]{40,64}$/;
|
||||
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/;
|
||||
const ZERO_SHA = '0'.repeat(40);
|
||||
|
||||
function git(root, args, { allowFailure = false } = {}) {
|
||||
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
||||
if (result.status !== 0 && !allowFailure) {
|
||||
throw new TorchError(`Git command failed: git ${args.join(' ')}`, {
|
||||
code: 'GIT_COMMAND_FAILED', details: result.stderr?.trim() || null,
|
||||
});
|
||||
}
|
||||
return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' };
|
||||
}
|
||||
|
||||
function tableExists(database, name) {
|
||||
return Boolean(database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name));
|
||||
}
|
||||
|
||||
function text(value, name) {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new TorchError(`${name} is required`, { code: 'ADOPTION_INPUT_INVALID', details: { name } });
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function sha(value, name) {
|
||||
const normalized = text(value, name);
|
||||
if (!SHA.test(normalized)) throw new TorchError(`${name} must be a full Git object id`, {
|
||||
code: 'ADOPTION_INPUT_INVALID', details: { name },
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function digest(value) {
|
||||
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
||||
}
|
||||
|
||||
function durableRecord(event) {
|
||||
if (!event?.details) return null;
|
||||
try {
|
||||
const record = JSON.parse(event.details);
|
||||
return record?.schema === ADOPTION_SCHEMA ? record : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function listWorktreePaths(root) {
|
||||
const records = [];
|
||||
for (const line of git(root, ['worktree', 'list', '--porcelain']).stdout.split('\n')) {
|
||||
if (line.startsWith('worktree ')) records.push(line.slice('worktree '.length));
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
function authorityFrom(approval) {
|
||||
if (!approval || approval.status !== 'approved' || approval.approver_id !== 'owner' || approval.decided_by !== 'owner') return null;
|
||||
try {
|
||||
const authority = JSON.parse(approval.decision_note ?? '');
|
||||
return authority?.schema === AUTHORITY_SCHEMA ? authority : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function exactAuthority(authority, binding) {
|
||||
if (!authority || authority.action !== binding.action) return false;
|
||||
const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest',
|
||||
'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef'];
|
||||
return fields.every((field) => authority[field] === binding[field])
|
||||
&& authority.adapter?.name === binding.adapter?.name
|
||||
&& authority.adapter?.id === binding.adapter?.id;
|
||||
}
|
||||
|
||||
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] !== (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 },
|
||||
});
|
||||
if (!Number.isInteger(input.expectedRecordRevision) || input.expectedRecordRevision !== record.revision) {
|
||||
throw new TorchError('Lifecycle input must bind the current adoption record revision', {
|
||||
code: 'ADOPTION_RECORD_REVISION_STALE',
|
||||
details: { adoptionId: record.id, expectedRevision: input.expectedRecordRevision ?? null, actual: record.revision },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Real Git private-ref transaction adapter. It is source code only: callers
|
||||
* decide when it is invoked, while the adapter itself gives temp-repository
|
||||
* scenarios the same atomic update-ref semantics as a real repository.
|
||||
*/
|
||||
export class GitPrivateRefAdapter {
|
||||
constructor({ repositoryRoot } = {}) {
|
||||
this.repositoryRoot = resolve(text(repositoryRoot, 'repositoryRoot'));
|
||||
this.name = ADAPTER_NAME;
|
||||
this.id = digest({ adapter: this.name, repositoryRoot: this.repositoryRoot });
|
||||
}
|
||||
|
||||
readRefs(refs) {
|
||||
const values = {};
|
||||
for (const ref of refs) {
|
||||
const result = git(this.repositoryRoot, ['rev-parse', '--verify', '--quiet', ref], { allowFailure: true });
|
||||
values[ref] = result.status === 0 ? result.stdout : null;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
compareAndSwapRefs({ expected = [], writes = [] } = {}) {
|
||||
const commands = ['start'];
|
||||
const writtenRefs = new Set(writes.map((entry) => entry.ref));
|
||||
for (const { ref, sha: expectedSha } of expected) {
|
||||
if (writtenRefs.has(ref)) continue;
|
||||
commands.push(`verify ${text(ref, 'ref')} ${expectedSha ?? ZERO_SHA}`);
|
||||
}
|
||||
for (const { ref, sha: nextSha } of writes) {
|
||||
const expectedEntry = expected.find((entry) => entry.ref === ref);
|
||||
if (!expectedEntry) throw new TorchError('Every private-ref write requires an exact expected value', {
|
||||
code: 'ADOPTION_REF_CAS_EXPECTED_REQUIRED', details: { ref },
|
||||
});
|
||||
const current = expectedEntry.sha ?? null;
|
||||
if (nextSha === null) commands.push(`delete ${text(ref, 'ref')} ${current ?? ZERO_SHA}`);
|
||||
else commands.push(`update ${text(ref, 'ref')} ${sha(nextSha, 'next ref sha')} ${current ?? ZERO_SHA}`);
|
||||
}
|
||||
commands.push('prepare', 'commit', '');
|
||||
const result = spawnSync('git', ['-C', this.repositoryRoot, 'update-ref', '--stdin'], {
|
||||
input: commands.join('\n'), encoding: 'utf8',
|
||||
});
|
||||
if (result.status !== 0) throw new TorchError('Private ref compare-and-swap refused', {
|
||||
code: 'ADOPTION_REF_CAS_FAILED', details: result.stderr?.trim() || null,
|
||||
});
|
||||
return { adapter: { name: this.name, id: this.id }, expected, writes };
|
||||
}
|
||||
}
|
||||
|
||||
export class AdoptionService {
|
||||
constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, refAdapter } = {}) {
|
||||
this.repositoryRoot = repositoryRoot;
|
||||
this.controlPlane = controlPlane;
|
||||
this.clock = clock;
|
||||
this.idFactory = idFactory;
|
||||
this.policy = policy ?? {};
|
||||
this.refAdapter = refAdapter ?? new GitPrivateRefAdapter({ repositoryRoot });
|
||||
}
|
||||
|
||||
plan({ action = 'plan', actorId, approvalId, approvalRevision, adoptionId,
|
||||
candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea,
|
||||
destinationArea, targetBranch, candidateRef, expectedOldRef, adapter } = {}) {
|
||||
const blockers = [];
|
||||
const id = adoptionId ? text(adoptionId, 'adoptionId') : null;
|
||||
if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' });
|
||||
if (!ACTIONS.has(action)) blockers.push({ code: 'ADOPTION_ACTION_INVALID' });
|
||||
if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' });
|
||||
if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' });
|
||||
const adapterBinding = adapter?.name && adapter?.id ? { name: adapter.name, id: adapter.id } : null;
|
||||
if (MUTATING_ACTIONS.has(action) && !adapterBinding) blockers.push({ code: 'ADOPTION_ADAPTER_BINDING_REQUIRED' });
|
||||
if (adapterBinding && (adapterBinding.name !== this.refAdapter.name || adapterBinding.id !== this.refAdapter.id)) {
|
||||
blockers.push({ code: 'ADOPTION_ADAPTER_PROVENANCE_INVALID' });
|
||||
}
|
||||
|
||||
let candidate; let base; let provenanceDigest;
|
||||
try {
|
||||
candidate = sha(candidateSha, 'candidateSha');
|
||||
base = sha(baseTargetSha, 'baseTargetSha');
|
||||
if (!Array.isArray(provenance) || !provenance.length || provenance.some((entry) => typeof entry !== 'string' || !entry)) {
|
||||
throw new TorchError('provenance must be a non-empty ordered string list', { code: 'ADOPTION_INPUT_INVALID' });
|
||||
}
|
||||
provenanceDigest = digest(provenance);
|
||||
if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest });
|
||||
} catch (error) {
|
||||
blockers.push({ code: error.code ?? 'ADOPTION_INPUT_INVALID', details: error.details ?? null });
|
||||
}
|
||||
|
||||
const target = targetBranch ?? this.policy.target;
|
||||
if (typeof target !== 'string' || !target.trim()) blockers.push({ code: 'ADOPTION_TARGET_REQUIRED' });
|
||||
const candidateReference = candidateRef ?? (id ? `refs/torch/integration-candidates/${id}` : null);
|
||||
const expectedReference = expectedOldRef ?? (target ? `refs/heads/${target}` : null);
|
||||
if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' });
|
||||
if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' });
|
||||
|
||||
let contributor; let destination;
|
||||
try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); }
|
||||
catch (error) { blockers.push({ code: 'ADOPTION_CONTRIBUTOR_INVALID', details: error.code ?? null }); }
|
||||
try { destination = this.controlPlane.assertIdentity(text(destinationArea, 'destinationArea')); }
|
||||
catch (error) { blockers.push({ code: 'ADOPTION_DESTINATION_INVALID', details: error.code ?? null }); }
|
||||
if (contributor && destination && contributor === destination) blockers.push({ code: 'ADOPTION_IDENTITIES_MUST_DIFFER' });
|
||||
|
||||
const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, provenanceManifestDigest: provenanceDigest,
|
||||
contributorArea: contributor, destinationArea: destination, targetBranch: target, candidateRef: candidateReference,
|
||||
expectedOldRef: expectedReference, adapter: adapterBinding };
|
||||
const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests WHERE id = ? AND project_id = ?`)
|
||||
.get(approvalId, this.controlPlane.projectId) : null;
|
||||
if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' });
|
||||
else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision });
|
||||
else if (!exactAuthority(authorityFrom(approval), binding)) blockers.push({ code: 'ADOPTION_APPROVAL_BINDING_INVALID' });
|
||||
|
||||
if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' });
|
||||
if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_BASE_MISSING' });
|
||||
if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' });
|
||||
if (expectedReference && base) {
|
||||
const current = this.refAdapter.readRefs([expectedReference])[expectedReference];
|
||||
if (!current) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' });
|
||||
else if (current !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current });
|
||||
}
|
||||
const candidateExists = candidateReference && this.refAdapter.readRefs([candidateReference])[candidateReference] != null;
|
||||
if (['plan', 'prepare', 'finalize'].includes(action) && candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' });
|
||||
if (action === 'request' && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' });
|
||||
|
||||
for (const worktree of listWorktreePaths(this.repositoryRoot)) {
|
||||
const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean);
|
||||
if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries });
|
||||
}
|
||||
if (tableExists(this.controlPlane.database, 'worktree_guards')) {
|
||||
for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard });
|
||||
}
|
||||
if (tableExists(this.controlPlane.database, 'resource_leases')) {
|
||||
for (const lease of this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases WHERE released_at IS NULL`).all()) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease });
|
||||
}
|
||||
if (tableExists(this.controlPlane.database, 'prepared_checks')) {
|
||||
for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check });
|
||||
}
|
||||
for (const area of [contributor, destination].filter(Boolean)) {
|
||||
const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot,
|
||||
projectId: this.controlPlane.projectId, areaId: area, now: this.clock });
|
||||
if (executor.state !== 'inactive') blockers.push({ code: 'ADOPTION_EXECUTOR_NOT_INACTIVE', areaId: area, executor });
|
||||
}
|
||||
return { action: `plan-adoption-${action}`, adoptionId: id, approval: approval ? { id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id } : null,
|
||||
authority: binding, candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null,
|
||||
contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, candidateRef: candidateReference,
|
||||
expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false };
|
||||
}
|
||||
|
||||
#append(adoptionId, expectedRevision, record, operation) {
|
||||
this.controlPlane.database.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
this.#appendInTransaction(adoptionId, expectedRevision, record, operation);
|
||||
this.controlPlane.database.exec('COMMIT');
|
||||
} catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; }
|
||||
}
|
||||
|
||||
#appendInTransaction(adoptionId, expectedRevision, record, operation) {
|
||||
const current = this.#find(adoptionId);
|
||||
if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) {
|
||||
throw new TorchError('Adoption journal changed before this transition could be recorded', {
|
||||
code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null },
|
||||
});
|
||||
}
|
||||
this.controlPlane.database.prepare(`INSERT INTO audit_events
|
||||
(id, project_id, actor_id, operation, entity_type, entity_id, details, created_at)
|
||||
VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`)
|
||||
.run(this.idFactory(), this.controlPlane.projectId, operation, adoptionId, JSON.stringify(record), this.clock().toISOString());
|
||||
}
|
||||
|
||||
#fromRecord(record, input, action) {
|
||||
exactInput(record, input);
|
||||
return this.plan({ action, actorId: input.actorId, approvalId: input.approvalId, approvalRevision: input.approvalRevision,
|
||||
adoptionId: record.id, candidateSha: record.candidateSha, baseTargetSha: record.baseTargetSha, provenance: record.provenance,
|
||||
provenanceManifestDigest: record.provenanceManifestDigest, contributorArea: record.contributorArea, destinationArea: record.destinationArea,
|
||||
targetBranch: record.targetBranch, candidateRef: record.candidateRef, expectedOldRef: record.expectedOldRef, adapter: input.adapter });
|
||||
}
|
||||
|
||||
prepare(input = {}) {
|
||||
const adoptionId = input.adoptionId ?? this.idFactory();
|
||||
const plan = this.plan({ ...input, action: 'prepare', adoptionId });
|
||||
if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers });
|
||||
const now = this.clock().toISOString();
|
||||
const record = { schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'),
|
||||
approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha,
|
||||
baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef,
|
||||
expectedOldRef: plan.expectedOldRef, contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch,
|
||||
archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, rolledBackAt: null };
|
||||
this.#append(adoptionId, null, record, 'integration.adoption.prepare');
|
||||
return { ...record, mutationPerformed: true };
|
||||
}
|
||||
|
||||
finalize(input = {}) {
|
||||
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
|
||||
if (record.state !== 'prepared') throw new TorchError('Only a prepared adoption can be finalized', { code: 'ADOPTION_FINALIZE_STATE_INVALID', details: { state: record.state } });
|
||||
const plan = this.#fromRecord(record, input, 'finalize');
|
||||
if (!plan.canProceed) throw new TorchError('Adoption finalize is blocked', { code: 'ADOPTION_FINALIZE_BLOCKED', details: plan.blockers });
|
||||
const receipt = this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }],
|
||||
writes: [{ ref: record.archiveRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: record.candidateSha }] });
|
||||
const finalized = { ...record, revision: record.revision + 1, state: 'finalized', finalizeApprovalId: text(input.approvalId, 'approvalId'),
|
||||
finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), refReceiptDigest: digest(receipt) };
|
||||
this.#append(record.id, record.revision, finalized, 'integration.adoption.finalize');
|
||||
return { ...finalized, mutationPerformed: true };
|
||||
}
|
||||
|
||||
reconcile(input = {}) {
|
||||
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
|
||||
const plan = this.#fromRecord(record, input, 'reconcile');
|
||||
if (!plan.canProceed) return { record, plan, state: 'blocked', mutationPerformed: false };
|
||||
const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
|
||||
const finalizedRefs = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha;
|
||||
if (record.state === 'prepared' && finalizedRefs) {
|
||||
const recovered = { ...record, revision: record.revision + 1, state: 'finalized', finalizedAt: this.clock().toISOString(),
|
||||
recoveredAt: this.clock().toISOString(), recovery: 'private-ref-cas-completed-before-journal' };
|
||||
this.#append(record.id, record.revision, recovered, 'integration.adoption.reconcile-finalized');
|
||||
return { record: recovered, refs, state: 'recovered-finalized', mutationPerformed: true };
|
||||
}
|
||||
if (record.state === 'finalized' && finalizedRefs) return { record, refs, state: 'consistent', mutationPerformed: false };
|
||||
if (record.state === 'prepared' && refs[record.candidateRef] === null && refs[record.archiveRef] === null) return { record, refs, state: 'prepared', mutationPerformed: false };
|
||||
return { record, refs, state: 'diverged', mutationPerformed: false };
|
||||
}
|
||||
|
||||
rollback(input = {}) {
|
||||
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
|
||||
if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can be rolled back', { code: 'ADOPTION_ROLLBACK_STATE_INVALID', details: { state: record.state } });
|
||||
const plan = this.#fromRecord(record, input, 'rollback');
|
||||
if (!plan.canProceed) throw new TorchError('Adoption rollback is blocked', { code: 'ADOPTION_ROLLBACK_BLOCKED', details: plan.blockers });
|
||||
const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
|
||||
const recovered = refs[record.expectedOldRef] === record.baseTargetSha
|
||||
&& refs[record.candidateRef] === null && refs[record.archiveRef] === record.baseTargetSha;
|
||||
if (!recovered && refs[record.candidateRef] !== record.candidateSha) {
|
||||
throw new TorchError('Candidate ref changed before rollback', { code: 'ADOPTION_ROLLBACK_REF_DIVERGED', details: refs });
|
||||
}
|
||||
const receipt = recovered ? { adapter: { name: this.refAdapter.name, id: this.refAdapter.id }, recovered: true, refs }
|
||||
: this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] });
|
||||
const rolledBack = { ...record, revision: record.revision + 1, state: 'rolled-back', rollbackApprovalId: text(input.approvalId, 'approvalId'),
|
||||
rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: digest(receipt),
|
||||
...(recovered ? { recoveredAt: this.clock().toISOString(), recovery: 'private-ref-rollback-completed-before-journal' } : {}) };
|
||||
this.#append(record.id, record.revision, rolledBack, 'integration.adoption.rollback');
|
||||
return { ...rolledBack, mutationPerformed: true };
|
||||
}
|
||||
|
||||
requestAdoptedCandidate(input = {}, { requestIntegration, findIntegrationRequest } = {}) {
|
||||
if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' });
|
||||
const adoptionId = text(input.adoptionId, 'adoptionId');
|
||||
this.controlPlane.database.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const record = this.getPrepared(adoptionId);
|
||||
exactInput(record, input);
|
||||
if (!['finalized', 'requesting'].includes(record.state)) {
|
||||
throw new TorchError('Only a finalized adoption can request native qualification', {
|
||||
code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state },
|
||||
});
|
||||
}
|
||||
const plan = this.#fromRecord(record, input, 'request');
|
||||
if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers });
|
||||
const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting',
|
||||
requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() };
|
||||
if (record.state !== 'requesting') this.#appendInTransaction(record.id, record.revision, requesting, 'integration.adoption.request-started');
|
||||
const existing = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha });
|
||||
if (existing && (existing.sourceArea !== requesting.contributorArea || existing.targetBranch !== requesting.targetBranch)) {
|
||||
throw new TorchError('Existing private-ref integration request does not match this adoption', {
|
||||
code: 'ADOPTION_INTEGRATION_REQUEST_CONFLICT', details: { adoptionId: requesting.id, integrationRequestId: existing.id },
|
||||
});
|
||||
}
|
||||
const request = existing ?? requestIntegration({ adoptionId: requesting.id, areaId: requesting.contributorArea,
|
||||
commit: requesting.candidateSha, candidateRef: requesting.candidateRef });
|
||||
const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() };
|
||||
this.#appendInTransaction(requesting.id, requesting.revision, requested, 'integration.adoption.request');
|
||||
this.controlPlane.database.exec('COMMIT');
|
||||
return { ...requested, integrationRequest: request, mutationPerformed: true };
|
||||
} catch (error) {
|
||||
this.controlPlane.database.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
#find(adoptionId) {
|
||||
const row = this.controlPlane.database.prepare(`SELECT details FROM audit_events WHERE project_id = ?
|
||||
AND entity_type = 'integration-adoption' AND entity_id = ? ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, adoptionId);
|
||||
return durableRecord(row);
|
||||
}
|
||||
|
||||
getPrepared(adoptionId) {
|
||||
const id = text(adoptionId, 'adoptionId');
|
||||
const record = this.#find(id);
|
||||
if (!record) throw new TorchError('Unknown adoption journal record', { code: 'ADOPTION_NOT_FOUND', details: { adoptionId: id } });
|
||||
return record;
|
||||
}
|
||||
}
|
||||
+101
-1
@@ -4,6 +4,7 @@ import { TorchError } from '../kernel/errors.mjs';
|
||||
import { readInstallManifest } from '../kernel/install.mjs';
|
||||
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
|
||||
import { BacklogService } from '../backlog/service.mjs';
|
||||
import { AdoptionService } from './adoption.mjs';
|
||||
|
||||
function git(root, args, { allowFailure = false } = {}) {
|
||||
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
||||
@@ -15,6 +16,21 @@ function git(root, args, { allowFailure = false } = {}) {
|
||||
return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' };
|
||||
}
|
||||
|
||||
function text(value, name) {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new TorchError(`${name} is required`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } });
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function sha(value, name) {
|
||||
const normalized = text(value, name);
|
||||
if (!/^[0-9a-f]{40,64}$/.test(normalized)) {
|
||||
throw new TorchError(`${name} must be a full Git object id`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } });
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function integrationRow(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
@@ -42,6 +58,8 @@ export class IntegrationService {
|
||||
this.worktrees = new Map((manifest.external ?? [])
|
||||
.filter((entry) => entry.type === 'worktree')
|
||||
.map((entry) => [entry.area, entry]));
|
||||
this.adoption = new AdoptionService({ repositoryRoot, controlPlane, clock, idFactory,
|
||||
policy: { target: this.policy.target } });
|
||||
this.controlPlane.database.exec(`
|
||||
CREATE TABLE IF NOT EXISTS integration_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -84,12 +102,56 @@ export class IntegrationService {
|
||||
code: 'INTEGRATION_SOURCE_MOVED', details: { branchTip, sourceCommit },
|
||||
});
|
||||
}
|
||||
return this.#registerRequest({ area, sourceBranch: worktree.branch, sourceCommit });
|
||||
}
|
||||
|
||||
requestCandidate({ areaId, commit, candidateRef } = {}) {
|
||||
throw new TorchError('Private candidate intake requires an authenticated finalized adoption', {
|
||||
code: 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY', details: { areaId, commit, candidateRef },
|
||||
});
|
||||
}
|
||||
|
||||
#requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef } = {}) {
|
||||
const area = this.controlPlane.assertIdentity(areaId);
|
||||
if (area === 'session-manager') {
|
||||
throw new TorchError('Session Manager does not own a feature worktree by default', {
|
||||
code: 'INTEGRATION_SOURCE_INVALID',
|
||||
});
|
||||
}
|
||||
const adoption = this.adoption.getPrepared(text(adoptionId, 'adoptionId'));
|
||||
if (adoption.state !== 'requesting' || adoption.contributorArea !== area
|
||||
|| adoption.candidateSha !== commit || adoption.candidateRef !== candidateRef) {
|
||||
throw new TorchError('Private candidate does not match an authenticated finalized adoption', {
|
||||
code: 'INTEGRATION_ADOPTED_BINDING_INVALID', details: { adoptionId, state: adoption.state },
|
||||
});
|
||||
}
|
||||
const ref = text(candidateRef, 'candidateRef');
|
||||
if (!/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(ref)) {
|
||||
throw new TorchError('Adopted candidates must use a private integration candidate ref', {
|
||||
code: 'INTEGRATION_ADOPTED_REF_INVALID', details: { candidateRef: ref },
|
||||
});
|
||||
}
|
||||
const sourceCommit = sha(commit, 'commit');
|
||||
const refTip = git(this.repositoryRoot, ['rev-parse', '--verify', ref], { allowFailure: true });
|
||||
if (refTip.status !== 0 || refTip.stdout !== sourceCommit) {
|
||||
throw new TorchError('Adopted candidate ref no longer names the exact requested commit', {
|
||||
code: 'INTEGRATION_ADOPTED_REF_STALE', details: { candidateRef: ref, sourceCommit, actual: refTip.stdout || null },
|
||||
});
|
||||
}
|
||||
const existing = this.controlPlane.database.prepare(`SELECT id FROM integration_requests
|
||||
WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`)
|
||||
.get(this.controlPlane.projectId, ref, sourceCommit);
|
||||
if (existing) return this.get(existing.id);
|
||||
return this.#registerRequest({ area, sourceBranch: ref, sourceCommit });
|
||||
}
|
||||
|
||||
#registerRequest({ area, sourceBranch, sourceCommit }) {
|
||||
const targetBranch = this.policy.target;
|
||||
const baseTargetCommit = git(this.repositoryRoot, ['rev-parse', targetBranch]).stdout;
|
||||
const now = this.clock().toISOString();
|
||||
const record = {
|
||||
id: this.idFactory(), projectId: this.controlPlane.projectId, sourceArea: area,
|
||||
sourceBranch: worktree.branch, sourceCommit, targetBranch, baseTargetCommit,
|
||||
sourceBranch, sourceCommit, targetBranch, baseTargetCommit,
|
||||
state: 'testing', reason: null, requiredChecks: this.policy.required_checks ?? [],
|
||||
authorizedBy: null, authorizedAt: null, createdAt: now, updatedAt: now, landedAt: null,
|
||||
};
|
||||
@@ -110,6 +172,44 @@ export class IntegrationService {
|
||||
return this.evaluate(record.id);
|
||||
}
|
||||
|
||||
planAdoption(input = {}) {
|
||||
return this.adoption.plan(input);
|
||||
}
|
||||
|
||||
prepareAdoption(input = {}) {
|
||||
return this.adoption.prepare(input);
|
||||
}
|
||||
|
||||
getPreparedAdoption(adoptionId) {
|
||||
return this.adoption.getPrepared(adoptionId);
|
||||
}
|
||||
|
||||
finalizeAdoption(input = {}) {
|
||||
return this.adoption.finalize(input);
|
||||
}
|
||||
|
||||
reconcileAdoption(input = {}) {
|
||||
return this.adoption.reconcile(input);
|
||||
}
|
||||
|
||||
rollbackAdoption(input = {}) {
|
||||
return this.adoption.rollback(input);
|
||||
}
|
||||
|
||||
requestAdoptedCandidate(input = {}) {
|
||||
return this.adoption.requestAdoptedCandidate(input, {
|
||||
requestIntegration: ({ adoptionId, areaId, commit, candidateRef }) => this.#requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef }),
|
||||
findIntegrationRequest: ({ candidateRef, commit }) => this.findCandidateRequest({ candidateRef, commit }),
|
||||
});
|
||||
}
|
||||
|
||||
findCandidateRequest({ candidateRef, commit } = {}) {
|
||||
const row = this.controlPlane.database.prepare(`SELECT id FROM integration_requests
|
||||
WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`)
|
||||
.get(this.controlPlane.projectId, candidateRef, commit);
|
||||
return row ? this.get(row.id) : null;
|
||||
}
|
||||
|
||||
get(requestId) {
|
||||
const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId);
|
||||
if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' });
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { execFileSync, spawn } 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', '[email protected]']);
|
||||
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, env, control, service, contributor, destination: 'review-area', base, candidate };
|
||||
}
|
||||
|
||||
function concurrentAdoptedRequest(context, requestInput) {
|
||||
const childSource = `
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
const { repositoryRoot, stateRoot, requestInput } = JSON.parse(process.argv[1]);
|
||||
const sourceRoot = process.cwd();
|
||||
const { openControlPlane } = await import(pathToFileURL(resolve(sourceRoot, 'src/control-plane/service.mjs')).href);
|
||||
const { IntegrationService } = await import(pathToFileURL(resolve(sourceRoot, 'src/integration/service.mjs')).href);
|
||||
const control = openControlPlane({ repositoryRoot, env: { ...process.env, XDG_DATA_HOME: stateRoot } });
|
||||
const service = new IntegrationService({ repositoryRoot, controlPlane: control, checkService: { exactPasses: () => false } });
|
||||
try {
|
||||
const result = service.requestAdoptedCandidate(requestInput);
|
||||
console.log(JSON.stringify({ outcome: 'requested', integrationRequestId: result.integrationRequest.id }));
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify({ outcome: 'refused', code: error.code ?? null }));
|
||||
} finally {
|
||||
control.close();
|
||||
}
|
||||
`;
|
||||
const encoded = JSON.stringify({ repositoryRoot: context.root, stateRoot: context.env.XDG_DATA_HOME, requestInput });
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ['--input-type=module', '-e', childSource, encoded], {
|
||||
cwd: process.cwd(), env: process.env, stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let stdout = ''; let stderr = '';
|
||||
child.stdout.on('data', chunk => { stdout += chunk; });
|
||||
child.stderr.on('data', chunk => { stderr += chunk; });
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) return reject(new Error(`concurrent adopted request child failed: ${stderr}`));
|
||||
try { resolve(JSON.parse(stdout.trim().split('\n').at(-1))); }
|
||||
catch (error) { reject(new Error(`invalid concurrent adopted request output: ${stdout}\n${stderr}\n${error.message}`)); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
const record = context.service.getPreparedAdoption(adoptionId);
|
||||
return { actorId: 'owner', adoptionId, expectedRecordRevision: record.revision, approvalId, approvalRevision, adapter, ...extra };
|
||||
}
|
||||
|
||||
function blockedCodes(error) {
|
||||
assert.equal(error.code.endsWith('_BLOCKED'), true, `expected a blocked adoption transition, got ${error.code}`);
|
||||
return error.details.map((blocker) => blocker.code);
|
||||
}
|
||||
|
||||
function lifecycleBoundary(context, adoptionId) {
|
||||
const record = context.service.getPreparedAdoption(adoptionId);
|
||||
return {
|
||||
record,
|
||||
refs: context.service.adoption.refAdapter.readRefs([
|
||||
record.expectedOldRef, record.candidateRef, record.archiveRef,
|
||||
]),
|
||||
queue: context.service.list(),
|
||||
auditCount: context.control.database.prepare(`SELECT COUNT(*) AS count FROM audit_events
|
||||
WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ?`)
|
||||
.get(context.control.projectId, adoptionId).count,
|
||||
};
|
||||
}
|
||||
|
||||
function assertLifecycleBoundaryUnchanged(context, adoptionId, before) {
|
||||
assert.deepEqual(lifecycleBoundary(context, adoptionId), before);
|
||||
}
|
||||
|
||||
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');
|
||||
assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, {
|
||||
expectedRecordRevision: prepared.revision + 1,
|
||||
})), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE');
|
||||
const refs = context.service.adoption.refAdapter.readRefs([
|
||||
prepared.expectedOldRef, prepared.candidateRef, prepared.archiveRef,
|
||||
]);
|
||||
assert.deepEqual(refs, {
|
||||
[prepared.expectedOldRef]: context.base,
|
||||
[prepared.candidateRef]: null,
|
||||
[prepared.archiveRef]: null,
|
||||
});
|
||||
const { mutationPerformed, ...durablePrepared } = prepared;
|
||||
assert.equal(mutationPerformed, true);
|
||||
assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared);
|
||||
context.control.close();
|
||||
});
|
||||
|
||||
test('SCN-integration-adoption-lifecycle-revision-cas: stale or omitted revisions for every lifecycle action refuse before refs, journal, queue, or audit effects', () => {
|
||||
const cases = [
|
||||
{
|
||||
action: 'finalize',
|
||||
setup: (context, adoptionId) => prepare(context, adoptionId),
|
||||
invoke: (context, input) => context.service.finalizeAdoption(input),
|
||||
},
|
||||
{
|
||||
action: 'reconcile',
|
||||
setup: (context, adoptionId) => prepare(context, adoptionId),
|
||||
invoke: (context, input) => context.service.reconcileAdoption(input),
|
||||
},
|
||||
{
|
||||
action: 'rollback',
|
||||
setup: (context, adoptionId) => {
|
||||
prepare(context, adoptionId);
|
||||
return context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
|
||||
},
|
||||
invoke: (context, input) => context.service.rollbackAdoption(input),
|
||||
},
|
||||
{
|
||||
action: 'request',
|
||||
setup: (context, adoptionId) => {
|
||||
prepare(context, adoptionId);
|
||||
return context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
|
||||
},
|
||||
invoke: (context, input) => context.service.requestAdoptedCandidate(input),
|
||||
},
|
||||
];
|
||||
|
||||
for (const { action, setup, invoke } of cases) {
|
||||
const context = fixture(`torch-adoption-${action}-revision-boundary`);
|
||||
const adoptionId = 'fixture-adoption';
|
||||
setup(context, adoptionId);
|
||||
for (const variant of ['omitted', 'stale']) {
|
||||
const current = lifecycleInput(context, action, adoptionId);
|
||||
const input = variant === 'omitted'
|
||||
? (({ expectedRecordRevision: _expectedRecordRevision, ...withoutRevision }) => withoutRevision)(current)
|
||||
: { ...current, expectedRecordRevision: current.expectedRecordRevision + 1 };
|
||||
const before = lifecycleBoundary(context, adoptionId);
|
||||
assert.throws(() => invoke(context, input), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE',
|
||||
`${action} must reject a ${variant} lifecycle revision`);
|
||||
assertLifecycleBoundaryUnchanged(context, adoptionId, before);
|
||||
}
|
||||
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 unfinalized = fixture('torch-adoption-unfinalized-intake');
|
||||
const unfinalizedId = 'fixture-adoption';
|
||||
const unfinalizedRef = `refs/torch/integration-candidates/${unfinalizedId}`;
|
||||
unfinalized.service.adoption.refAdapter.compareAndSwapRefs({
|
||||
expected: [{ ref: unfinalizedRef, sha: null }],
|
||||
writes: [{ ref: unfinalizedRef, sha: unfinalized.candidate }],
|
||||
});
|
||||
assert.throws(() => unfinalized.service.requestCandidate({
|
||||
areaId: unfinalized.contributor, commit: unfinalized.candidate, candidateRef: unfinalizedRef,
|
||||
}), (error) => error.code === 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY');
|
||||
assert.deepEqual(unfinalized.service.list(), []);
|
||||
unfinalized.control.close();
|
||||
|
||||
const context = fixture('torch-adoption-native-intake');
|
||||
const adoptionId = 'fixture-adoption';
|
||||
prepare(context, adoptionId);
|
||||
const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
|
||||
assert.throws(() => context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId, {
|
||||
expectedRecordRevision: finalized.revision + 1,
|
||||
})), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE');
|
||||
assert.deepEqual(context.service.list(), []);
|
||||
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();
|
||||
});
|
||||
|
||||
test('SCN-integration-adoption-approval-boundaries: unrelated or source-only approval records cannot authorize an owner adoption, and actor, action, revision, and provenance mismatches each refuse', () => {
|
||||
const unrelated = fixture('torch-adoption-unrelated-approval');
|
||||
const unrelatedId = 'fixture-adoption';
|
||||
const unrelatedApproval = unrelated.control.requestApproval({
|
||||
requester: unrelated.contributor, approver: 'owner', task: 'TASK-unrelated',
|
||||
title: 'Ordinary source branch review', summary: 'Not an integration-adoption authority.',
|
||||
});
|
||||
const unrelatedApproved = unrelated.control.decideApproval({
|
||||
approvalId: unrelatedApproval.id, decidedBy: 'owner', decision: 'approved', expectedRevision: unrelatedApproval.revision,
|
||||
note: JSON.stringify({ schema: 'torch.dev/ordinary-source-review/v1alpha1', sourceArea: unrelated.contributor }),
|
||||
});
|
||||
const unrelatedInput = {
|
||||
actorId: 'owner', adoptionId: unrelatedId, candidateSha: unrelated.candidate, baseTargetSha: unrelated.base,
|
||||
contributorArea: unrelated.contributor, destinationArea: unrelated.destination, targetBranch: 'main',
|
||||
candidateRef: `refs/torch/integration-candidates/${unrelatedId}`, expectedOldRef: 'refs/heads/main',
|
||||
approvalId: unrelatedApproved.id, approvalRevision: unrelatedApproved.revision,
|
||||
provenance: ['fixture:owner-approved', `candidate:${unrelated.candidate}`],
|
||||
provenanceManifestDigest: digest(['fixture:owner-approved', `candidate:${unrelated.candidate}`]),
|
||||
adapter: { name: unrelated.service.adoption.refAdapter.name, id: unrelated.service.adoption.refAdapter.id },
|
||||
};
|
||||
assert.throws(() => unrelated.service.prepareAdoption(unrelatedInput), (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID'));
|
||||
assert.deepEqual(unrelated.service.list(), []);
|
||||
unrelated.control.close();
|
||||
|
||||
const wrongActor = fixture('torch-adoption-wrong-actor');
|
||||
assert.throws(() => wrongActor.service.prepareAdoption({
|
||||
...input(wrongActor, 'prepare', 'fixture-adoption'), actorId: wrongActor.contributor,
|
||||
}), (error) => blockedCodes(error).includes('OWNER_ACTOR_REQUIRED'));
|
||||
wrongActor.control.close();
|
||||
|
||||
const wrongAction = fixture('torch-adoption-wrong-action');
|
||||
prepare(wrongAction, 'fixture-adoption');
|
||||
assert.throws(() => wrongAction.service.finalizeAdoption(lifecycleInput(wrongAction, 'rollback', 'fixture-adoption')),
|
||||
(error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID'));
|
||||
wrongAction.control.close();
|
||||
|
||||
const staleRevision = fixture('torch-adoption-stale-revision');
|
||||
prepare(staleRevision, 'fixture-adoption');
|
||||
const staleInput = lifecycleInput(staleRevision, 'finalize', 'fixture-adoption');
|
||||
assert.throws(() => staleRevision.service.finalizeAdoption({ ...staleInput, approvalRevision: staleInput.approvalRevision - 1 }),
|
||||
(error) => blockedCodes(error).includes('ADOPTION_APPROVAL_REVISION_STALE'));
|
||||
staleRevision.control.close();
|
||||
|
||||
const wrongProvenance = fixture('torch-adoption-wrong-provenance');
|
||||
assert.throws(() => wrongProvenance.service.prepareAdoption({
|
||||
...input(wrongProvenance, 'prepare', 'fixture-adoption'), provenance: ['fixture:owner-approved', 'unapproved:alternate-provenance'],
|
||||
}), (error) => blockedCodes(error).includes('ADOPTION_PROVENANCE_DIGEST_MISMATCH'));
|
||||
wrongProvenance.control.close();
|
||||
});
|
||||
|
||||
test('SCN-integration-adoption-request-replay: concurrent authenticated API callers serialize on the adoption journal and create exactly one private-ref queue request', async () => {
|
||||
const context = fixture('torch-adoption-request-replay');
|
||||
const adoptionId = 'fixture-adoption';
|
||||
prepare(context, adoptionId);
|
||||
context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
|
||||
const requestInput = lifecycleInput(context, 'request', adoptionId);
|
||||
const outcomes = await Promise.all([
|
||||
concurrentAdoptedRequest(context, requestInput),
|
||||
concurrentAdoptedRequest(context, requestInput),
|
||||
]);
|
||||
assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']);
|
||||
assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_RECORD_REVISION_STALE');
|
||||
const requests = context.service.list();
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested');
|
||||
context.control.close();
|
||||
});
|
||||
|
||||
test('SCN-integration-adoption-legacy-retries: historical ordinary-request retries remain readable when integration service starts', () => {
|
||||
const context = fixture('torch-adoption-legacy-retries');
|
||||
const insert = context.control.database.prepare(`INSERT INTO integration_requests (
|
||||
id, project_id, source_area, source_branch, source_commit, target_branch,
|
||||
base_target_commit, state, reason, required_checks, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
for (const [id, state] of [['historical-failed', 'blocked'], ['historical-retry', 'superseded']]) {
|
||||
insert.run(id, context.control.projectId, context.contributor, 'refs/heads/candidate', context.candidate,
|
||||
'main', context.base, state, 'historical-retry', '[]', '2026-10-03T00:00:00.000Z', '2026-10-03T00:00:00.000Z');
|
||||
}
|
||||
assert.doesNotThrow(() => new IntegrationService({
|
||||
repositoryRoot: context.root, controlPlane: context.control, checkService: { exactPasses: () => false },
|
||||
}));
|
||||
assert.equal(context.service.list().length, 2);
|
||||
context.control.close();
|
||||
});
|
||||
Reference in new issue
Block a user