mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-07 13:22:14 +00:00
TASK-f40126ee: bind adoption lifecycle revisions
This commit is contained in:
1 parent
18a1b4a2f5
commit
7533d86462
3 files changed
+69
-2
No files matched your search
@@ -3199,3 +3199,47 @@ test_integrity_note:
|
|||||||
did_relax_any_assertion: false
|
did_relax_any_assertion: false
|
||||||
if_true_explain_spec_basis: ""
|
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: ""
|
||||||
|
```
|
||||||
@@ -88,6 +88,12 @@ function exactInput(record, input) {
|
|||||||
if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', {
|
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 },
|
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');
|
this.controlPlane.database.exec('BEGIN IMMEDIATE');
|
||||||
try {
|
try {
|
||||||
const record = this.getPrepared(adoptionId);
|
const record = this.getPrepared(adoptionId);
|
||||||
|
exactInput(record, input);
|
||||||
if (!['finalized', 'requesting'].includes(record.state)) {
|
if (!['finalized', 'requesting'].includes(record.state)) {
|
||||||
throw new TorchError('Only a finalized adoption can request native qualification', {
|
throw new TorchError('Only a finalized adoption can request native qualification', {
|
||||||
code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state },
|
code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state },
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ function prepare(context, adoptionId = 'fixture-adoption') {
|
|||||||
|
|
||||||
function lifecycleInput(context, action, adoptionId, extra = {}) {
|
function lifecycleInput(context, action, adoptionId, extra = {}) {
|
||||||
const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId });
|
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) {
|
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');
|
(error) => error.code === 'ADOPTION_RECORD_REVISION_STALE');
|
||||||
assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })),
|
assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })),
|
||||||
(error) => error.code === 'ADOPTION_IMMUTABLE_BINDING_STALE');
|
(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;
|
const { mutationPerformed, ...durablePrepared } = prepared;
|
||||||
assert.equal(mutationPerformed, true);
|
assert.equal(mutationPerformed, true);
|
||||||
assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared);
|
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';
|
const adoptionId = 'fixture-adoption';
|
||||||
prepare(context, adoptionId);
|
prepare(context, adoptionId);
|
||||||
const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', 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));
|
const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId));
|
||||||
assert.equal(requested.state, 'requested');
|
assert.equal(requested.state, 'requested');
|
||||||
assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef);
|
assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef);
|
||||||
@@ -313,7 +329,7 @@ test('SCN-integration-adoption-request-replay: concurrent authenticated API call
|
|||||||
concurrentAdoptedRequest(context, requestInput),
|
concurrentAdoptedRequest(context, requestInput),
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']);
|
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();
|
const requests = context.service.list();
|
||||||
assert.equal(requests.length, 1);
|
assert.equal(requests.length, 1);
|
||||||
assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested');
|
assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested');
|
||||||
|
|||||||
Reference in new issue
Block a user