TASK-f40126ee: bind adoption lifecycle revisions

This commit is contained in:
thePR0M3TH3AN committed 2026-10-03 18:03:16 -04:00
1 parent 18a1b4a2f5
commit 7533d86462
3 files changed
+69 -2

No files matched your search

+44
View File
@@ -3199,3 +3199,47 @@ test_integrity_note:
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 expectedRecordRevision to finalize or request native qualification"
then: "The operation refuses before private-ref or queue effects; the original refs, journal state, and queue remain unchanged"
observable_outcomes:
- "ADOPTION_RECORD_REVISION_STALE is returned for stale lifecycle callers"
- "Private candidate/archive refs remain absent before stale finalize and no integration request is created by stale native request"
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: ""
```
+7
View File
@@ -88,6 +88,12 @@ function exactInput(record, input) {
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 },
});
}
}
/**
@@ -334,6 +340,7 @@ export class AdoptionService {
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 },
+18 -2
View File
@@ -130,7 +130,8 @@ function prepare(context, adoptionId = 'fixture-adoption') {
function lifecycleInput(context, action, adoptionId, extra = {}) {
const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId });
return { actorId: 'owner', adoptionId, approvalId, approvalRevision, adapter, ...extra };
const record = context.service.getPreparedAdoption(adoptionId);
return { actorId: 'owner', adoptionId, expectedRecordRevision: record.revision, approvalId, approvalRevision, adapter, ...extra };
}
function blockedCodes(error) {
@@ -167,6 +168,17 @@ test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cann
(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);
@@ -243,6 +255,10 @@ test('SCN-integration-adoption-private-ref-intake: finalized private refs enter
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);
@@ -313,7 +329,7 @@ test('SCN-integration-adoption-request-replay: concurrent authenticated API call
concurrentAdoptedRequest(context, requestInput),
]);
assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']);
assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_REQUEST_STATE_INVALID');
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');