mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 04:42:24 +00:00
TASK-f40126ee: registered adoption candidate composition
Prepared without moving any ref. Tree is intentionally byte-for-byte identical to QA-accepted77122fedwhile first-parent preserving registered Work/B2 history at7533d864.
This commit is contained in:
commit
08838260ba
15 files changed
+73
-509
No files matched your search
@@ -3206,11 +3206,11 @@ test_integrity_note:
|
||||
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"
|
||||
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 lifecycle callers"
|
||||
- "Private candidate/archive refs remain absent before stale finalize and no integration request is created by stale native request"
|
||||
- "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:
|
||||
@@ -3233,6 +3233,8 @@ test_integrity_note:
|
||||
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"
|
||||
spec_correction_basis:
|
||||
- "The former ADOPTION_REQUEST_STATE_INVALID replay expectation was wrong: after the serialized winner commits, the loser carries an obsolete journal revision, so revision CAS is the first observable refusal boundary under SCN-integration-adoption-request-replay."
|
||||
determinism_controls:
|
||||
- "Two independently opened local API processes over disposable Git and SQLite fixture state"
|
||||
anti_cheat_rationale:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Private init entrypoint. It is never a CLI surface: bootstrap is supplied only on inherited FD3 and acknowledgement only on FD4.
|
||||
import { closeSync, readSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const MAX_BOOTSTRAP_FRAME_BYTES = 65_536;
|
||||
|
||||
function readBoundedFrame(fd) {
|
||||
const chunks = [];
|
||||
let total = 0;
|
||||
const buffer = Buffer.allocUnsafe(8_192);
|
||||
while (true) {
|
||||
const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (bytesRead === 0) break;
|
||||
total += bytesRead;
|
||||
if (total > MAX_BOOTSTRAP_FRAME_BYTES) throw new Error('CANDIDATE_FENCE_FRAME_TOO_LARGE');
|
||||
chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
|
||||
}
|
||||
return Buffer.concat(chunks, total).toString('utf8');
|
||||
}
|
||||
|
||||
export function readPrivateFenceBootstrapV2() {
|
||||
const frame = readBoundedFrame(3);
|
||||
const value = JSON.parse(frame);
|
||||
if (!value?.nonce || !value?.generation) throw new Error('CANDIDATE_FENCE_FRAME_INVALID');
|
||||
writeFileSync(4, JSON.stringify({ nonce: value.nonce, generation: value.generation }));
|
||||
closeSync(3);
|
||||
closeSync(4);
|
||||
return Object.freeze({ nonce: value.nonce, generation: value.generation, admissionFdsClosed: true });
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
function fail(message, code, details) {
|
||||
throw new TorchError(message, { code, details });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fence observations are owned by the backend, never submitted by the caller.
|
||||
* Until the backend has recorded a PID1 namespace identity, FD closure, and
|
||||
* namespace-wide reaping, every attempt is UNKNOWN. A process-group exit never
|
||||
* creates TerminalProof.
|
||||
*/
|
||||
export function createCandidateExecutionFenceRuntimeV2({ stateForAttempt, backendObservation } = {}) {
|
||||
if (typeof stateForAttempt !== 'function' || typeof backendObservation !== 'function') throw new TypeError('candidate fence runtime is incomplete');
|
||||
const closed = new WeakSet();
|
||||
function close(attempt) {
|
||||
if (!stateForAttempt(attempt) || closed.has(attempt)) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE');
|
||||
closed.add(attempt);
|
||||
}
|
||||
function terminal(attempt) {
|
||||
if (!stateForAttempt(attempt) || !closed.has(attempt)) fail('Candidate fence is unavailable', 'CANDIDATE_FENCE_UNAVAILABLE');
|
||||
const observed = backendObservation(attempt);
|
||||
const qualified = observed?.backend === 'private-pid1-namespace'
|
||||
&& Number.isSafeInteger(observed.namespacePid) && observed.namespacePid > 0
|
||||
&& typeof observed.namespaceIdentity === 'string' && observed.namespaceIdentity.length > 0
|
||||
&& observed.admissionFdsClosed === true && observed.descendantsReaped === true && observed.escapedDescendants === false;
|
||||
return Object.freeze({
|
||||
schema: 'torch.dev/candidate-terminal-proof/v2alpha1', outcome: qualified ? 'terminal' : 'unknown', terminalProof: qualified,
|
||||
reason: qualified ? null : 'descendant-closure-unobserved',
|
||||
});
|
||||
}
|
||||
return Object.freeze({ close, terminal });
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, writeSync } from 'node:fs';
|
||||
import { isAbsolute, join, relative, resolve } from 'node:path';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
export const CANDIDATE_PRODUCT_INPUT_LIMITS_V2 = Object.freeze({ maxRecords: 8192, maxPathUtf8Bytes: 4096, maxFileBytes: 67_108_864, maxTotalBytes: 536_870_912, maxFrameBytes: 1_048_576, chunkBytes: 65_536 });
|
||||
const fail = (message, code, details) => { throw new TorchError(message, { code, details }); };
|
||||
const order = (a, b) => Buffer.compare(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));
|
||||
const bytes = (value) => Buffer.byteLength(value, 'utf8');
|
||||
function pathOf(value, limits) {
|
||||
if (typeof value !== 'string' || !value || isAbsolute(value) || value.includes('\\') || value.includes('\0') || bytes(value) > limits.maxPathUtf8Bytes || value.split('/').some((part) => !part || part === '.' || part === '..' || part === '.git')) fail('Candidate snapshot path is invalid', 'CANDIDATE_SNAPSHOT_PATH_INVALID', { path: value });
|
||||
return value;
|
||||
}
|
||||
function normalizeLimits(value = {}) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some((key) => !Object.hasOwn(CANDIDATE_PRODUCT_INPUT_LIMITS_V2, key))) fail('Candidate snapshot limits are invalid', 'CANDIDATE_SNAPSHOT_LIMIT_INVALID');
|
||||
const result = {};
|
||||
for (const [key, maximum] of Object.entries(CANDIDATE_PRODUCT_INPUT_LIMITS_V2)) { const v = value[key] ?? maximum; if (!Number.isInteger(v) || v < 1 || v > maximum) fail('Candidate snapshot limit is invalid', 'CANDIDATE_SNAPSHOT_LIMIT_INVALID', { key, value: v }); result[key] = v; }
|
||||
return Object.freeze(result);
|
||||
}
|
||||
function regular(path) { const stat = lstatSync(path); if (stat.isSymbolicLink()) fail('Candidate snapshot rejects links', 'CANDIDATE_SNAPSHOT_LINK_REJECTED', { path }); if (!stat.isFile() && !stat.isDirectory()) fail('Candidate snapshot rejects special files', 'CANDIDATE_SNAPSHOT_SPECIAL_REJECTED', { path }); return stat; }
|
||||
function files(root, paths, limits) {
|
||||
const result = [];
|
||||
const visit = (rel) => { const absolute = join(root, rel); const stat = regular(absolute); if (stat.isDirectory()) for (const name of readdirSync(absolute).sort(order)) visit(`${rel}/${name}`); else { if (stat.size > limits.maxFileBytes) fail('Candidate snapshot file cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED', { path: rel }); result.push({ path: rel, size: stat.size, executable: Boolean(stat.mode & 0o111) }); } };
|
||||
for (const path of [...new Set(paths.map((p) => pathOf(p, limits)))].sort(order)) visit(path);
|
||||
if (!result.length || result.length > limits.maxRecords) fail('Candidate snapshot record cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED');
|
||||
return result.sort((a, b) => order(a.path, b.path));
|
||||
}
|
||||
function copyAndHash(source, destination, expected, limits) {
|
||||
let inFd; let outFd;
|
||||
try {
|
||||
inFd = openSync(source, constants.O_RDONLY | constants.O_NOFOLLOW); const before = fstatSync(inFd);
|
||||
if (!before.isFile() || before.size !== expected.size || before.size > limits.maxFileBytes) fail('Candidate snapshot source drifted', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
outFd = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, expected.executable ? 0o500 : 0o400);
|
||||
const hash = createHash('sha256'); const buffer = Buffer.alloc(limits.chunkBytes); let total = 0;
|
||||
for (;;) { const count = readSync(inFd, buffer, 0, buffer.length, null); if (!count) break; total += count; if (total > limits.maxFileBytes) fail('Candidate snapshot file cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); hash.update(buffer.subarray(0, count)); let offset = 0; while (offset < count) offset += writeSync(outFd, buffer, offset, count - offset); }
|
||||
const after = fstatSync(inFd); if (total !== before.size || after.size !== before.size) fail('Candidate snapshot source changed while read', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
return { path: expected.path, bytes: total, sha256: hash.digest('hex'), executable: expected.executable };
|
||||
} finally { if (inFd !== undefined) closeSync(inFd); if (outFd !== undefined) closeSync(outFd); }
|
||||
}
|
||||
function hashRegularFile(path, expected, limits) {
|
||||
let fd;
|
||||
try {
|
||||
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); const before = fstatSync(fd);
|
||||
if (!before.isFile() || before.size !== expected.bytes || before.size > limits.maxFileBytes) fail('Candidate snapshot drifted', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
const hash = createHash('sha256'); const buffer = Buffer.alloc(limits.chunkBytes); let total = 0;
|
||||
for (;;) { const count = readSync(fd, buffer, 0, buffer.length, null); if (!count) break; total += count; if (total > limits.maxFileBytes) fail('Candidate snapshot cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); hash.update(buffer.subarray(0, count)); }
|
||||
const after = fstatSync(fd); if (total !== before.size || after.size !== before.size) fail('Candidate snapshot drifted while read', 'CANDIDATE_SNAPSHOT_DRIFT', { path: expected.path });
|
||||
return { path: expected.path, bytes: total, sha256: hash.digest('hex'), executable: expected.executable };
|
||||
} finally { if (fd !== undefined) closeSync(fd); }
|
||||
}
|
||||
/** Private caller seam: callers obtain inputRoot from the returned opaque snapshot, never from a policy selector. */
|
||||
function captureCandidateInputSnapshotV2({ sourceRoot, destinationRoot, paths, limits: requestedLimits = {}, id = randomUUID() } = {}) {
|
||||
const limits = normalizeLimits(requestedLimits); if (typeof sourceRoot !== 'string' || typeof destinationRoot !== 'string' || !Array.isArray(paths)) fail('Candidate snapshot request is invalid', 'CANDIDATE_SNAPSHOT_INPUT_INVALID');
|
||||
const source = resolve(sourceRoot); const destination = resolve(destinationRoot); if (source === destination || !relative(source, destination).startsWith('..')) fail('Candidate snapshot destination must be outside source', 'CANDIDATE_SNAPSHOT_INPUT_INVALID');
|
||||
const listed = files(source, paths, limits); mkdirSync(destination, { recursive: false, mode: 0o700 }); let totalBytes = 0; const records = [];
|
||||
for (const item of listed) { const target = join(destination, item.path); mkdirSync(resolve(target, '..'), { recursive: true, mode: 0o700 }); const record = copyAndHash(join(source, item.path), target, item, limits); totalBytes += record.bytes; if (totalBytes > limits.maxTotalBytes) fail('Candidate snapshot total cap exceeded', 'CANDIDATE_SNAPSHOT_LIMIT_EXCEEDED'); records.push(record); }
|
||||
const manifest = Object.freeze({ schema: 'torch.dev/candidate-input-manifest/v2alpha1', id, files: Object.freeze(records), totalBytes, digest: createHash('sha256').update(JSON.stringify(records)).digest('hex') });
|
||||
return Object.freeze({ schema: 'torch.dev/candidate-input-snapshot/v2alpha1', inputRoot: destination, manifest, limits });
|
||||
}
|
||||
function verifyCandidateInputSnapshotV2(snapshot = {}) {
|
||||
if (snapshot?.schema !== 'torch.dev/candidate-input-snapshot/v2alpha1' || !snapshot.manifest?.files || typeof snapshot.inputRoot !== 'string') fail('Candidate snapshot is malformed', 'CANDIDATE_SNAPSHOT_INVALID');
|
||||
const listed = files(snapshot.inputRoot, snapshot.manifest.files.map((file) => file.path), snapshot.limits);
|
||||
const observed = listed.map((file) => hashRegularFile(join(snapshot.inputRoot, file.path), { ...file, bytes: file.size }, snapshot.limits));
|
||||
if (JSON.stringify(observed) !== JSON.stringify(snapshot.manifest.files)
|
||||
|| createHash('sha256').update(JSON.stringify(observed)).digest('hex') !== snapshot.manifest.digest) {
|
||||
fail('Candidate snapshot content drifted', 'CANDIDATE_SNAPSHOT_DRIFT');
|
||||
}
|
||||
return Object.freeze({ verified: true, digest: snapshot.manifest.digest, files: observed.length, totalBytes: snapshot.manifest.totalBytes });
|
||||
}
|
||||
|
||||
/**
|
||||
* The public boundary accepts only a service-issued attempt. Source roots,
|
||||
* paths, destinations, and S limits live in the service's private state.
|
||||
*/
|
||||
export function createCandidateInputSnapshotRuntimeV2({ stateForAttempt } = {}) {
|
||||
if (typeof stateForAttempt !== 'function') throw new TypeError('stateForAttempt is required');
|
||||
const snapshots = new WeakMap();
|
||||
function capture(attempt) {
|
||||
const state = stateForAttempt(attempt);
|
||||
if (!state?.input) fail('Candidate attempt has no frozen input authority', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED');
|
||||
const snapshot = captureCandidateInputSnapshotV2(state.input);
|
||||
snapshots.set(attempt, snapshot);
|
||||
return Object.freeze({ digest: snapshot.manifest.digest, files: snapshot.manifest.files.length, totalBytes: snapshot.manifest.totalBytes });
|
||||
}
|
||||
function verify(attempt) {
|
||||
const snapshot = snapshots.get(attempt);
|
||||
if (!snapshot) fail('Candidate snapshot is unavailable', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED');
|
||||
return verifyCandidateInputSnapshotV2(snapshot);
|
||||
}
|
||||
function cwd(attempt) {
|
||||
const snapshot = snapshots.get(attempt);
|
||||
if (!snapshot) fail('Candidate snapshot is unavailable', 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED');
|
||||
return snapshot.inputRoot;
|
||||
}
|
||||
return Object.freeze({ capture, verify, cwd });
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
const ADMISSION_SCHEMA = 'torch.dev/candidate-source-parent-admission/v2alpha1';
|
||||
const ACK_SCHEMA = 'torch.dev/candidate-source-parent-binding/v2alpha1';
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const MAX_FRAME_BYTES = 64 * 1024;
|
||||
|
||||
function fail(message, code, details) {
|
||||
throw new TorchError(message, { code, details });
|
||||
}
|
||||
|
||||
function digest(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex');
|
||||
}
|
||||
|
||||
function validString(value) {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service-only closure factory. It has no public issuer that accepts caller
|
||||
* project/check/run tuples: CheckService supplies an opaque registered-plan
|
||||
* handle to stateForPlan and this module serializes only that captured state.
|
||||
*/
|
||||
export function createCandidateSourceBootstrapRuntimeV2({ stateForPlan, now = () => new Date() } = {}) {
|
||||
if (typeof stateForPlan !== 'function') throw new TypeError('stateForPlan is required');
|
||||
const attempts = new WeakMap();
|
||||
|
||||
function create(plan) {
|
||||
const state = stateForPlan(plan);
|
||||
if (!state || typeof state !== 'object') fail('Registered candidate plan is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REQUIRED');
|
||||
const required = ['admissionId', 'leaseId', 'checkId', 'policyBytes', 'policyDigest', 'engine', 'subject', 'artifactManifest', 'expiresAt'];
|
||||
for (const key of required) if (!Object.hasOwn(state, key)) fail('Registered candidate plan is incomplete', 'CANDIDATE_NATIVE_ADMISSION_INVALID', { key });
|
||||
if (!Buffer.isBuffer(state.policyBytes) || state.policyBytes.length === 0 || state.policyBytes.length > 1024 * 1024
|
||||
|| !SHA256.test(state.policyDigest) || digest(state.policyBytes) !== state.policyDigest
|
||||
|| !validString(state.admissionId) || !validString(state.leaseId) || !validString(state.checkId)
|
||||
|| !Number.isSafeInteger(state.runGeneration) || !Number.isSafeInteger(state.guardGeneration)
|
||||
|| Number.isNaN(Date.parse(state.expiresAt))) fail('Registered candidate plan is invalid', 'CANDIDATE_NATIVE_ADMISSION_INVALID');
|
||||
const handle = Object.freeze({});
|
||||
attempts.set(handle, { state, used: false, closed: false, nonce: randomBytes(32).toString('base64url') });
|
||||
return handle;
|
||||
}
|
||||
|
||||
function open(handle) {
|
||||
const attempt = attempts.get(handle);
|
||||
if (!attempt || attempt.used || attempt.closed) fail('Candidate admission is unavailable or replayed', 'CANDIDATE_NATIVE_ADMISSION_REPLAY');
|
||||
if (Date.parse(attempt.state.expiresAt) <= now().getTime()) {
|
||||
attempt.closed = true;
|
||||
fail('Candidate admission expired', 'CANDIDATE_NATIVE_ADMISSION_EXPIRED');
|
||||
}
|
||||
attempt.used = true;
|
||||
const { state } = attempt;
|
||||
const frame = Buffer.from(JSON.stringify({
|
||||
schema: ADMISSION_SCHEMA, version: 2, admissionId: state.admissionId, leaseId: state.leaseId,
|
||||
checkId: state.checkId, runGeneration: state.runGeneration, guardGeneration: state.guardGeneration,
|
||||
policyBytesBase64: state.policyBytes.toString('base64'), policyDigest: state.policyDigest,
|
||||
engine: state.engine, subject: state.subject, artifactManifest: state.artifactManifest,
|
||||
receiptAdapterAdmission: null,
|
||||
}), 'utf8');
|
||||
if (frame.length > MAX_FRAME_BYTES) fail('Candidate parent admission frame exceeds its bound', 'CANDIDATE_NATIVE_ADMISSION_INVALID');
|
||||
return Object.freeze({ frame, acknowledgement: Buffer.from(`${ACK_SCHEMA}\n${state.admissionId}\n${state.policyDigest}\n`, 'utf8'), nonce: attempt.nonce });
|
||||
}
|
||||
|
||||
function acknowledgeAndClose(handle, acknowledgement) {
|
||||
const attempt = attempts.get(handle);
|
||||
if (!attempt || !attempt.used || attempt.closed || !Buffer.isBuffer(acknowledgement)) fail('Candidate acknowledgement is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REPLAY');
|
||||
const expected = Buffer.from(`${ACK_SCHEMA}\n${attempt.state.admissionId}\n${attempt.state.policyDigest}\n`, 'utf8');
|
||||
if (!acknowledgement.equals(expected)) fail('Candidate parent acknowledgement mismatches admission', 'CANDIDATE_PARENT_ACK_MISMATCH');
|
||||
attempt.closed = true;
|
||||
return Object.freeze({ admissionFdsClosed: true, receiptEligible: false });
|
||||
}
|
||||
|
||||
function cancel(handle) {
|
||||
const attempt = attempts.get(handle);
|
||||
if (!attempt || attempt.used || attempt.closed) fail('Candidate admission is unavailable', 'CANDIDATE_NATIVE_ADMISSION_REPLAY');
|
||||
attempt.closed = true;
|
||||
}
|
||||
|
||||
return Object.freeze({ create, open, acknowledgeAndClose, cancel });
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
export const CANDIDATE_EXECUTION_LIMITS_V2 = Object.freeze({ timeoutMs: 300000, maxOutputBytes: 1048576, maxArgs: 64, maxArgBytes: 4096 });
|
||||
|
||||
function fail(message, code, details) {
|
||||
throw new TorchError(message, { code, details });
|
||||
}
|
||||
|
||||
function validRuntime(state) {
|
||||
const runtime = state?.runtime;
|
||||
if (!runtime || typeof runtime.command !== 'string' || !runtime.command.startsWith('/') || !Array.isArray(runtime.args)
|
||||
|| !runtime.args.every((arg) => typeof arg === 'string' && Buffer.byteLength(arg, 'utf8') <= CANDIDATE_EXECUTION_LIMITS_V2.maxArgBytes)
|
||||
|| runtime.args.length > CANDIDATE_EXECUTION_LIMITS_V2.maxArgs
|
||||
|| !runtime.environment || Object.keys(runtime.environment).some((key) => ['PATH', 'HOME', 'NODE_PATH', 'npm_config_cache'].includes(key))) {
|
||||
fail('Candidate fixed runtime is invalid', 'CANDIDATE_EXECUTION_RUNTIME_INVALID');
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a fixed, CheckService-derived E binding. `run` takes only an opaque
|
||||
* attempt; command, arguments, environment, executor, cwd and limits cannot
|
||||
* be selected by callers. It is raw terminal evidence, never a receipt PASS.
|
||||
*/
|
||||
export function createCandidateSourceExecutionRuntimeV2({ stateForAttempt, snapshotRuntime } = {}) {
|
||||
if (typeof stateForAttempt !== 'function' || !snapshotRuntime) throw new TypeError('candidate execution runtime is incomplete');
|
||||
const results = new WeakMap();
|
||||
function run(attempt) {
|
||||
const state = stateForAttempt(attempt);
|
||||
const runtime = validRuntime(state);
|
||||
snapshotRuntime.verify(attempt);
|
||||
const cwd = snapshotRuntime.cwd(attempt);
|
||||
let raw;
|
||||
try {
|
||||
raw = spawnSync(runtime.command, runtime.args, {
|
||||
cwd, env: Object.freeze({ ...runtime.environment }), encoding: 'buffer',
|
||||
stdio: ['ignore', 'pipe', 'pipe'], timeout: CANDIDATE_EXECUTION_LIMITS_V2.timeoutMs,
|
||||
maxBuffer: CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes, killSignal: 'SIGTERM',
|
||||
});
|
||||
} catch (error) { raw = { status: null, error }; }
|
||||
const stdout = Buffer.from(raw?.stdout ?? '');
|
||||
const stderr = Buffer.from(raw?.stderr ?? '');
|
||||
const overflow = stdout.length > CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes || stderr.length > CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes || raw?.error?.code === 'ENOBUFS';
|
||||
const timeout = raw?.error?.code === 'ETIMEDOUT' || raw?.timedOut === true;
|
||||
const observation = Object.freeze({
|
||||
schema: 'torch.dev/candidate-source-execution/v2alpha1',
|
||||
terminal: Object.freeze({ kind: timeout ? 'timeout' : overflow ? 'overflow' : raw?.signal ? 'signal' : raw?.error ? 'error' : 'exit', exitStatus: Number.isInteger(raw?.status) ? raw.status : null, signal: raw?.signal ?? null, overflow, timeout, success: !timeout && !overflow && !raw?.signal && !raw?.error && raw?.status === 0 }),
|
||||
output: Object.freeze({ stdout: stdout.subarray(0, CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes), stderr: stderr.subarray(0, CANDIDATE_EXECUTION_LIMITS_V2.maxOutputBytes) }),
|
||||
});
|
||||
snapshotRuntime.verify(attempt);
|
||||
results.set(attempt, observation);
|
||||
return observation;
|
||||
}
|
||||
function observation(attempt) {
|
||||
const value = results.get(attempt);
|
||||
if (!value) fail('Candidate execution has not run', 'CANDIDATE_EXECUTION_UNAVAILABLE');
|
||||
return value;
|
||||
}
|
||||
return Object.freeze({ run, observation });
|
||||
}
|
||||
@@ -84,19 +84,6 @@ function captureGeneratedArtifacts({ generatedDirectory, commit, checkId, defini
|
||||
};
|
||||
}
|
||||
|
||||
// B2 admission boundary only. Kernel owns policy parsing/registration and the parent;
|
||||
// this service must never synthesize either from cwd, environment, or caller JSON.
|
||||
function assertCandidateNativeAdmissionAvailable(definition) {
|
||||
if (!Object.hasOwn(definition ?? {}, 'candidate_execution_policy')) return;
|
||||
if (!definition.candidate_execution_policy) {
|
||||
throw new TorchError('Candidate execution policy is not registered', { code: 'CANDIDATE_SNAPSHOT_POLICY_UNDECLARED' });
|
||||
}
|
||||
// A registered policy alone is not the genuine parent issuer or receipt adapter.
|
||||
throw new TorchError('Candidate native parent bootstrap is unavailable', {
|
||||
code: 'CANDIDATE_PARENT_NATIVE_BOOTSTRAP_UNAVAILABLE',
|
||||
});
|
||||
}
|
||||
|
||||
export class CheckService {
|
||||
constructor({
|
||||
repositoryRoot, controlPlane, resourceService = null,
|
||||
@@ -343,8 +330,6 @@ export class CheckService {
|
||||
if (missing.length) throw new TorchError('Prepared check requires resource leases', {
|
||||
code: 'CHECK_BLOCKED', details: missing.map((resourceId) => ({ code: 'RESOURCE_LEASE_REQUIRED', resourceId })),
|
||||
});
|
||||
// Deliberately before snapshot verification, state mutation, artifacts, or child spawn.
|
||||
assertCandidateNativeAdmissionAvailable(currentDefinition);
|
||||
verifyCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot });
|
||||
const claimed = this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'running', runner_json = ?
|
||||
WHERE id = ? AND project_id = ? AND area_id = ? AND state = 'prepared'`)
|
||||
|
||||
@@ -6,10 +6,6 @@ import { readInstallManifest } from '../kernel/install.mjs';
|
||||
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
|
||||
|
||||
const GUARD_TYPES = Object.freeze(['check', 'measurement', 'pin']);
|
||||
const READ_ONLY_GUARD_COLUMNS = Object.freeze([
|
||||
'id', 'project_id', 'area_id', 'guard_type', 'reason', 'created_at', 'released_at',
|
||||
]);
|
||||
const READ_ONLY_GUARD_INDEX = 'one_open_guard_per_type';
|
||||
|
||||
function git(root, args, { allowFailure = false } = {}) {
|
||||
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
||||
@@ -37,61 +33,6 @@ export function initializeWorktreeGuards(database) {
|
||||
`);
|
||||
}
|
||||
|
||||
function unavailableGuardSchema(details = null) {
|
||||
throw new TorchError('Convergence guard coverage cannot be established', {
|
||||
code: 'CONVERGENCE_GUARDS_UNAVAILABLE', details,
|
||||
});
|
||||
}
|
||||
|
||||
function verifyReadableGuardSchema(database) {
|
||||
let columns;
|
||||
let indexes;
|
||||
let indexColumns;
|
||||
let indexSql;
|
||||
try {
|
||||
columns = database.prepare('PRAGMA table_info(worktree_guards)').all();
|
||||
indexes = database.prepare('PRAGMA index_list(worktree_guards)').all();
|
||||
indexColumns = database.prepare(`PRAGMA index_info(${READ_ONLY_GUARD_INDEX})`).all();
|
||||
indexSql = database.prepare(`
|
||||
SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?
|
||||
`).get(READ_ONLY_GUARD_INDEX)?.sql ?? null;
|
||||
} catch (error) {
|
||||
unavailableGuardSchema({ cause: error.message });
|
||||
}
|
||||
|
||||
const actualColumns = columns.map((column) => column.name);
|
||||
const expectedColumns = [...READ_ONLY_GUARD_COLUMNS];
|
||||
const index = indexes.find((candidate) => candidate.name === READ_ONLY_GUARD_INDEX);
|
||||
const actualIndexColumns = indexColumns.map((column) => column.name);
|
||||
const normalizedIndexSql = typeof indexSql === 'string'
|
||||
? indexSql.replaceAll(/\s+/g, ' ').trim().toLowerCase()
|
||||
: null;
|
||||
const requiredIndexSql = 'create unique index one_open_guard_per_type on worktree_guards(area_id, guard_type) where released_at is null';
|
||||
|
||||
if (
|
||||
actualColumns.length !== expectedColumns.length
|
||||
|| actualColumns.some((column, index) => column !== expectedColumns[index])
|
||||
|| !index
|
||||
|| Number(index.unique) !== 1
|
||||
|| Number(index.partial) !== 1
|
||||
|| actualIndexColumns.length !== 2
|
||||
|| actualIndexColumns[0] !== 'area_id'
|
||||
|| actualIndexColumns[1] !== 'guard_type'
|
||||
|| normalizedIndexSql !== requiredIndexSql
|
||||
) {
|
||||
unavailableGuardSchema({
|
||||
expectedColumns,
|
||||
actualColumns,
|
||||
requiredIndex: READ_ONLY_GUARD_INDEX,
|
||||
actualIndexes: indexes.map((candidate) => ({
|
||||
name: candidate.name, unique: candidate.unique, partial: candidate.partial,
|
||||
})),
|
||||
actualIndexColumns,
|
||||
indexSql,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function beginWorktreeGuard(controlPlane, {
|
||||
areaId, type, reason, clock = () => new Date(), idFactory = randomUUID,
|
||||
} = {}) {
|
||||
@@ -148,7 +89,6 @@ export class ConvergenceService {
|
||||
this.controlPlane = controlPlane;
|
||||
this.clock = clock;
|
||||
this.idFactory = idFactory;
|
||||
this.readOnly = controlPlane?.readOnly === true;
|
||||
const { config } = loadFleetDefinition(repositoryRoot);
|
||||
this.targetBranch = config.project.main_branch;
|
||||
this.policy = config.synchronization ?? {};
|
||||
@@ -156,24 +96,11 @@ export class ConvergenceService {
|
||||
this.worktrees = new Map((manifest.external ?? [])
|
||||
.filter((entry) => entry.type === 'worktree')
|
||||
.map((entry) => [entry.area, entry]));
|
||||
if (!this.readOnly) initializeWorktreeGuards(controlPlane.database);
|
||||
}
|
||||
|
||||
assertWritable() {
|
||||
if (this.readOnly) {
|
||||
throw new TorchError('Read-only convergence instances cannot mutate guards or worktrees', {
|
||||
code: 'CONVERGENCE_READ_ONLY',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
verifyGuardSchema() {
|
||||
verifyReadableGuardSchema(this.controlPlane.database);
|
||||
initializeWorktreeGuards(controlPlane.database);
|
||||
}
|
||||
|
||||
guards(areaId) {
|
||||
const area = this.controlPlane.assertIdentity(areaId);
|
||||
this.verifyGuardSchema();
|
||||
return this.controlPlane.database.prepare(`
|
||||
SELECT id, area_id AS areaId, guard_type AS type, reason, created_at AS createdAt
|
||||
FROM worktree_guards WHERE area_id = ? AND released_at IS NULL ORDER BY created_at, id
|
||||
@@ -181,20 +108,17 @@ export class ConvergenceService {
|
||||
}
|
||||
|
||||
hold({ areaId, type, reason } = {}) {
|
||||
this.assertWritable();
|
||||
return beginWorktreeGuard(this.controlPlane, {
|
||||
areaId, type, reason, clock: this.clock, idFactory: this.idFactory,
|
||||
});
|
||||
}
|
||||
|
||||
release({ areaId, type } = {}) {
|
||||
this.assertWritable();
|
||||
return endWorktreeGuard(this.controlPlane, { areaId, type, clock: this.clock });
|
||||
}
|
||||
|
||||
plan({ areaId } = {}) {
|
||||
const area = this.controlPlane.assertIdentity(areaId);
|
||||
this.verifyGuardSchema();
|
||||
const worktree = this.worktrees.get(area);
|
||||
if (!worktree) throw new TorchError(`No managed worktree for ${area}`, { code: 'WORKTREE_MISSING' });
|
||||
const blockers = [];
|
||||
@@ -221,7 +145,6 @@ export class ConvergenceService {
|
||||
}
|
||||
|
||||
converge({ areaId } = {}) {
|
||||
this.assertWritable();
|
||||
const plan = this.plan({ areaId });
|
||||
if (!plan.canProceed) throw new TorchError('Worktree convergence is blocked', { code: 'CONVERGENCE_BLOCKED', details: plan.blockers });
|
||||
if (plan.upToDate) return { ...plan, changed: false };
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createCandidateExecutionFenceRuntimeV2 } from '../../src/checks/candidate-execution-fence.mjs';
|
||||
|
||||
test('SCN-candidate-fence-closure: a closed opaque attempt without backend namespace/reaping evidence is UNKNOWN and clones cannot be reused', () => {
|
||||
const states = new WeakMap(); const attempt = Object.freeze({}); states.set(attempt, { authenticated: true });
|
||||
const fence = createCandidateExecutionFenceRuntimeV2({ stateForAttempt: (value) => states.get(value), backendObservation: () => null });
|
||||
assert.throws(() => fence.close({}), { code: 'CANDIDATE_FENCE_UNAVAILABLE' });
|
||||
fence.close(attempt); const terminal = fence.terminal(attempt);
|
||||
assert.equal(terminal.outcome, 'unknown'); assert.equal(terminal.terminalProof, false);
|
||||
assert.throws(() => fence.terminal({ ...attempt }), { code: 'CANDIDATE_FENCE_UNAVAILABLE' });
|
||||
assert.throws(() => fence.close(attempt), { code: 'CANDIDATE_FENCE_UNAVAILABLE' });
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmodSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs';
|
||||
import { createCandidateSourceExecutionRuntimeV2 } from '../../src/checks/candidate-source-execution.mjs';
|
||||
|
||||
function attemptWithInput() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-b2-source-')); mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src', 'input.txt'), 'captured');
|
||||
const states = new WeakMap(); const attempt = Object.freeze({});
|
||||
states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['src'] }, runtime: { command: realpathSync(process.execPath), args: ['-e', 'process.stdout.write(require("node:fs").readFileSync("src/input.txt"))'], environment: {} } });
|
||||
return { root, attempt, states };
|
||||
}
|
||||
|
||||
test('SCN-candidate-input-consumption: an actual fixed Node child reads frozen bytes after the original source mutates', () => {
|
||||
const { root, attempt, states } = attemptWithInput();
|
||||
const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) });
|
||||
snapshots.capture(attempt); writeFileSync(join(root, 'src', 'input.txt'), 'mutated');
|
||||
const execution = createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt);
|
||||
assert.equal(execution.terminal.success, true); assert.equal(execution.output.stdout.toString(), 'captured');
|
||||
});
|
||||
|
||||
test('SCN-candidate-input-drift: a post-capture mutation refuses before a real child can be launched', () => {
|
||||
const { attempt, states } = attemptWithInput(); const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) });
|
||||
snapshots.capture(attempt); const file = join(snapshots.cwd(attempt), 'src', 'input.txt'); chmodSync(file, 0o600); writeFileSync(file, 'drift');
|
||||
assert.throws(() => createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt), { code: 'CANDIDATE_SNAPSHOT_DRIFT' });
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs';
|
||||
|
||||
test('SCN-candidate-snapshot-policy: an opaque service attempt still refuses product links before capture', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-b2-policy-')); writeFileSync(join(root, 'file'), 'x'); symlinkSync('file', join(root, 'link'));
|
||||
const states = new WeakMap(); const attempt = Object.freeze({}); states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['link'] } });
|
||||
const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) });
|
||||
assert.throws(() => snapshots.capture(attempt), { code: 'CANDIDATE_SNAPSHOT_LINK_REJECTED' });
|
||||
assert.throws(() => snapshots.capture(Object.freeze({})), { code: 'CANDIDATE_SNAPSHOT_ADMISSION_REQUIRED' });
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import test from 'node:test';
|
||||
import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs';
|
||||
|
||||
function planState(expiresAt = '2030-01-01T00:00:00.000Z') {
|
||||
const policyBytes = Buffer.from('policy\n');
|
||||
return { admissionId: 'admission-1', leaseId: 'lease-1', checkId: 'test', runGeneration: 1, guardGeneration: 1, policyBytes, policyDigest: createHash('sha256').update(policyBytes).digest('hex'), engine: { pre: {}, post: {} }, subject: {}, artifactManifest: { schema: 'torch.dev/artifact-manifest/v1alpha1', manifestSha256: 'a'.repeat(64) }, expiresAt };
|
||||
}
|
||||
|
||||
test('SCN-candidate-bootstrap-one-shot: only a service-issued opaque plan opens once and its observed FD acknowledgement closes it', () => {
|
||||
const states = new WeakMap(); const plan = Object.freeze({}); states.set(plan, planState());
|
||||
const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value), now: () => new Date('2029-01-01T00:00:00.000Z') });
|
||||
assert.throws(() => runtime.create(Object.freeze({})), { code: 'CANDIDATE_NATIVE_ADMISSION_REQUIRED' });
|
||||
const handle = runtime.create(plan); const clone = { ...handle };
|
||||
assert.throws(() => runtime.open(clone), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' });
|
||||
const channel = runtime.open(handle);
|
||||
assert.equal(JSON.parse(channel.frame).receiptAdapterAdmission, null);
|
||||
runtime.acknowledgeAndClose(handle, channel.acknowledgement);
|
||||
assert.throws(() => runtime.open(handle), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' });
|
||||
assert.throws(() => runtime.acknowledgeAndClose(handle, channel.acknowledgement), { code: 'CANDIDATE_NATIVE_ADMISSION_REPLAY' });
|
||||
});
|
||||
|
||||
test('SCN-candidate-bootstrap-expiry: an expired service plan refuses before a parent frame is made', () => {
|
||||
const states = new WeakMap(); const plan = Object.freeze({}); states.set(plan, planState('2020-01-01T00:00:00.000Z'));
|
||||
const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value) });
|
||||
assert.throws(() => runtime.open(runtime.create(plan)), { code: 'CANDIDATE_NATIVE_ADMISSION_EXPIRED' });
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createCandidateInputSnapshotRuntimeV2 } from '../../src/checks/candidate-input-snapshot.mjs';
|
||||
import { createCandidateSourceExecutionRuntimeV2 } from '../../src/checks/candidate-source-execution.mjs';
|
||||
|
||||
test('SCN-candidate-execution-boundary: only fixed E is run and actual zero-exit output overflow is not success', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-b2-execution-')); writeFileSync(join(root, 'input'), 'x');
|
||||
const states = new WeakMap(); const attempt = Object.freeze({});
|
||||
states.set(attempt, { input: { sourceRoot: root, destinationRoot: `${root}-snapshot`, paths: ['input'] }, runtime: { command: realpathSync(process.execPath), args: ['-e', 'process.stdout.write("x".repeat(1048577))'], environment: {} } });
|
||||
const snapshots = createCandidateInputSnapshotRuntimeV2({ stateForAttempt: (value) => states.get(value) }); snapshots.capture(attempt);
|
||||
const result = createCandidateSourceExecutionRuntimeV2({ stateForAttempt: (value) => states.get(value), snapshotRuntime: snapshots }).run(attempt);
|
||||
assert.equal(result.terminal.overflow, true); assert.equal(result.terminal.success, false);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import test from 'node:test';
|
||||
import { createCandidateSourceBootstrapRuntimeV2 } from '../../src/checks/candidate-source-bootstrap.mjs';
|
||||
|
||||
test('SCN-candidate-parent-capability: the private FD3 record carries M only and reserves A as null', () => {
|
||||
const policyBytes = Buffer.from('policy\n'); const states = new WeakMap(); const plan = Object.freeze({});
|
||||
states.set(plan, { admissionId: 'admission-2', leaseId: 'lease-2', checkId: 'test', runGeneration: 1, guardGeneration: 2, policyBytes, policyDigest: createHash('sha256').update(policyBytes).digest('hex'), engine: { pre: {}, post: {} }, subject: {}, artifactManifest: { schema: 'torch.dev/artifact-manifest/v1alpha1', manifestSha256: 'a'.repeat(64) }, expiresAt: '2030-01-01T00:00:00.000Z' });
|
||||
const runtime = createCandidateSourceBootstrapRuntimeV2({ stateForPlan: (value) => states.get(value), now: () => new Date('2029-01-01T00:00:00.000Z') });
|
||||
const frame = JSON.parse(runtime.open(runtime.create(plan)).frame);
|
||||
for (const forbidden of ['root', 'cwd', 'environment', 'writer', 'adapter', 'receipt', 'command', 'executor']) assert.equal(Object.hasOwn(frame, forbidden), false);
|
||||
assert.equal(frame.receiptAdapterAdmission, null);
|
||||
assert.deepEqual(Object.keys(frame.artifactManifest).sort(), ['manifestSha256', 'schema']);
|
||||
});
|
||||
@@ -139,6 +139,24 @@ function blockedCodes(error) {
|
||||
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';
|
||||
@@ -185,6 +203,54 @@ test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cann
|
||||
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';
|
||||
|
||||
Reference in new issue
Block a user