agentic-workflow-manager 9.12.3 → 9.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/watch/admission-recovery.js +13 -2
- package/dist/src/commands/watch/apply.js +74 -0
- package/dist/src/commands/watch/supervisor.js +13 -6
- package/dist/src/core/admission/index.js +2 -0
- package/dist/src/core/admission/registry-contracts.js +74 -3
- package/dist/tests/commands/watch/admission-recovery.test.js +44 -0
- package/dist/tests/commands/watch/reopened-196.test.js +343 -11
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -32,7 +32,12 @@ function sameContext(a, b) {
|
|
|
32
32
|
&& a.runtime?.accountScopeDigest === b.runtime?.accountScopeDigest;
|
|
33
33
|
}
|
|
34
34
|
function admissionCustodyCause(reason) {
|
|
35
|
-
if (!reason
|
|
35
|
+
if (!reason)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (reason === 'runtime local cambió durante generacion admitida'
|
|
38
|
+
|| /^runtime local no verificable: (?:NATIVE_QUERY_FAILED|ACCOUNT_UNVERIFIED|MACHINE_KEY_MISSING|HOOK_CAPTURE_FAILED)$/.test(reason))
|
|
39
|
+
return 'runtime';
|
|
40
|
+
if ((reason.match(/ADMISSION_[A-Z_]+/g) ?? []).length !== 1)
|
|
36
41
|
return undefined;
|
|
37
42
|
const old = reason.startsWith('admisión compacta desatendida bloqueada antes de dispatch: ');
|
|
38
43
|
if ((old || reason.startsWith('admisión currentness no verificable tras 3 reintentos: '))
|
|
@@ -46,6 +51,12 @@ function admissionCustodyCause(reason) {
|
|
|
46
51
|
function assertNoAmbiguousWork(repoRoot, branch, state) {
|
|
47
52
|
if ((0, types_1.activeRequestProblems)(state).length > 0)
|
|
48
53
|
throw new Error('recovery bloqueado: request rechazada o corrupta activa');
|
|
54
|
+
if (state.routingAttempts?.some(attempt => attempt.state !== 'reserved'
|
|
55
|
+
|| attempt.nativeAgentId !== undefined || attempt.nativeEventDigest !== undefined
|
|
56
|
+
|| attempt.observed !== undefined))
|
|
57
|
+
throw new Error('recovery bloqueado: intento nativo activo o ambiguo');
|
|
58
|
+
if (state.dispatches.some(dispatch => dispatch.routingAttemptId !== undefined))
|
|
59
|
+
throw new Error('recovery bloqueado: dispatch nativo ACK-applied sin prueba de child terminado');
|
|
49
60
|
for (const generation of state.generations) {
|
|
50
61
|
if ((0, generations_1.controllerGenerationHasUnresolvedClaim)(repoRoot, branch, generation))
|
|
51
62
|
throw new Error('recovery bloqueado: claim de controller sin identidad demostrable');
|
|
@@ -87,7 +98,7 @@ async function recoverAdmissionCustody(repoRoot, branch, input, dispatchAdmissio
|
|
|
87
98
|
&& sameContext(previous.context, context))
|
|
88
99
|
return 'already-recovered';
|
|
89
100
|
if (state.cycle.status !== 'BLOCKED' || !admissionCustodyCause(state.cycle.blockedReason)) {
|
|
90
|
-
throw new Error('recovery solo admite custodia causada por admisión currentness o
|
|
101
|
+
throw new Error('recovery solo admite custodia causada por admisión currentness, sensores o runtime local verificable');
|
|
91
102
|
}
|
|
92
103
|
const latest = state.generations.at(-1);
|
|
93
104
|
if (latest?.token !== input.generationToken || (latest === undefined && input.generationToken !== undefined)) {
|
|
@@ -29,6 +29,7 @@ const machine_key_1 = require("../../core/model-policy/machine-key");
|
|
|
29
29
|
const paths_2 = require("../../core/paths");
|
|
30
30
|
const native_routing_proof_1 = require("../../core/model-policy/native-routing-proof");
|
|
31
31
|
const selection_policy_digest_1 = require("../../core/model-policy/selection-policy-digest");
|
|
32
|
+
const process_1 = require("../../core/journal/process");
|
|
32
33
|
function now() { return new Date().toISOString(); }
|
|
33
34
|
function currentNativeAttempt(repoRoot, envelope, eventScope, at) {
|
|
34
35
|
if (!eventScope || JSON.stringify(eventScope.runtime) !== JSON.stringify(envelope.runtime))
|
|
@@ -494,7 +495,80 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
|
|
|
494
495
|
}
|
|
495
496
|
else if (p.routingAttemptId !== undefined)
|
|
496
497
|
throw new Error('register --entity dispatch: routingAttemptId solo para compact v2');
|
|
498
|
+
const noLiveJobs = () => Object.values(s.jobs).every(job => ['exited', 'cancelled'].includes(job.executionState)
|
|
499
|
+
&& [job.processRef, job.wrapperRef].every(ref => !ref || (!(0, process_1.refIsAlive)(ref) && (0, process_1.groupIsGone)(ref.processGroup))));
|
|
500
|
+
const historical = v2 ? s.dispatches.filter(d => d.taskId === taskId && !d.routingAttemptId) : [];
|
|
501
|
+
// A blocked routing verdict does not prove its native child has
|
|
502
|
+
// stopped. This migration permits exactly the first linked handoff;
|
|
503
|
+
// a subsequent retry needs a separately verified ownership path.
|
|
504
|
+
if (historical.some(d => d.id !== dispatchId))
|
|
505
|
+
throw new Error('register --entity dispatch: historical unlinked dispatch ID conflictivo');
|
|
497
506
|
const existing = s.dispatches.find(d => d.id === dispatchId);
|
|
507
|
+
// A 9.12.2 controller could ACK the unlinked intent before it
|
|
508
|
+
// reserved the native route. Keep that record and ACK as history;
|
|
509
|
+
// only a fresh request can create the first linked handoff.
|
|
510
|
+
if (v2 && existing?.taskId === taskId && !existing.routingAttemptId && !existing.dispatchRequestId && routingAttemptId) {
|
|
511
|
+
const attempt = s.routingAttempts.find(candidate => candidate.id === routingAttemptId);
|
|
512
|
+
const linkedReplay = s.dispatches.find(d => d.dispatchRequestId === env.requestId);
|
|
513
|
+
if (linkedReplay) {
|
|
514
|
+
const replayAck = s.appliedRequests[env.requestId];
|
|
515
|
+
if (linkedReplay.taskId !== taskId || linkedReplay.routingAttemptId !== routingAttemptId
|
|
516
|
+
|| replayAck?.outcome !== 'applied' || replayAck.idempotencyKey !== env.idempotencyKey
|
|
517
|
+
|| replayAck.payloadDigest !== digest || replayAck.resultRef !== linkedReplay.id)
|
|
518
|
+
throw new Error('register --entity dispatch: routed replay conflictivo');
|
|
519
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: linkedReplay.id });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const linkedPrior = s.dispatches.find(d => d.routingAttemptId === routingAttemptId);
|
|
523
|
+
if (linkedPrior) {
|
|
524
|
+
const priorAck = linkedPrior.dispatchRequestId ? s.appliedRequests[linkedPrior.dispatchRequestId] : undefined;
|
|
525
|
+
if (linkedPrior.taskId !== taskId || priorAck?.outcome !== 'applied'
|
|
526
|
+
|| priorAck.idempotencyKey !== env.idempotencyKey || priorAck.payloadDigest !== digest)
|
|
527
|
+
throw new Error('register --entity dispatch: routed replay conflictivo');
|
|
528
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: linkedPrior.id });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const oldAt = Date.parse(existing.at);
|
|
532
|
+
const reservedAt = Date.parse(attempt.reservedAt ?? '');
|
|
533
|
+
// 9.12.2 emits {...JSON.parse(--json), entity}; JSON.stringify
|
|
534
|
+
// preserves caller key order. The old record has no request
|
|
535
|
+
// link, so accept only permutations of these exact three
|
|
536
|
+
// fields, not an unrelated resultRef or extra payload fields.
|
|
537
|
+
const legacyDigests = [
|
|
538
|
+
{ entity: 'dispatch', dispatchId: existing.id, taskId },
|
|
539
|
+
{ entity: 'dispatch', taskId, dispatchId: existing.id },
|
|
540
|
+
{ dispatchId: existing.id, entity: 'dispatch', taskId },
|
|
541
|
+
{ dispatchId: existing.id, taskId, entity: 'dispatch' },
|
|
542
|
+
{ taskId, entity: 'dispatch', dispatchId: existing.id },
|
|
543
|
+
{ taskId, dispatchId: existing.id, entity: 'dispatch' },
|
|
544
|
+
].map(payload => (0, requests_1.digestOf)(payload));
|
|
545
|
+
const appliedAcks = Object.values(s.appliedRequests);
|
|
546
|
+
const oldAckIndex = appliedAcks.findIndex(ack => ack.outcome === 'applied'
|
|
547
|
+
&& ack.resultRef === existing.id && legacyDigests.includes(ack.payloadDigest));
|
|
548
|
+
const reservationAckIndex = appliedAcks.findIndex(ack => ack.requestId === attempt.reservationRequestId);
|
|
549
|
+
// The single writer appends ACKs to appliedRequests and persists
|
|
550
|
+
// their insertion order. Equal millisecond timestamps need this
|
|
551
|
+
// additional durable ordering evidence, not an inferred child.
|
|
552
|
+
const legacyPrecedesReservation = oldAt < reservedAt
|
|
553
|
+
|| (oldAt === reservedAt && oldAckIndex >= 0 && reservationAckIndex > oldAckIndex);
|
|
554
|
+
const safeLegacy = reservedForNewDispatch && Number.isFinite(oldAt) && Number.isFinite(reservedAt)
|
|
555
|
+
&& legacyPrecedesReservation
|
|
556
|
+
&& oldAckIndex >= 0
|
|
557
|
+
&& s.dispatches.filter(d => d.taskId === taskId).length === 1
|
|
558
|
+
&& s.tasks.find(t => t.id === taskId)?.attempts === 1
|
|
559
|
+
&& s.tasks.find(t => t.id === taskId)?.status === 'in-progress'
|
|
560
|
+
&& noLiveJobs()
|
|
561
|
+
&& !s.routingAttempts?.some(candidate => candidate.envelope.sliceId === taskId
|
|
562
|
+
&& (candidate.nativeAgentId || candidate.nativeEventDigest || candidate.observed));
|
|
563
|
+
if (!safeLegacy)
|
|
564
|
+
throw new Error('register --entity dispatch: legacy handoff no reconciliable sin evidencia exclusiva');
|
|
565
|
+
const linkedId = `routed-${crypto_1.default.createHash('sha256').update(env.requestId).digest('hex').slice(0, 32)}`;
|
|
566
|
+
if (s.dispatches.some(d => d.id === linkedId) || s.dispatches.some(d => d.routingAttemptId === routingAttemptId))
|
|
567
|
+
throw new Error('register --entity dispatch: routed ID o reservation conflictivo');
|
|
568
|
+
s.dispatches.push({ id: linkedId, taskId, at: now(), routingAttemptId, dispatchRequestId: env.requestId });
|
|
569
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: linkedId });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
498
572
|
if (existing && (existing.taskId !== taskId || existing.routingAttemptId !== routingAttemptId)) {
|
|
499
573
|
throw new Error('register --entity dispatch: dispatchId conflictivo');
|
|
500
574
|
}
|
|
@@ -53,7 +53,7 @@ function routingFacts(provider, identity, cwd, deps = {}) {
|
|
|
53
53
|
capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at };
|
|
54
54
|
}
|
|
55
55
|
function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {}, autonomy) {
|
|
56
|
-
return async () => {
|
|
56
|
+
return async (request = {}) => {
|
|
57
57
|
const observed = (0, store_1.readJournal)(repoRoot, branch);
|
|
58
58
|
const binding = observed.state?.schema === 2 ? observed.state.planBinding : undefined;
|
|
59
59
|
if (!binding || binding.executionMode !== 'desatendido') {
|
|
@@ -92,6 +92,7 @@ function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {
|
|
|
92
92
|
routing = routingFacts(provider, identity, repoRoot, deps);
|
|
93
93
|
return (deps.admit ?? registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
|
|
94
94
|
executionMode: 'desatendido', requireCurrent: true, verifySensors: true,
|
|
95
|
+
freshSensorsRequired: request.freshSensorsRequired === true,
|
|
95
96
|
journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path,
|
|
96
97
|
routing,
|
|
97
98
|
controllerAutonomy: autonomy });
|
|
@@ -370,11 +371,17 @@ class Supervisor {
|
|
|
370
371
|
try {
|
|
371
372
|
local = await (0, local_event_scope_1.queryLocalEventScope)(this.repoRoot, expected.target, expected.kind);
|
|
372
373
|
}
|
|
373
|
-
catch
|
|
374
|
-
|
|
374
|
+
catch {
|
|
375
|
+
// Stable, non-sensitive custody reason remains recoverable
|
|
376
|
+
// after a transient native query or permission failure.
|
|
377
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime local no verificable: NATIVE_QUERY_FAILED');
|
|
378
|
+
return 'custody';
|
|
379
|
+
}
|
|
380
|
+
if (local.state !== 'current') {
|
|
381
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `runtime local no verificable: ${local.reason}`);
|
|
375
382
|
return 'custody';
|
|
376
383
|
}
|
|
377
|
-
if (
|
|
384
|
+
if (JSON.stringify(local.scope.runtime) !== JSON.stringify(expected)) {
|
|
378
385
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime local cambió durante generacion admitida');
|
|
379
386
|
return 'custody';
|
|
380
387
|
}
|
|
@@ -814,7 +821,7 @@ class Supervisor {
|
|
|
814
821
|
// after RED edits, runtime drift, or a currentness change.
|
|
815
822
|
let replacementAdmission;
|
|
816
823
|
try {
|
|
817
|
-
replacementAdmission = await this.dispatchAdmission();
|
|
824
|
+
replacementAdmission = await this.dispatchAdmission({ freshSensorsRequired: true });
|
|
818
825
|
}
|
|
819
826
|
catch (error) {
|
|
820
827
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo no verificable: ${error.message}`);
|
|
@@ -822,7 +829,7 @@ class Supervisor {
|
|
|
822
829
|
}
|
|
823
830
|
if (replacementAdmission.state !== 'admitted' || replacementAdmission.executionMode !== 'desatendido'
|
|
824
831
|
|| replacementAdmission.currentness !== 'current' || replacementAdmission.sensors !== 'pass'
|
|
825
|
-
|| replacementAdmission.journal !== 'current') {
|
|
832
|
+
|| replacementAdmission.journal !== 'current' || replacementAdmission.sensorEvidence === 'generation-bound') {
|
|
826
833
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo bloqueada antes de dispatch: ${replacementAdmission.diagnostics.map(d => `${d.code}: ${d.message}`).join('; ')}`);
|
|
827
834
|
return 'custody';
|
|
828
835
|
}
|
|
@@ -166,6 +166,8 @@ function sanitizeAdmissionReport(report) {
|
|
|
166
166
|
invalidAdmission();
|
|
167
167
|
if (value.cliCurrentness !== undefined && !CLI_CURRENTNESS.has(value.cliCurrentness))
|
|
168
168
|
invalidAdmission();
|
|
169
|
+
if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound')
|
|
170
|
+
invalidAdmission();
|
|
169
171
|
if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
|
|
170
172
|
invalidAdmission();
|
|
171
173
|
if (value.executionMode !== undefined && value.executionMode !== 'interactivo' && value.executionMode !== 'desatendido')
|
|
@@ -19,6 +19,10 @@ const versioning_1 = require("../versioning");
|
|
|
19
19
|
const check_1 = require("../currentness/check");
|
|
20
20
|
const run_1 = require("../../commands/sensors/run");
|
|
21
21
|
const index_1 = require("./index");
|
|
22
|
+
const store_1 = require("../journal/store");
|
|
23
|
+
const paths_1 = require("../journal/paths");
|
|
24
|
+
const generations_1 = require("../../commands/watch/generations");
|
|
25
|
+
const process_1 = require("../journal/process");
|
|
22
26
|
const FRAMEWORK_CONTRACTS = ['using-awm', 'writing-plans', 'development-process', 'subagent-driven-development', 'executing-plans', 'post-implementation-qa', 'post-implementation-docs', 'harness-retro', 'finishing-a-development-branch'];
|
|
23
27
|
const MAX_CONTRACT_BYTES = 1024 * 1024;
|
|
24
28
|
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
@@ -217,6 +221,56 @@ function registryCompatibility(registries, consumed) {
|
|
|
217
221
|
}
|
|
218
222
|
return diagnostics;
|
|
219
223
|
}
|
|
224
|
+
/** A controller may ask for admission while its supervisor is writing journal
|
|
225
|
+
* events. The already-launched generation passed sensors before launch; its
|
|
226
|
+
* durable intent plus verified live process identity is the evidence, not a
|
|
227
|
+
* second tree snapshot. A claim alone never certifies process ownership. */
|
|
228
|
+
function hasOwnedGenerationAdmission(input) {
|
|
229
|
+
if (!input.verifySensors || input.freshSensorsRequired || input.executionMode !== 'desatendido' || input.plan.state !== 'valid'
|
|
230
|
+
|| input.journalCorrupt || !input.journalState || input.journalState.schema !== 2
|
|
231
|
+
|| input.journalState.cycle.status !== 'IN_PROGRESS'
|
|
232
|
+
|| input.journalState.admissionContext?.provider !== input.provider
|
|
233
|
+
|| input.journalState.admissionContext.controllerAutonomy !== input.controllerAutonomy)
|
|
234
|
+
return false;
|
|
235
|
+
const observed = input.journalState;
|
|
236
|
+
const binding = observed.planBinding;
|
|
237
|
+
if (!binding)
|
|
238
|
+
return false;
|
|
239
|
+
if (input.plan.schema === 'compact-slices/v2' && !observed.admissionContext?.runtime)
|
|
240
|
+
return false;
|
|
241
|
+
let expectedPath;
|
|
242
|
+
try {
|
|
243
|
+
expectedPath = (0, paths_1.bindingPlanPath)(input.planPath ?? '');
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (binding.path !== expectedPath || binding.digest !== input.plan.planDigest || binding.schema !== input.plan.schema
|
|
249
|
+
|| (binding.executionDigest !== undefined && binding.executionDigest !== input.plan.executionDigest))
|
|
250
|
+
return false;
|
|
251
|
+
let fresh;
|
|
252
|
+
try {
|
|
253
|
+
fresh = (0, store_1.readJournal)(input.cwd, observed.branch);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (fresh.corrupt || !fresh.state || fresh.state.cycle.status !== 'IN_PROGRESS'
|
|
259
|
+
|| fresh.state.revision < observed.revision || JSON.stringify(fresh.state.planBinding) !== JSON.stringify(binding)
|
|
260
|
+
|| JSON.stringify(fresh.state.admissionContext) !== JSON.stringify(observed.admissionContext))
|
|
261
|
+
return false;
|
|
262
|
+
const generation = (0, generations_1.activeGeneration)(fresh.state);
|
|
263
|
+
if (!generation || generation !== fresh.state.generations.at(-1)
|
|
264
|
+
|| generation.provider !== input.provider || !generation.controllerJobId || !generation.launchArgvDigest)
|
|
265
|
+
return false;
|
|
266
|
+
const prior = observed.generations.find(item => item.token === generation.token);
|
|
267
|
+
if (!prior || prior.controllerJobId !== generation.controllerJobId || prior.launchArgvDigest !== generation.launchArgvDigest)
|
|
268
|
+
return false;
|
|
269
|
+
return (generation.processRef !== undefined && generation.processRef.spawnNonce === generation.spawnNonce
|
|
270
|
+
&& generation.processRef.argvDigest === generation.launchArgvDigest && (0, process_1.refIsAlive)(generation.processRef))
|
|
271
|
+
|| (generation.wrapperRef !== undefined && generation.wrapperRef.spawnNonce === generation.spawnNonce
|
|
272
|
+
&& (0, process_1.refIsAlive)(generation.wrapperRef));
|
|
273
|
+
}
|
|
220
274
|
/** One admission authority for the public command AND actual watch dispatch.
|
|
221
275
|
* No empirical execution occurs before provenance/currentness/CLI-floor gates. */
|
|
222
276
|
async function admitRegistryPlan(input, dependencies = {}) {
|
|
@@ -253,12 +307,14 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
253
307
|
}
|
|
254
308
|
return scope.provenance === 'proven';
|
|
255
309
|
};
|
|
310
|
+
let generationBound = false;
|
|
256
311
|
const compose = async (fields) => {
|
|
257
312
|
const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...fields });
|
|
313
|
+
const withEvidence = generationBound && report.sensors === 'pass' ? { ...report, sensorEvidence: 'generation-bound' } : report;
|
|
258
314
|
if (scope.provenanceDiagnostic && report.diagnostics.some(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED')) {
|
|
259
|
-
return { ...
|
|
315
|
+
return { ...withEvidence, diagnostics: report.diagnostics.map(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' ? scope.provenanceDiagnostic : diagnostic) };
|
|
260
316
|
}
|
|
261
|
-
return
|
|
317
|
+
return withEvidence;
|
|
262
318
|
};
|
|
263
319
|
let currentness = input.currentness;
|
|
264
320
|
let compatibilityDiagnostics;
|
|
@@ -277,7 +333,10 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
277
333
|
if (compatibilityGate.currentness !== 'current')
|
|
278
334
|
return compatibilityGate;
|
|
279
335
|
}
|
|
280
|
-
|
|
336
|
+
generationBound = input.sensors === undefined && hasOwnedGenerationAdmission(input);
|
|
337
|
+
const sensors = input.verifySensors ? input.sensors ?? (generationBound
|
|
338
|
+
? { sensors: [], overall: 'pass', reason: 'generation-bound-admission' }
|
|
339
|
+
: await (dependencies.runSensors ?? run_1.runSensors)({ cwd: input.cwd, all: true, readOnly: true })) : undefined;
|
|
281
340
|
if (input.requireCurrent && input.verifySensors) {
|
|
282
341
|
if (!refresh())
|
|
283
342
|
return compose({ currentness, sensors });
|
|
@@ -286,5 +345,17 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
286
345
|
return currentnessGate;
|
|
287
346
|
compatibilityDiagnostics = registryCompatibility(registries, scope.consumedRegistryComponents);
|
|
288
347
|
}
|
|
348
|
+
if (generationBound && input.plan.schema === 'compact-slices/v2') {
|
|
349
|
+
const routing = input.routing ?? await dependencies.readRouting?.();
|
|
350
|
+
const expected = input.journalState?.admissionContext?.runtime;
|
|
351
|
+
if (!expected || !routing?.runtime || routing.runtime.target !== input.provider
|
|
352
|
+
|| routing.runtime.kind !== expected.kind || routing.runtime.version !== expected.version
|
|
353
|
+
|| routing.runtime.accountScopeDigest !== expected.accountScopeDigest) {
|
|
354
|
+
return compose({ currentness, compatibilityDiagnostics, sensors,
|
|
355
|
+
routing: { ...routing, eventIssue: { code: 'ADMISSION_GENERATION_RUNTIME_MISMATCH',
|
|
356
|
+
message: 'Live runtime identity differs from the admitted controller generation; stop and recover with a fresh admission.' } } });
|
|
357
|
+
}
|
|
358
|
+
return compose({ currentness, compatibilityDiagnostics, sensors, routing });
|
|
359
|
+
}
|
|
289
360
|
return compose({ currentness, compatibilityDiagnostics, sensors, ...(dependencies.readRouting ? { routingReader: dependencies.readRouting } : {}) });
|
|
290
361
|
}
|
|
@@ -51,6 +51,7 @@ const supervisor_1 = require("../../../src/commands/watch/supervisor");
|
|
|
51
51
|
const admission_recovery_1 = require("../../../src/commands/watch/admission-recovery");
|
|
52
52
|
const watch_1 = require("../../../src/commands/watch");
|
|
53
53
|
const supervisorModule = __importStar(require("../../../src/commands/watch/supervisor"));
|
|
54
|
+
const routing_approval_1 = require("../../helpers/routing-approval");
|
|
54
55
|
const DIGEST = 'a'.repeat(64);
|
|
55
56
|
const identity = { kind: 'native', version: '1.0.0', accountScopeDigest: DIGEST };
|
|
56
57
|
const input = { provider: 'codex', routingIdentity: identity, controllerAutonomy: 'approval-free',
|
|
@@ -157,6 +158,29 @@ describe('issue #196: admission custody recovery', () => {
|
|
|
157
158
|
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('BLOCKED');
|
|
158
159
|
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).toBe('recovered');
|
|
159
160
|
});
|
|
161
|
+
test.each([
|
|
162
|
+
'runtime local no verificable: NATIVE_QUERY_FAILED',
|
|
163
|
+
'runtime local cambió durante generacion admitida',
|
|
164
|
+
])('runtime custody %s recovers only after strict offline revalidation', async (reason) => {
|
|
165
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'plans', 'cycle.md'), fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2/valid.md'), 'utf8'));
|
|
166
|
+
const v2Plan = (0, validate_1.validatePlanFile)('plans/cycle.md', repo);
|
|
167
|
+
if (v2Plan.state !== 'valid' || !v2Plan.executionDigest)
|
|
168
|
+
throw new Error('invalid v2 recovery fixture');
|
|
169
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
170
|
+
state.planBinding = { ...state.planBinding, schema: v2Plan.schema, digest: v2Plan.planDigest,
|
|
171
|
+
executionDigest: v2Plan.executionDigest, executionIdentitySchema: 'awm-plan-execution/v1' };
|
|
172
|
+
state.cycle.blockedReason = reason;
|
|
173
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
174
|
+
const revision = (0, store_1.readJournal)(repo, 'main').state.revision;
|
|
175
|
+
await expect((0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, blocked('ADMISSION_SENSORS_BLOCKED', 'current', 'not-certified'))).rejects.toThrow();
|
|
176
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revision);
|
|
177
|
+
expect(await (0, admission_recovery_1.recoverAdmissionCustody)(repo, 'main', input, admitted)).toBe('recovered');
|
|
178
|
+
const recovered = (0, store_1.readJournal)(repo, 'main').state;
|
|
179
|
+
expect(recovered.cycle.status).toBe('IN_PROGRESS');
|
|
180
|
+
expect(recovered.planBinding?.schema).toBe('compact-slices/v2');
|
|
181
|
+
expect(recovered.admissionRecoveries?.at(-1)?.blockedReason).toBe(reason);
|
|
182
|
+
expect(recovered.generations).toEqual(state.generations);
|
|
183
|
+
});
|
|
160
184
|
test('changed binding, active request problem, pending job and identity mismatch all fail closed', async () => {
|
|
161
185
|
const check = async (mutate, pattern) => {
|
|
162
186
|
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
@@ -181,6 +205,26 @@ describe('issue #196: admission custody recovery', () => {
|
|
|
181
205
|
const sJob = (0, store_1.readJournal)(repo, 'main').state;
|
|
182
206
|
sJob.jobs = {};
|
|
183
207
|
(0, store_1.writeJournal)(repo, 'main', sJob);
|
|
208
|
+
await check(state => {
|
|
209
|
+
state.routingAttempts = [{ schema: 'routing-attempt/v1', id: 'route-1',
|
|
210
|
+
obligationId: 'S1-implementer', lineageId: 'S1', attempt: 1, envelope: routing_approval_1.routingEnvelope,
|
|
211
|
+
envelopeDigest: 'e'.repeat(64), fingerprint: 'f'.repeat(64), state: 'active',
|
|
212
|
+
nativeAgentId: 'native-child' }];
|
|
213
|
+
}, /nativ|routing|attempt|intento/i);
|
|
214
|
+
const sRouting = (0, store_1.readJournal)(repo, 'main').state;
|
|
215
|
+
sRouting.routingAttempts = [];
|
|
216
|
+
(0, store_1.writeJournal)(repo, 'main', sRouting);
|
|
217
|
+
await check(state => {
|
|
218
|
+
state.routingAttempts = [{ schema: 'routing-attempt/v1', id: 'route-1',
|
|
219
|
+
obligationId: 'S1-implementer', lineageId: 'S1', attempt: 1, envelope: routing_approval_1.routingEnvelope,
|
|
220
|
+
envelopeDigest: 'e'.repeat(64), fingerprint: 'f'.repeat(64), state: 'reserved' }];
|
|
221
|
+
state.dispatches.push({ id: 'linked', taskId: 'S1', at: new Date().toISOString(),
|
|
222
|
+
routingAttemptId: 'route-1', dispatchRequestId: 'linked-ack' });
|
|
223
|
+
}, /nativ|routing|attempt|intento|dispatch/i);
|
|
224
|
+
const sPendingChild = (0, store_1.readJournal)(repo, 'main').state;
|
|
225
|
+
sPendingChild.routingAttempts = [];
|
|
226
|
+
sPendingChild.dispatches = [];
|
|
227
|
+
(0, store_1.writeJournal)(repo, 'main', sPendingChild);
|
|
184
228
|
await check(state => { state.generations[0].processRef = (0, process_1.captureSelfRef)('recovery-test'); }, /controller/i);
|
|
185
229
|
const sController = (0, store_1.readJournal)(repo, 'main').state;
|
|
186
230
|
sController.generations[0].processRef = undefined;
|
|
@@ -55,6 +55,7 @@ const supervisor_1 = require("../../../src/commands/watch/supervisor");
|
|
|
55
55
|
const supervisorModule = __importStar(require("../../../src/commands/watch/supervisor"));
|
|
56
56
|
const generationModule = __importStar(require("../../../src/commands/watch/generations"));
|
|
57
57
|
const adapterModule = __importStar(require("../../../src/core/journal/adapter"));
|
|
58
|
+
const registry_contracts_1 = require("../../../src/core/admission/registry-contracts");
|
|
58
59
|
const routing_approval_1 = require("../../helpers/routing-approval");
|
|
59
60
|
const routing_approval_2 = require("../../helpers/routing-approval");
|
|
60
61
|
const canonical_1 = require("../../../src/core/model-policy/canonical");
|
|
@@ -67,6 +68,7 @@ const localScope = __importStar(require("../../../src/core/model-policy/local-ev
|
|
|
67
68
|
const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
|
|
68
69
|
const paths_1 = require("../../../src/core/journal/paths");
|
|
69
70
|
const paths_2 = require("../../../src/core/journal/paths");
|
|
71
|
+
const process_1 = require("../../../src/core/journal/process");
|
|
70
72
|
function v2State() {
|
|
71
73
|
const state = (0, types_1.emptyState)('main');
|
|
72
74
|
state.schema = 2;
|
|
@@ -90,6 +92,11 @@ function dispatch(state, payload) {
|
|
|
90
92
|
generationToken: 'g1', idempotencyKey: 'dispatch-key', payload: { entity: 'dispatch',
|
|
91
93
|
dispatchId: 'dispatch-S1', taskId: 'S1', ...payload } }, 'd'.repeat(64), process.cwd());
|
|
92
94
|
}
|
|
95
|
+
function legacyDispatchDigest(rawJson = '{"dispatchId":"dispatch-S1","taskId":"S1"}') {
|
|
96
|
+
// 9.12.2 job register emits { ...JSON.parse(--json), entity: --entity }.
|
|
97
|
+
// Preserve the caller's key order: digestOf uses JSON.stringify bytes.
|
|
98
|
+
return (0, requests_1.digestOf)({ ...JSON.parse(rawJson), entity: 'dispatch' });
|
|
99
|
+
}
|
|
93
100
|
function boundRepo(version = 'v1') {
|
|
94
101
|
const repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-196-bound-'));
|
|
95
102
|
(0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'main'], { cwd: repo });
|
|
@@ -181,6 +188,169 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
181
188
|
expect(state.dispatches).toHaveLength(1);
|
|
182
189
|
expect(state.tasks[0].attempts).toBe(1);
|
|
183
190
|
});
|
|
191
|
+
test('9.12.2 unlinked S1 dispatch is preserved while a fresh routed dispatch gets its own real ACK', () => {
|
|
192
|
+
expect(legacyDispatchDigest()).toBe('01edb739d12771ff6ffea2ea4296bf477b745f76afb66b4b20cb90cd470f5666');
|
|
193
|
+
const state = v2State();
|
|
194
|
+
const oldAt = new Date(Date.parse(state.routingAttempts[0].reservedAt) - 60_000).toISOString();
|
|
195
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at: oldAt });
|
|
196
|
+
state.tasks[0].attempts = 1;
|
|
197
|
+
state.tasks[0].status = 'in-progress';
|
|
198
|
+
state.jobs['completed-test'] = { id: 'completed-test', fingerprint: 'fixture', commandDigest: 'fixture',
|
|
199
|
+
argv: ['true'], cwd: '.', paths: [], expandedPaths: [], executionState: 'exited',
|
|
200
|
+
observationState: 'progressing', phaseTimestamps: { exited: new Date().toISOString() } };
|
|
201
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
202
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
203
|
+
state.appliedRequests['old-dispatch-retry'] = { ...state.appliedRequests['old-dispatch-request'], requestId: 'old-dispatch-retry' };
|
|
204
|
+
const oldRecord = structuredClone(state.dispatches[0]);
|
|
205
|
+
const oldAck = structuredClone(state.appliedRequests['old-dispatch-request']);
|
|
206
|
+
const retryAck = structuredClone(state.appliedRequests['old-dispatch-retry']);
|
|
207
|
+
dispatch(state, { routingAttemptId: 'route-1' });
|
|
208
|
+
expect(state.dispatches[0]).toEqual(oldRecord);
|
|
209
|
+
expect(state.appliedRequests['old-dispatch-request']).toEqual(oldAck);
|
|
210
|
+
expect(state.appliedRequests['old-dispatch-retry']).toEqual(retryAck);
|
|
211
|
+
expect(state.dispatches).toHaveLength(2);
|
|
212
|
+
const linked = state.dispatches[1];
|
|
213
|
+
expect(linked).toMatchObject({ taskId: 'S1', routingAttemptId: 'route-1', dispatchRequestId: 'dispatch-request' });
|
|
214
|
+
expect(linked.id).not.toBe('dispatch-S1');
|
|
215
|
+
expect(state.appliedRequests['dispatch-request']).toMatchObject({ outcome: 'applied', resultRef: linked.id });
|
|
216
|
+
expect(state.tasks[0].attempts).toBe(1);
|
|
217
|
+
expect((0, types_1.isWellFormedState)(state)).toBe(true);
|
|
218
|
+
dispatch(state, { routingAttemptId: 'route-1' });
|
|
219
|
+
expect(state.dispatches).toHaveLength(2);
|
|
220
|
+
expect(state.tasks[0].attempts).toBe(1);
|
|
221
|
+
expect(() => (0, apply_1.applyRequestToState)(state, { kind: 'register-entity', requestId: 'unrelated-dispatch-request',
|
|
222
|
+
generationToken: 'g1', idempotencyKey: 'different-dispatch-key', payload: { entity: 'dispatch',
|
|
223
|
+
dispatchId: 'dispatch-S1', taskId: 'S1', routingAttemptId: 'route-1' } }, 'd'.repeat(64), process.cwd()))
|
|
224
|
+
.toThrow(/conflictivo/);
|
|
225
|
+
expect(state.dispatches).toHaveLength(2);
|
|
226
|
+
});
|
|
227
|
+
test('equal-millisecond legacy timestamps reconcile only when the durable dispatch ACK precedes the reservation ACK', () => {
|
|
228
|
+
const state = v2State();
|
|
229
|
+
const at = state.routingAttempts[0].reservedAt;
|
|
230
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at });
|
|
231
|
+
state.tasks[0].attempts = 1;
|
|
232
|
+
state.tasks[0].status = 'in-progress';
|
|
233
|
+
const reservationAck = state.appliedRequests['reserve-1'];
|
|
234
|
+
const historicalAck = { requestId: 'old-dispatch-request', idempotencyKey: 'old-dispatch-key',
|
|
235
|
+
payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
236
|
+
state.appliedRequests = { 'old-dispatch-request': historicalAck, 'reserve-1': reservationAck };
|
|
237
|
+
const durable = JSON.parse(JSON.stringify(state));
|
|
238
|
+
expect(Object.keys(durable.appliedRequests)).toEqual(['old-dispatch-request', 'reserve-1']);
|
|
239
|
+
dispatch(durable, { routingAttemptId: 'route-1' });
|
|
240
|
+
expect(durable.dispatches).toHaveLength(2);
|
|
241
|
+
expect(durable.dispatches[0]).toEqual({ id: 'dispatch-S1', taskId: 'S1', at });
|
|
242
|
+
expect(durable.appliedRequests['old-dispatch-request']).toEqual(historicalAck);
|
|
243
|
+
expect(durable.tasks[0].attempts).toBe(1);
|
|
244
|
+
expect((0, types_1.isWellFormedState)(durable)).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
test('equal-millisecond legacy timestamps do not infer order from a later dispatch ACK', () => {
|
|
247
|
+
const state = v2State();
|
|
248
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at: state.routingAttempts[0].reservedAt });
|
|
249
|
+
state.tasks[0].attempts = 1;
|
|
250
|
+
state.tasks[0].status = 'in-progress';
|
|
251
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
252
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(),
|
|
253
|
+
outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
254
|
+
expect(() => dispatch(state, { routingAttemptId: 'route-1' })).toThrow(/legacy handoff/);
|
|
255
|
+
expect(state.dispatches).toHaveLength(1);
|
|
256
|
+
expect(state.appliedRequests['dispatch-request']).toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
test('historical ACK matching is independent of JSON key order but not payload identity', () => {
|
|
259
|
+
for (const rawJson of [
|
|
260
|
+
'{"taskId":"S1","dispatchId":"dispatch-S1"}',
|
|
261
|
+
'{"entity":"dispatch","taskId":"S1","dispatchId":"dispatch-S1"}',
|
|
262
|
+
]) {
|
|
263
|
+
const state = v2State();
|
|
264
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1',
|
|
265
|
+
at: new Date(Date.parse(state.routingAttempts[0].reservedAt) - 60_000).toISOString() });
|
|
266
|
+
state.tasks[0].status = 'in-progress';
|
|
267
|
+
state.tasks[0].attempts = 1;
|
|
268
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
269
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(rawJson),
|
|
270
|
+
outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
271
|
+
dispatch(state, { routingAttemptId: 'route-1' });
|
|
272
|
+
expect(state.dispatches).toHaveLength(2);
|
|
273
|
+
expect(state.appliedRequests['dispatch-request']).toMatchObject({ outcome: 'applied', resultRef: state.dispatches[1].id });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
test('historical unlinked S1 cannot be bypassed with a different requested dispatch ID', () => {
|
|
277
|
+
const state = v2State();
|
|
278
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1',
|
|
279
|
+
at: new Date(Date.parse(state.routingAttempts[0].reservedAt) - 60_000).toISOString() });
|
|
280
|
+
state.tasks[0].status = 'in-progress';
|
|
281
|
+
state.tasks[0].attempts = 1;
|
|
282
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
283
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
284
|
+
expect(() => dispatch(state, { dispatchId: 'new-S1', routingAttemptId: 'route-1' })).toThrow(/legacy|historical|conflict/i);
|
|
285
|
+
expect(state.dispatches).toHaveLength(1);
|
|
286
|
+
expect(state.tasks[0].attempts).toBe(1);
|
|
287
|
+
expect(state.appliedRequests['dispatch-request']).toBeUndefined();
|
|
288
|
+
});
|
|
289
|
+
test('historical handoff requires a matching applied legacy dispatch ACK', () => {
|
|
290
|
+
for (const ack of ['missing', 'unrelated', 'rejected']) {
|
|
291
|
+
const state = v2State();
|
|
292
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1',
|
|
293
|
+
at: new Date(Date.parse(state.routingAttempts[0].reservedAt) - 60_000).toISOString() });
|
|
294
|
+
state.tasks[0].status = 'in-progress';
|
|
295
|
+
state.tasks[0].attempts = 1;
|
|
296
|
+
if (ack !== 'missing')
|
|
297
|
+
state.appliedRequests['old-dispatch-request'] = {
|
|
298
|
+
requestId: 'old-dispatch-request', idempotencyKey: 'old-dispatch-key',
|
|
299
|
+
payloadDigest: ack === 'unrelated' ? (0, requests_1.digestOf)({ entity: 'task', taskId: 'S1' }) : legacyDispatchDigest(),
|
|
300
|
+
outcome: ack === 'rejected' ? 'rejected-stale-generation' : 'applied', resultRef: 'dispatch-S1',
|
|
301
|
+
};
|
|
302
|
+
expect(() => dispatch(state, { routingAttemptId: 'route-1' })).toThrow(/legacy|historical|ACK|evidencia/i);
|
|
303
|
+
expect(state.dispatches).toHaveLength(1);
|
|
304
|
+
expect(state.appliedRequests['dispatch-request']).toBeUndefined();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
test('a failed linked handoff does not authorize another child without native ownership proof', () => {
|
|
308
|
+
const state = v2State();
|
|
309
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1',
|
|
310
|
+
at: new Date(Date.parse(state.routingAttempts[0].reservedAt) - 60_000).toISOString() });
|
|
311
|
+
state.tasks[0].status = 'in-progress';
|
|
312
|
+
state.tasks[0].attempts = 1;
|
|
313
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
314
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
315
|
+
dispatch(state, { routingAttemptId: 'route-1' });
|
|
316
|
+
state.routingAttempts[0].state = 'blocked';
|
|
317
|
+
state.routingAttempts[0].verdict = 'fail';
|
|
318
|
+
state.routingAttempts.push({ ...state.routingAttempts[0], id: 'route-2', state: 'reserved',
|
|
319
|
+
verdict: undefined, reservationRequestId: 'reserve-2' });
|
|
320
|
+
state.appliedRequests['reserve-2'] = { requestId: 'reserve-2', idempotencyKey: 'reserve-2',
|
|
321
|
+
payloadDigest: 'd'.repeat(64), outcome: 'applied', resultRef: 'route-2' };
|
|
322
|
+
expect(() => (0, apply_1.applyRequestToState)(state, { kind: 'register-entity', requestId: 'dispatch-request-2',
|
|
323
|
+
generationToken: 'g1', idempotencyKey: 'dispatch-key-2', payload: { entity: 'dispatch',
|
|
324
|
+
dispatchId: 'dispatch-S1-retry', taskId: 'S1', routingAttemptId: 'route-2' } }, 'd'.repeat(64), process.cwd()))
|
|
325
|
+
.toThrow(/historical|conflict/i);
|
|
326
|
+
expect(state.dispatches).toHaveLength(2);
|
|
327
|
+
expect(state.tasks[0].attempts).toBe(1);
|
|
328
|
+
expect(state.appliedRequests['dispatch-request-2']).toBeUndefined();
|
|
329
|
+
expect((0, types_1.isWellFormedState)(state)).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
test('legacy dispatch reconciliation refuses ambiguous or already-observed work', () => {
|
|
332
|
+
for (const scenario of ['two-old-dispatches', 'observed', 'wrong-order', 'existing-job']) {
|
|
333
|
+
const state = v2State();
|
|
334
|
+
state.dispatches.push({ id: 'dispatch-S1', taskId: 'S1',
|
|
335
|
+
at: new Date(Date.parse(state.routingAttempts[0].reservedAt) - 60_000).toISOString() });
|
|
336
|
+
state.tasks[0].attempts = 1;
|
|
337
|
+
state.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
338
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
339
|
+
if (scenario === 'two-old-dispatches')
|
|
340
|
+
state.dispatches.push({ id: 'other-old', taskId: 'S1', at: state.dispatches[0].at });
|
|
341
|
+
if (scenario === 'observed')
|
|
342
|
+
state.routingAttempts[0].nativeAgentId = 'native-child';
|
|
343
|
+
if (scenario === 'wrong-order')
|
|
344
|
+
state.dispatches[0].at = new Date(Date.parse(state.routingAttempts[0].reservedAt) + 60_000).toISOString();
|
|
345
|
+
if (scenario === 'existing-job')
|
|
346
|
+
state.jobs['unreconciled'] = { id: 'unreconciled',
|
|
347
|
+
fingerprint: 'fixture', commandDigest: 'fixture', argv: ['true'], cwd: '.', paths: [], expandedPaths: [],
|
|
348
|
+
executionState: 'received', observationState: 'progressing', phaseTimestamps: {} };
|
|
349
|
+
expect(() => dispatch(state, { routingAttemptId: 'route-1' })).toThrow();
|
|
350
|
+
expect(state.dispatches.some(item => item.routingAttemptId === 'route-1')).toBe(false);
|
|
351
|
+
expect(state.appliedRequests['dispatch-request']).toBeUndefined();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
184
354
|
test('an exact dispatch replay after native observation does not duplicate S1', () => {
|
|
185
355
|
const state = v2State();
|
|
186
356
|
dispatch(state, { routingAttemptId: 'route-1' });
|
|
@@ -336,6 +506,118 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
336
506
|
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
337
507
|
}
|
|
338
508
|
});
|
|
509
|
+
test('controller admission reuses only an owned generation sensor pass, never a new generation', async () => {
|
|
510
|
+
const repo = boundRepo();
|
|
511
|
+
try {
|
|
512
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
513
|
+
(0, generations_1.launchControllerGeneration)(repo, 'main', 'codex', { schema: 'controller-recovery/v1', kind: 'resume-cycle' }, () => { });
|
|
514
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
515
|
+
const generation = state.generations[0];
|
|
516
|
+
const claim = (0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'main'), generation.controllerJobId, generation.spawnNonce);
|
|
517
|
+
fs_1.default.writeFileSync(claim, '{}');
|
|
518
|
+
generation.wrapperRef = (0, process_1.captureSelfRef)(generation.spawnNonce);
|
|
519
|
+
state.admissionContext = { provider: 'codex', controllerAutonomy: 'approval-free' };
|
|
520
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
521
|
+
const sensorRun = jest.fn(async () => ({ sensors: [], overall: 'not_certified',
|
|
522
|
+
projectRoot: repo, manifestPath: path_1.default.join(repo, '.awm', 'sensors.json'), mode: 'project-sensors' }));
|
|
523
|
+
const admit = async (freshSensorsRequired = false) => (0, registry_contracts_1.admitRegistryPlan)({
|
|
524
|
+
plan: (0, validate_1.validatePlanFile)('plans/cycle.md', repo), provider: 'codex', cwd: repo,
|
|
525
|
+
executionMode: 'desatendido', planPath: 'plans/cycle.md', journalState: (0, store_1.readJournal)(repo, 'main').state,
|
|
526
|
+
verifySensors: true, controllerAutonomy: 'approval-free',
|
|
527
|
+
...(freshSensorsRequired ? { freshSensorsRequired: true } : {}),
|
|
528
|
+
}, { runSensors: sensorRun });
|
|
529
|
+
const active = await admit();
|
|
530
|
+
expect(active.state).toBe('admitted');
|
|
531
|
+
expect(active).toHaveProperty('sensorEvidence', 'generation-bound');
|
|
532
|
+
expect(sensorRun).not.toHaveBeenCalled();
|
|
533
|
+
const replacement = await admit(true);
|
|
534
|
+
expect(replacement).toMatchObject({ state: 'blocked', sensors: 'not-certified' });
|
|
535
|
+
expect(sensorRun).toHaveBeenCalledTimes(1);
|
|
536
|
+
const claimOnly = (0, store_1.readJournal)(repo, 'main').state;
|
|
537
|
+
claimOnly.generations[0].wrapperRef = undefined;
|
|
538
|
+
(0, store_1.writeJournal)(repo, 'main', claimOnly);
|
|
539
|
+
expect(await admit()).toMatchObject({ state: 'blocked', sensors: 'not-certified' });
|
|
540
|
+
expect(sensorRun).toHaveBeenCalledTimes(2);
|
|
541
|
+
const mismatched = (0, store_1.readJournal)(repo, 'main').state;
|
|
542
|
+
mismatched.generations[0].processRef = (0, process_1.captureSelfRef)('different-nonce');
|
|
543
|
+
(0, store_1.writeJournal)(repo, 'main', mismatched);
|
|
544
|
+
expect(await admit()).toMatchObject({ state: 'blocked', sensors: 'not-certified' });
|
|
545
|
+
expect(sensorRun).toHaveBeenCalledTimes(3);
|
|
546
|
+
fs_1.default.unlinkSync(claim);
|
|
547
|
+
const unowned = await admit();
|
|
548
|
+
expect(unowned).toMatchObject({ state: 'blocked', sensors: 'not-certified' });
|
|
549
|
+
expect(sensorRun).toHaveBeenCalledTimes(4);
|
|
550
|
+
const stale = (0, store_1.readJournal)(repo, 'main').state;
|
|
551
|
+
stale.generations[0].processRef = { pid: 99999997, processGroup: 99999997,
|
|
552
|
+
startTime: 'Thu Sep 24 20:00:00 2026', spawnNonce: generation.spawnNonce,
|
|
553
|
+
argvDigest: generation.launchArgvDigest, psArgsDigest: 'b'.repeat(64) };
|
|
554
|
+
(0, store_1.writeJournal)(repo, 'main', stale);
|
|
555
|
+
expect(await admit()).toMatchObject({ state: 'blocked', sensors: 'not-certified' });
|
|
556
|
+
expect(sensorRun).toHaveBeenCalledTimes(5);
|
|
557
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
558
|
+
const next = await admit();
|
|
559
|
+
expect(next).toMatchObject({ state: 'blocked', sensors: 'not-certified' });
|
|
560
|
+
expect(sensorRun).toHaveBeenCalledTimes(6);
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
test('generation-bound sensor reuse rejects a changed live v2 runtime', async () => {
|
|
567
|
+
const repo = boundRepo('v2');
|
|
568
|
+
try {
|
|
569
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
570
|
+
(0, generations_1.launchControllerGeneration)(repo, 'main', 'codex', { schema: 'controller-recovery/v1', kind: 'resume-cycle' }, () => { });
|
|
571
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
572
|
+
const generation = state.generations[0];
|
|
573
|
+
fs_1.default.writeFileSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'main'), generation.controllerJobId, generation.spawnNonce), '{}');
|
|
574
|
+
generation.wrapperRef = (0, process_1.captureSelfRef)(generation.spawnNonce);
|
|
575
|
+
state.admissionContext = { provider: 'codex', controllerAutonomy: 'approval-free',
|
|
576
|
+
runtime: { kind: 'native', version: '0.156.0', accountScopeDigest: 'a'.repeat(64) } };
|
|
577
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
578
|
+
const sensorRun = jest.fn(async () => ({ sensors: [], overall: 'not_certified' }));
|
|
579
|
+
const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: (0, validate_1.validatePlanFile)('plans/cycle.md', repo),
|
|
580
|
+
provider: 'codex', cwd: repo, executionMode: 'desatendido', planPath: 'plans/cycle.md',
|
|
581
|
+
journalState: (0, store_1.readJournal)(repo, 'main').state, verifySensors: true, controllerAutonomy: 'approval-free',
|
|
582
|
+
}, { runSensors: sensorRun, readRouting: async () => ({ runtime: { target: 'codex', kind: 'native',
|
|
583
|
+
version: '0.157.0', accountScopeDigest: 'a'.repeat(64) } }) });
|
|
584
|
+
expect(report.state).toBe('blocked');
|
|
585
|
+
expect(report.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ code: 'ADMISSION_GENERATION_RUNTIME_MISMATCH' })]));
|
|
586
|
+
expect(sensorRun).not.toHaveBeenCalled();
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
test('active v2 controller admission reuses owned sensors while still checking routing evidence', async () => {
|
|
593
|
+
const repo = boundRepo('v2');
|
|
594
|
+
try {
|
|
595
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
596
|
+
(0, generations_1.launchControllerGeneration)(repo, 'main', 'codex', { schema: 'controller-recovery/v1', kind: 'resume-cycle' }, () => { });
|
|
597
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
598
|
+
const generation = state.generations[0];
|
|
599
|
+
fs_1.default.writeFileSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'main'), generation.controllerJobId, generation.spawnNonce), '{}');
|
|
600
|
+
generation.wrapperRef = (0, process_1.captureSelfRef)(generation.spawnNonce);
|
|
601
|
+
state.admissionContext = { provider: 'codex', controllerAutonomy: 'approval-free',
|
|
602
|
+
runtime: { kind: routing_approval_1.routingEnvelope.runtime.kind, version: routing_approval_1.routingEnvelope.runtime.version,
|
|
603
|
+
accountScopeDigest: routing_approval_1.routingEnvelope.runtime.accountScopeDigest } };
|
|
604
|
+
(0, store_1.writeJournal)(repo, 'main', state);
|
|
605
|
+
const evidence = (0, routing_approval_2.routingApproval)();
|
|
606
|
+
const sensorRun = jest.fn(async () => ({ sensors: [], overall: 'not_certified' }));
|
|
607
|
+
const readRouting = jest.fn(async () => ({ runtime: routing_approval_1.routingEnvelope.runtime,
|
|
608
|
+
policy: evidence.policy, capabilities: evidence.capabilities, now: evidence.checkedAt }));
|
|
609
|
+
const report = await (0, registry_contracts_1.admitRegistryPlan)({ plan: (0, validate_1.validatePlanFile)('plans/cycle.md', repo),
|
|
610
|
+
provider: 'codex', cwd: repo, executionMode: 'desatendido', planPath: 'plans/cycle.md',
|
|
611
|
+
journalState: (0, store_1.readJournal)(repo, 'main').state, verifySensors: true, controllerAutonomy: 'approval-free',
|
|
612
|
+
}, { runSensors: sensorRun, readRouting });
|
|
613
|
+
expect(report).toMatchObject({ state: 'admitted', sensors: 'pass', sensorEvidence: 'generation-bound' });
|
|
614
|
+
expect(sensorRun).not.toHaveBeenCalled();
|
|
615
|
+
expect(readRouting).toHaveBeenCalledTimes(1);
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
619
|
+
}
|
|
620
|
+
});
|
|
339
621
|
test('the same RED continuation applies to compact v2 with unchanged native scope', async () => {
|
|
340
622
|
const repo = boundRepo('v2');
|
|
341
623
|
try {
|
|
@@ -381,6 +663,33 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
381
663
|
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
382
664
|
}
|
|
383
665
|
});
|
|
666
|
+
test.each(['unverified', 'throws'])('an active v2 generation records an inconclusive local runtime query (%s) without claiming drift', async (scenario) => {
|
|
667
|
+
const repo = boundRepo('v2');
|
|
668
|
+
try {
|
|
669
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
670
|
+
(0, generations_1.launchControllerGeneration)(repo, 'main', 'codex', { schema: 'controller-recovery/v1', kind: 'resume-cycle' }, () => { });
|
|
671
|
+
const gen = (0, store_1.readJournal)(repo, 'main').state.generations[0];
|
|
672
|
+
fs_1.default.writeFileSync((0, exec_wrapper_1.claimPath)((0, paths_1.logsDir)(repo, 'main'), gen.controllerJobId, gen.spawnNonce), '{}');
|
|
673
|
+
const query = jest.spyOn(localScope, 'queryLocalEventScope');
|
|
674
|
+
if (scenario === 'throws')
|
|
675
|
+
query.mockRejectedValue(new Error('permission denied while reading a private path'));
|
|
676
|
+
else
|
|
677
|
+
query.mockResolvedValue({ state: 'unverified', reason: 'NATIVE_QUERY_FAILED' });
|
|
678
|
+
const expected = routing_approval_1.routingEnvelope.runtime;
|
|
679
|
+
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, routingIdentity: {
|
|
680
|
+
kind: expected.kind, version: expected.version, accountScopeDigest: expected.accountScopeDigest,
|
|
681
|
+
}, controllerAutonomy: 'approval-free' };
|
|
682
|
+
let spawns = 0;
|
|
683
|
+
expect(await new supervisor_1.Supervisor(repo, 'main', cfg, () => { spawns++; }, undefined, async () => admitted, true).tick()).toBe('custody');
|
|
684
|
+
const final = (0, store_1.readJournal)(repo, 'main').state;
|
|
685
|
+
expect(final.cycle.blockedReason).toBe('runtime local no verificable: NATIVE_QUERY_FAILED');
|
|
686
|
+
expect(spawns).toBe(0);
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
jest.restoreAllMocks();
|
|
690
|
+
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
384
693
|
test('an active v2 generation refuses dispatch when local runtime identity drifts', async () => {
|
|
385
694
|
const repo = boundRepo('v2');
|
|
386
695
|
try {
|
|
@@ -433,15 +742,14 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
433
742
|
(0, request_1.requestJob)(repo, 'main', state.generations[0].token, [process.execPath, '-e', 'process.exit(0)'], [], '.');
|
|
434
743
|
const adapter = adapterModule.adapterFor('codex');
|
|
435
744
|
jest.spyOn(adapterModule, 'adapterFor').mockReturnValue({ ...adapter, activity: () => null, safeToReplace: () => 'safe' });
|
|
436
|
-
jest.spyOn(generationModule, 'resolveGeneration').
|
|
437
|
-
const current = (0, store_1.readJournal)(repo, 'main').state;
|
|
438
|
-
current.generations[0].state = 'terminated';
|
|
439
|
-
(0, store_1.writeJournal)(repo, 'main', current);
|
|
440
|
-
return 'proven-dead';
|
|
441
|
-
});
|
|
745
|
+
jest.spyOn(generationModule, 'resolveGeneration').mockResolvedValue('proven-dead');
|
|
442
746
|
let admissions = 0;
|
|
443
747
|
let jobSpawns = 0;
|
|
444
|
-
const sup = new supervisor_1.Supervisor(repo, 'main', { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, heartbeatTimeoutMs: 0, activityWindowMs: 0 }, () => { jobSpawns += 1; }, undefined, async () => {
|
|
748
|
+
const sup = new supervisor_1.Supervisor(repo, 'main', { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, heartbeatTimeoutMs: 0, activityWindowMs: 0 }, () => { jobSpawns += 1; }, undefined, async (request) => {
|
|
749
|
+
admissions += 1;
|
|
750
|
+
expect(request).toMatchObject({ freshSensorsRequired: true });
|
|
751
|
+
return { ...admitted, sensorEvidence: 'generation-bound' };
|
|
752
|
+
}, true);
|
|
445
753
|
expect(await sup.tick()).toBe('custody');
|
|
446
754
|
expect((0, store_1.readJournal)(repo, 'main').state.generations).toHaveLength(1);
|
|
447
755
|
expect(admissions).toBe(1);
|
|
@@ -625,7 +933,7 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
625
933
|
fs_1.default.rmSync(repo, { recursive: true, force: true });
|
|
626
934
|
}
|
|
627
935
|
}, 10000);
|
|
628
|
-
test('recovered v2 S1 uses real CLI reservation, dispatch ack, observation and one continuation', async () => {
|
|
936
|
+
test('recovered historical v2 S1 uses real CLI reservation, new dispatch ack, observation and one continuation', async () => {
|
|
629
937
|
const repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-196-integral-'));
|
|
630
938
|
const previousHome = process.env.AWM_HOME;
|
|
631
939
|
const operator = path_1.default.join(repo, 'operator');
|
|
@@ -695,6 +1003,16 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
695
1003
|
(0, apply_1.consumePendingRequests)(repo, 'main', generation.token, scope);
|
|
696
1004
|
expect((0, store_1.readJournal)(repo, 'main').state.requestProblems).toEqual([]);
|
|
697
1005
|
expect(cli('ack', task.requestId).state).toBe('applied');
|
|
1006
|
+
// Durable 9.12.2 intent and ACK precede the route reservation.
|
|
1007
|
+
// Equal timestamp granularity alone must not imply a child.
|
|
1008
|
+
const historical = (0, store_1.readJournal)(repo, 'main').state;
|
|
1009
|
+
let historicalAt = new Date(Date.now() - 60_000).toISOString();
|
|
1010
|
+
historical.dispatches.push({ id: 'dispatch-S1', taskId: 'S1', at: historicalAt });
|
|
1011
|
+
historical.tasks[0].attempts = 1;
|
|
1012
|
+
historical.tasks[0].status = 'in-progress';
|
|
1013
|
+
historical.appliedRequests['old-dispatch-request'] = { requestId: 'old-dispatch-request',
|
|
1014
|
+
idempotencyKey: 'old-dispatch-key', payloadDigest: legacyDispatchDigest(), outcome: 'applied', resultRef: 'dispatch-S1' };
|
|
1015
|
+
(0, store_1.writeJournal)(repo, 'main', historical);
|
|
698
1016
|
const reserve = cli('routing-reserve', '--generation', generation.token, '--obligation', 'S1-implementer', '--lineage', 'lineage-S1', '--envelope-file', envelopeFile, '--fingerprint', 'f'.repeat(64));
|
|
699
1017
|
expect(cli('ack', reserve.requestId).state).toBe('pending');
|
|
700
1018
|
(0, apply_1.consumePendingRequests)(repo, 'main', generation.token, scope);
|
|
@@ -702,12 +1020,22 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
702
1020
|
const reservation = cli('ack', reserve.requestId);
|
|
703
1021
|
expect(reservation.state).toBe('applied');
|
|
704
1022
|
const attemptId = reservation.resultRef;
|
|
1023
|
+
// Reproduce the real journal's millisecond tie without changing
|
|
1024
|
+
// the durable ACK order or adding any native evidence.
|
|
1025
|
+
const tied = (0, store_1.readJournal)(repo, 'main').state;
|
|
1026
|
+
historicalAt = tied.routingAttempts.find(attempt => attempt.id === attemptId).reservedAt;
|
|
1027
|
+
tied.dispatches[0].at = historicalAt;
|
|
1028
|
+
expect(Object.keys(tied.appliedRequests).indexOf('old-dispatch-request'))
|
|
1029
|
+
.toBeLessThan(Object.keys(tied.appliedRequests).indexOf(reserve.requestId));
|
|
1030
|
+
(0, store_1.writeJournal)(repo, 'main', tied);
|
|
705
1031
|
const dispatchPayload = JSON.stringify({ dispatchId: 'dispatch-S1', taskId: 'S1', routingAttemptId: attemptId });
|
|
706
1032
|
const queuedDispatch = cli('register', '--generation', generation.token, '--entity', 'dispatch', '--json', dispatchPayload);
|
|
707
1033
|
expect(cli('ack', queuedDispatch.requestId).state).toBe('pending');
|
|
708
1034
|
expect(fs_1.default.existsSync(path_1.default.join(repo, 's1-red.test.ts'))).toBe(false);
|
|
709
1035
|
(0, apply_1.consumePendingRequests)(repo, 'main', generation.token, scope);
|
|
710
|
-
|
|
1036
|
+
const routedAck = cli('ack', queuedDispatch.requestId);
|
|
1037
|
+
expect(routedAck.state).toBe('applied');
|
|
1038
|
+
expect(routedAck.resultRef).not.toBe('dispatch-S1');
|
|
711
1039
|
const worker = (0, child_process_1.spawnSync)(process.execPath, ['-e', `require('fs').writeFileSync(${JSON.stringify(path_1.default.join(repo, 's1-red.test.ts'))}, 'test("RED", () => { throw new Error("RED"); });\\n')`], {
|
|
712
1040
|
cwd: repo, encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' },
|
|
713
1041
|
});
|
|
@@ -722,7 +1050,7 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
722
1050
|
expect(cli('ack', observation.requestId).state).toBe('applied');
|
|
723
1051
|
const replay = cli('register', '--generation', generation.token, '--entity', 'dispatch', '--json', dispatchPayload);
|
|
724
1052
|
(0, apply_1.consumePendingRequests)(repo, 'main', generation.token, scope);
|
|
725
|
-
expect(cli('ack', replay.requestId)
|
|
1053
|
+
expect(cli('ack', replay.requestId)).toMatchObject({ state: 'applied', resultRef: routedAck.resultRef });
|
|
726
1054
|
expect(cli('reconcile').nextAction).toBeTruthy();
|
|
727
1055
|
const unsupported = (0, child_process_1.spawnSync)(process.execPath, [compiled, 'job', 'reconcile', '--generation', generation.token], {
|
|
728
1056
|
cwd: repo, encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' },
|
|
@@ -731,7 +1059,11 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
731
1059
|
expect(unsupported.stderr).toMatch(/generation|unknown option/i);
|
|
732
1060
|
const final = (0, store_1.readJournal)(repo, 'main').state;
|
|
733
1061
|
expect(final.tasks).toHaveLength(1);
|
|
734
|
-
expect(final.dispatches).toHaveLength(
|
|
1062
|
+
expect(final.dispatches).toHaveLength(2);
|
|
1063
|
+
expect(final.dispatches[0]).toEqual({ id: 'dispatch-S1', taskId: 'S1', at: historicalAt });
|
|
1064
|
+
expect(final.appliedRequests['old-dispatch-request']).toEqual(historical.appliedRequests['old-dispatch-request']);
|
|
1065
|
+
expect(final.dispatches[1]).toMatchObject({ id: routedAck.resultRef,
|
|
1066
|
+
taskId: 'S1', routingAttemptId: attemptId, dispatchRequestId: queuedDispatch.requestId });
|
|
735
1067
|
expect(final.routingAttempts).toHaveLength(1);
|
|
736
1068
|
expect(final.routingAttempts[0].state).toBe('active');
|
|
737
1069
|
expect(final.tasks[0].attempts).toBe(1);
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|