agentic-workflow-manager 9.12.5 → 9.13.1
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/plan/index.js +6 -1
- package/dist/src/core/journal/process.js +54 -2
- package/dist/src/core/model-policy/resolve.js +2 -0
- package/dist/src/core/plan/validate.js +7 -0
- package/dist/tests/commands/plan/index.test.js +6 -0
- package/dist/tests/commands/watch/plan-lifecycle.test.js +1 -1
- package/dist/tests/commands/watch/reopened-196.test.js +1 -1
- package/dist/tests/commands/watch/supervisor-loop.test.js +42 -1
- package/dist/tests/core/journal/process.test.js +64 -0
- package/dist/tests/core/plan/validate.test.js +37 -0
- 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
|
@@ -106,6 +106,7 @@ function reportPayload(report, planPath) {
|
|
|
106
106
|
case 'valid':
|
|
107
107
|
return {
|
|
108
108
|
state: report.state, path: planPath, schema: report.schema, planId: report.manifest.planId,
|
|
109
|
+
...(report.dispatchMode ? { dispatchMode: report.dispatchMode } : {}),
|
|
109
110
|
planDigest: report.planDigest,
|
|
110
111
|
requirements: report.manifest.requirements.length, sources: report.manifest.sources.length,
|
|
111
112
|
commands: report.manifest.commands.length, slices: report.manifest.slices.length, completeOwnership: true,
|
|
@@ -259,11 +260,15 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
|
|
|
259
260
|
.description('validate a compact plan without modifying it')
|
|
260
261
|
.option('--json', 'emit one stable JSON report')
|
|
261
262
|
.option('--cwd <path>', 'repository root for plan containment and source resolution')
|
|
263
|
+
.option('--require-dispatch-mode', 'require an explicit dispatch choice for a newly authored plan')
|
|
262
264
|
.action((planPath, options) => {
|
|
263
265
|
assertText(planPath, 'plan path');
|
|
264
266
|
const cwd = options.cwd ?? process.cwd();
|
|
265
267
|
assertText(cwd, '--cwd');
|
|
266
|
-
const
|
|
268
|
+
const validated = deps.validatePlanFile(planPath, cwd);
|
|
269
|
+
const report = options.requireDispatchMode && validated.state === 'valid' && !validated.dispatchMode
|
|
270
|
+
? { state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE', message: 'new plans must declare Modo de despacho before the manifest' }] }
|
|
271
|
+
: validated;
|
|
267
272
|
const output = options.json === true
|
|
268
273
|
? `${JSON.stringify(reportPayload(report, planPath))}\n`
|
|
269
274
|
: formatReport(report, planPath);
|
|
@@ -12,6 +12,7 @@ exports.spawnStructured = spawnStructured;
|
|
|
12
12
|
exports.refIsAlive = refIsAlive;
|
|
13
13
|
exports.processStatesAreGone = processStatesAreGone;
|
|
14
14
|
exports.groupIsGone = groupIsGone;
|
|
15
|
+
exports.descendantActivity = descendantActivity;
|
|
15
16
|
exports.activitySnapshot = activitySnapshot;
|
|
16
17
|
exports.terminateGroupConfirmed = terminateGroupConfirmed;
|
|
17
18
|
exports.terminatePreviouslyOwnedGroup = terminatePreviouslyOwnedGroup;
|
|
@@ -455,6 +456,57 @@ function groupIsGone(pgid) {
|
|
|
455
456
|
return false; // sin observacion completa, falla cerrado
|
|
456
457
|
}
|
|
457
458
|
}
|
|
459
|
+
const MAX_PS_ROWS = 65536;
|
|
460
|
+
/** Activity of every process descending from `rootPid` in a `ps -A -o
|
|
461
|
+
* pid=,ppid=,time=` table. The process group alone misses work a controller
|
|
462
|
+
* waits on when the provider runs it in its own group or session (sandboxed
|
|
463
|
+
* tool commands), which turned a healthy wait on a native subagent into a
|
|
464
|
+
* "frozen" controller and a false custody (#196, v1 smoke). Root excluded;
|
|
465
|
+
* malformed rows ignored; a ppid cycle cannot loop because each pid is
|
|
466
|
+
* visited once. */
|
|
467
|
+
function descendantActivity(psTable, rootPid) {
|
|
468
|
+
if (!Number.isInteger(rootPid) || rootPid <= 0)
|
|
469
|
+
throw new Error(`descendantActivity: root pid must be a positive integer, got ${rootPid}`);
|
|
470
|
+
const children = new Map();
|
|
471
|
+
let rows = 0;
|
|
472
|
+
for (const line of psTable.split('\n')) {
|
|
473
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
|
|
474
|
+
if (match === null)
|
|
475
|
+
continue;
|
|
476
|
+
if (++rows > MAX_PS_ROWS)
|
|
477
|
+
break;
|
|
478
|
+
const pid = Number(match[1]);
|
|
479
|
+
const ppid = Number(match[2]);
|
|
480
|
+
const siblings = children.get(ppid) ?? [];
|
|
481
|
+
siblings.push({ pid, time: match[3] });
|
|
482
|
+
children.set(ppid, siblings);
|
|
483
|
+
}
|
|
484
|
+
const seen = new Set([rootPid]);
|
|
485
|
+
const found = [];
|
|
486
|
+
const queue = [rootPid];
|
|
487
|
+
while (queue.length > 0) {
|
|
488
|
+
for (const child of children.get(queue.shift()) ?? []) {
|
|
489
|
+
if (seen.has(child.pid))
|
|
490
|
+
continue;
|
|
491
|
+
seen.add(child.pid);
|
|
492
|
+
found.push(`${child.pid}:${child.time}`);
|
|
493
|
+
queue.push(child.pid);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (found.length === 0)
|
|
497
|
+
return '0:none';
|
|
498
|
+
found.sort();
|
|
499
|
+
return `${found.length}:${crypto_1.default.createHash('sha256').update(found.join('\n')).digest('hex')}`;
|
|
500
|
+
}
|
|
501
|
+
function observeDescendants(rootPid) {
|
|
502
|
+
try {
|
|
503
|
+
const table = (0, child_process_1.execFileSync)('ps', ['-A', '-o', 'pid=,ppid=,time='], { encoding: 'utf8', stdio: exports.EXEC_STDIO, timeout: PS_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 });
|
|
504
|
+
return descendantActivity(table, rootPid);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return 'unknown';
|
|
508
|
+
}
|
|
509
|
+
}
|
|
458
510
|
/** Llamada UNA VEZ POR TICK por el supervisor (ver superviseController en
|
|
459
511
|
* commands/watch/supervisor.ts) mientras un controlador este activo — el
|
|
460
512
|
* path mas caliente de todo el modulo. En win32, `ps`/`pgrep`, cuando
|
|
@@ -474,7 +526,7 @@ function activitySnapshot(ref) {
|
|
|
474
526
|
if (!refIsAlive(ref))
|
|
475
527
|
return null;
|
|
476
528
|
if ((0, paths_1.isWindowsNative)())
|
|
477
|
-
return { cpuTime: 'unknown', groupSize: 1 };
|
|
529
|
+
return { cpuTime: 'unknown', groupSize: 1, descendants: 'unknown' };
|
|
478
530
|
let cpu = '0';
|
|
479
531
|
try {
|
|
480
532
|
cpu = psField(ref.pid, 'time') ?? '0';
|
|
@@ -490,7 +542,7 @@ function activitySnapshot(ref) {
|
|
|
490
542
|
catch {
|
|
491
543
|
groupSize = 1;
|
|
492
544
|
}
|
|
493
|
-
return { cpuTime: cpu, groupSize };
|
|
545
|
+
return { cpuTime: cpu, groupSize, descendants: observeDescendants(ref.pid) };
|
|
494
546
|
}
|
|
495
547
|
/** win32: sin process groups POSIX ni convencion de pid negativo, y sin
|
|
496
548
|
* distincion real SIGTERM/SIGKILL (Node mapea ambas a TerminateProcess
|
|
@@ -152,6 +152,8 @@ function resolveV1Dispatch(input) {
|
|
|
152
152
|
if (!input.plan || input.plan.schema !== 'compact-slices/v1')
|
|
153
153
|
throw new Error('resolveV1Dispatch requires an authenticated v1 plan report');
|
|
154
154
|
(0, validate_2.assertVerifiedValidPlanReport)(input.plan);
|
|
155
|
+
if (input.plan.dispatchMode === 'proveedor-nativo' && input.optInV1)
|
|
156
|
+
return blocked('ROUTING_DISPATCH_MODE', 'Provider-native plan forbids v1 routing opt-in; create a separately approved routed plan.');
|
|
155
157
|
if (!input.optInV1)
|
|
156
158
|
return { state: 'not-required', reason: 'v1-without-opt-in' };
|
|
157
159
|
return resolveConfiguredSelection({ ...input, requestedProfile: 'full' });
|
|
@@ -395,6 +395,12 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
395
395
|
return diagnostic('PLAN_LIMIT', 'plan id exceeds maximum string size');
|
|
396
396
|
if ((raw.schema !== 'compact-slices/v1' && raw.schema !== 'compact-slices/v2') || (raw.schema === 'compact-slices/v1' && start !== START) || (raw.schema === 'compact-slices/v2' && start !== START_V2) || typeof raw.planId !== 'string' || !PLAN_ID.test(raw.planId) || !allStrings(raw.requirements) || new Set(raw.requirements).size !== raw.requirements.length)
|
|
397
397
|
return diagnostic('PLAN_SHAPE', 'manifest scalar fields are invalid');
|
|
398
|
+
const dispatchHeaders = Array.from(text.slice(0, text.indexOf(start)).matchAll(/^[ \t]*\*\*Modo de despacho:\*\*[ \t]*([^\r\n]*)$/gm), match => match[1].trim());
|
|
399
|
+
if (dispatchHeaders.length > 1 || dispatchHeaders.some(value => value !== 'proveedor-nativo' && value !== 'awm-routed'))
|
|
400
|
+
return diagnostic('PLAN_DISPATCH_MODE', 'dispatch mode must occur once and be proveedor-nativo or awm-routed');
|
|
401
|
+
const dispatchMode = dispatchHeaders[0];
|
|
402
|
+
if (dispatchMode && ((dispatchMode === 'proveedor-nativo') !== (raw.schema === 'compact-slices/v1')))
|
|
403
|
+
return diagnostic('PLAN_DISPATCH_MODE', 'dispatch mode does not match compact plan schema');
|
|
398
404
|
if (raw.requirements.some((id) => !validRequirementId(id)) || !allStrings(raw.closureCommands) || new Set(raw.closureCommands).size !== raw.closureCommands.length)
|
|
399
405
|
return diagnostic('PLAN_SHAPE', 'manifest arrays or identifiers are invalid');
|
|
400
406
|
const sourceIds = new Set();
|
|
@@ -488,6 +494,7 @@ function validatePlan(planPath, cwd, snapshot) {
|
|
|
488
494
|
planDigest: crypto_1.default.createHash('sha256').update(text, 'utf8').digest('hex'),
|
|
489
495
|
executionDigest: (0, identity_1.executionPlanDigest)(text),
|
|
490
496
|
manifest: raw, executionMode: executionModeFromValidatedText(text),
|
|
497
|
+
...(dispatchMode ? { dispatchMode } : {}),
|
|
491
498
|
};
|
|
492
499
|
freezeJson(report);
|
|
493
500
|
verifiedValidReports.add(report);
|
|
@@ -57,6 +57,12 @@ describe('plan validate Commander wiring', () => {
|
|
|
57
57
|
stdoutWrite.mockRestore();
|
|
58
58
|
process.exitCode = previousExitCode;
|
|
59
59
|
});
|
|
60
|
+
it('requires an explicit dispatch header for strict new-plan validation', async () => {
|
|
61
|
+
const program = commandFor(valid);
|
|
62
|
+
await program.parseAsync(['node', 'awm', 'plan', 'validate', 'plans/r4.md', '--cwd', repositoryRoot, '--require-dispatch-mode', '--json']);
|
|
63
|
+
expect(JSON.parse(String(stdoutWrite.mock.calls.at(-1)[0]))).toMatchObject({ state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE' }] });
|
|
64
|
+
expect(process.exitCode).toBe(2);
|
|
65
|
+
});
|
|
60
66
|
it('returns v1 not-required before reading personal policy or capability state', async () => {
|
|
61
67
|
const policy = jest.fn();
|
|
62
68
|
const capabilities = jest.fn();
|
|
@@ -183,7 +183,7 @@ describe('compact lifecycle proof and unused bootstrap archival', () => {
|
|
|
183
183
|
(0, store_1.writeJournal)(repo, 'main', launched);
|
|
184
184
|
const adapter = adapterModule.adapterFor('codex');
|
|
185
185
|
jest.spyOn(adapterModule, 'adapterFor').mockReturnValue({ ...adapter,
|
|
186
|
-
activity: () => ({ cpuTime: '0', groupSize: 1 }), safeToReplace: () => 'indeterminate' });
|
|
186
|
+
activity: () => ({ cpuTime: '0', groupSize: 1, descendants: '0:none' }), safeToReplace: () => 'indeterminate' });
|
|
187
187
|
for (let i = 0; i < 100; i++) {
|
|
188
188
|
expect(await supervisor.tick()).toBe('continue');
|
|
189
189
|
if (Object.values((0, store_1.readJournal)(repo, 'main').state.jobs).some(job => job.verdict === 'pass'))
|
|
@@ -489,7 +489,7 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
489
489
|
(0, store_1.writeJournal)(repo, 'main', launched);
|
|
490
490
|
const adapter = adapterModule.adapterFor('codex');
|
|
491
491
|
jest.spyOn(adapterModule, 'adapterFor').mockReturnValue({ ...adapter,
|
|
492
|
-
activity: () => ({ cpuTime: '0', groupSize: 1 }), safeToReplace: () => 'indeterminate' });
|
|
492
|
+
activity: () => ({ cpuTime: '0', groupSize: 1, descendants: '0:none' }), safeToReplace: () => 'indeterminate' });
|
|
493
493
|
fs_1.default.writeFileSync(path_1.default.join(repo, 's1-red.test.ts'), 'test("RED", () => { throw new Error("RED"); });\n');
|
|
494
494
|
(0, request_1.requestJob)(repo, 'main', gen.token, [process.execPath, '-e', 'process.exit(0)'], [], '.');
|
|
495
495
|
report = redSensor;
|
|
@@ -104,7 +104,7 @@ function adoptFixtureController(repo) {
|
|
|
104
104
|
(0, store_1.writeJournal)(repo, 'main', state);
|
|
105
105
|
const adapter = adapterModule.adapterFor('codex');
|
|
106
106
|
jest.spyOn(adapterModule, 'adapterFor').mockReturnValue({ ...adapter,
|
|
107
|
-
activity: () => ({ cpuTime: '0', groupSize: 1 }), safeToReplace: () => 'indeterminate' });
|
|
107
|
+
activity: () => ({ cpuTime: '0', groupSize: 1, descendants: '0:none' }), safeToReplace: () => 'indeterminate' });
|
|
108
108
|
}
|
|
109
109
|
/** Legacy fixtures predate compact-only admission; make their execution contract explicit. */
|
|
110
110
|
function initUnattendedFixture(repo) {
|
|
@@ -672,6 +672,47 @@ describe('supervisor loop', () => {
|
|
|
672
672
|
expect(child.killed).toBe(false); // y NO mato al controlador
|
|
673
673
|
child.kill('SIGKILL');
|
|
674
674
|
});
|
|
675
|
+
// v1 smoke 2026-09-26 (#196): a Codex controller waiting on a native
|
|
676
|
+
// subagent kept an idle leader while the work ran in processes outside its
|
|
677
|
+
// process group, and the supervisor entered a false custody. Work in the
|
|
678
|
+
// descendant tree is activity: observe, never custody, while it moves.
|
|
679
|
+
itPosix('espera sana: lider ocioso + descendiente fuera del grupo trabajando => observa, no custodia (R4.2)', async () => {
|
|
680
|
+
initUnattendedFixture(repo);
|
|
681
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
682
|
+
const worker = "require('child_process').spawn('sh', ['-c', 'while :; do sleep 0.02; done'], { detached: true, stdio: 'ignore' }).unref(); setTimeout(()=>{}, 20000)";
|
|
683
|
+
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', worker], process.cwd(), 'nWait');
|
|
684
|
+
try {
|
|
685
|
+
let s = (0, store_1.readJournal)(repo, 'main').state;
|
|
686
|
+
(0, generations_1.activeGeneration)(s).processRef = ref;
|
|
687
|
+
s.controllerHeartbeatAt = new Date(Date.now() - 3600000).toISOString(); // heartbeat vencido: bloqueado en la espera
|
|
688
|
+
(0, store_1.writeJournal)(repo, 'main', s);
|
|
689
|
+
await until(() => ((0, process_1.activitySnapshot)(ref)?.descendants ?? '0:none') !== '0:none');
|
|
690
|
+
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, provider: 'codex', heartbeatTimeoutMs: 1, activityWindowMs: 50, tickMs: 20 };
|
|
691
|
+
const sup = new supervisor_1.Supervisor(repo, 'main', cfg, fakeSpawner, undefined, admitted);
|
|
692
|
+
await sup.tick();
|
|
693
|
+
await new Promise((r) => setTimeout(r, 150)); // mas que la ventana, pero el arbol se movio
|
|
694
|
+
expect(await sup.tick()).not.toBe('custody');
|
|
695
|
+
const after = (0, store_1.readJournal)(repo, 'main').state;
|
|
696
|
+
expect(after.cycle.status).toBe('IN_PROGRESS');
|
|
697
|
+
expect((0, generations_1.activeGeneration)(after).state).toBe('controller-suspected-stall'); // observado, jamas matado
|
|
698
|
+
expect(child.killed).toBe(false);
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
// The detached shell leads its own group: kill that group, never a pattern match.
|
|
702
|
+
let shells = [];
|
|
703
|
+
try {
|
|
704
|
+
shells = (0, child_process_1.execFileSync)('pgrep', ['-P', String(ref.pid)], { encoding: 'utf8' }).split('\n').filter(Boolean).map(Number);
|
|
705
|
+
}
|
|
706
|
+
catch { /* sin hijos */ }
|
|
707
|
+
for (const pid of shells) {
|
|
708
|
+
try {
|
|
709
|
+
process.kill(-pid, 'SIGKILL');
|
|
710
|
+
}
|
|
711
|
+
catch { /* ya ausente */ }
|
|
712
|
+
}
|
|
713
|
+
child.kill('SIGKILL');
|
|
714
|
+
}
|
|
715
|
+
});
|
|
675
716
|
itPosix('runSupervisorLoop: bootstrap gen-1 con stub codex, COMPLETE => libera lock y termina su generacion (R4.1/R4.5/R2.4)', async () => {
|
|
676
717
|
initUnattendedFixture(repo);
|
|
677
718
|
jest.spyOn(fingerprint, 'reconcileUnattendedRecovery').mockReturnValue({
|
|
@@ -167,6 +167,70 @@ describe('process identity', () => {
|
|
|
167
167
|
expect(snap.groupSize).toBeGreaterThanOrEqual(1);
|
|
168
168
|
child.kill('SIGKILL');
|
|
169
169
|
});
|
|
170
|
+
// False-stall root cause (Kodria/agentic-workflow#196, v1 smoke 2026-09-26):
|
|
171
|
+
// a Codex controller waiting on a native subagent barely moves its own CPU
|
|
172
|
+
// time, and the work it waits on can run in processes OUTSIDE its process
|
|
173
|
+
// group (sandboxed tool commands). Activity must see the whole descendant
|
|
174
|
+
// tree, or a healthy wait reads as frozen and the cycle enters custody.
|
|
175
|
+
describe('descendantActivity (descendant tree, not only the process group)', () => {
|
|
176
|
+
const table = [
|
|
177
|
+
' 10 1 00:00:05',
|
|
178
|
+
' 20 10 00:00:01', // child
|
|
179
|
+
' 30 20 00:00:02', // grandchild (e.g. setsid'd sandbox command)
|
|
180
|
+
' 40 1 00:00:09', // unrelated
|
|
181
|
+
' 50 40 00:00:03', // child of the unrelated process
|
|
182
|
+
].join('\n');
|
|
183
|
+
test('counts children and grandchildren, excludes the root and unrelated processes', () => {
|
|
184
|
+
expect((0, process_1.descendantActivity)(table, 10)).toMatch(/^2:[0-9a-f]{64}$/);
|
|
185
|
+
expect((0, process_1.descendantActivity)(table, 20)).toMatch(/^1:/);
|
|
186
|
+
expect((0, process_1.descendantActivity)(table, 30)).toBe('0:none');
|
|
187
|
+
expect((0, process_1.descendantActivity)(table, 999)).toBe('0:none');
|
|
188
|
+
});
|
|
189
|
+
test('changes when a descendant accumulates cpu, appears or exits, and ignores unrelated churn', () => {
|
|
190
|
+
const base = (0, process_1.descendantActivity)(table, 10);
|
|
191
|
+
expect((0, process_1.descendantActivity)(table.replace('30 20 00:00:02', '30 20 00:00:03'), 10)).not.toBe(base);
|
|
192
|
+
expect((0, process_1.descendantActivity)(`${table}\n 60 30 00:00:00`, 10)).not.toBe(base);
|
|
193
|
+
expect((0, process_1.descendantActivity)(table.split('\n').filter((line) => !line.includes('30 20')).join('\n'), 10)).not.toBe(base);
|
|
194
|
+
expect((0, process_1.descendantActivity)(table.replace('50 40 00:00:03', '50 40 00:00:07'), 10)).toBe(base);
|
|
195
|
+
expect((0, process_1.descendantActivity)(`${table}\n 70 40 00:00:00`, 10)).toBe(base);
|
|
196
|
+
});
|
|
197
|
+
test('rejects an invalid root and never follows a ppid cycle forever', () => {
|
|
198
|
+
expect(() => (0, process_1.descendantActivity)(table, 0)).toThrow(/root pid/);
|
|
199
|
+
expect(() => (0, process_1.descendantActivity)(table, Number.NaN)).toThrow(/root pid/);
|
|
200
|
+
expect((0, process_1.descendantActivity)(' 10 1 00:00:00\n 20 10 00:00:00\n 10 20 00:00:00', 10)).toMatch(/^1:/);
|
|
201
|
+
});
|
|
202
|
+
test('ignores malformed ps lines instead of inventing processes', () => {
|
|
203
|
+
expect((0, process_1.descendantActivity)('garbage\n 20 10 00:00:01\n\n x y z', 10)).toMatch(/^1:/);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
test('activitySnapshot ve un nieto fuera del process group del controlador', async () => {
|
|
207
|
+
if ((0, paths_1.isWindowsNative)())
|
|
208
|
+
return; // win32 degrada activity sin ps (ver activitySnapshot)
|
|
209
|
+
const script = "setTimeout(() => { require('child_process').spawn(process.execPath, ['-e', 'setTimeout(()=>{}, 4000)'], { detached: true, stdio: 'ignore' }).unref(); }, 400); setTimeout(()=>{}, 4000)";
|
|
210
|
+
const { child, ref } = (0, process_1.spawnStructured)(['node', '-e', script], process.cwd(), 'n-desc');
|
|
211
|
+
try {
|
|
212
|
+
const before = (0, process_1.activitySnapshot)(ref);
|
|
213
|
+
expect(before).not.toBeNull();
|
|
214
|
+
let after = (0, process_1.activitySnapshot)(ref);
|
|
215
|
+
for (let i = 0; i < 40 && after !== null && after.descendants === before.descendants; i++) {
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
217
|
+
after = (0, process_1.activitySnapshot)(ref);
|
|
218
|
+
}
|
|
219
|
+
expect(after).not.toBeNull();
|
|
220
|
+
// The detached grandchild lives in its own process group: the group
|
|
221
|
+
// signal cannot see it, the descendant signal must.
|
|
222
|
+
expect(after.groupSize).toBe(before.groupSize);
|
|
223
|
+
expect(after.descendants).not.toBe(before.descendants);
|
|
224
|
+
expect(after.descendants).toMatch(/^1:/);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
try {
|
|
228
|
+
process.kill(-ref.processGroup, 'SIGKILL');
|
|
229
|
+
}
|
|
230
|
+
catch { /* ya ausente */ }
|
|
231
|
+
child.kill('SIGKILL');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
170
234
|
test('captureSelfRef captura la identidad del proceso actual (R2.1)', () => {
|
|
171
235
|
const self = (0, process_1.captureSelfRef)('nonce-self');
|
|
172
236
|
expect(self.pid).toBe(process.pid);
|
|
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
10
10
|
const crypto_1 = __importDefault(require("crypto"));
|
|
11
11
|
const json_1 = require("../../../src/core/plan/json");
|
|
12
12
|
const validate_1 = require("../../../src/core/plan/validate");
|
|
13
|
+
const resolve_1 = require("../../../src/core/model-policy/resolve");
|
|
13
14
|
jest.mock('child_process', () => ({ exec: jest.fn(), execFileSync: jest.fn(), execSync: jest.fn(), spawn: jest.fn(), spawnSync: jest.fn() }));
|
|
14
15
|
const START = '<!-- AWM:COMPACT-SLICES:START v1 -->';
|
|
15
16
|
const END = '<!-- AWM:COMPACT-SLICES:END v1 -->';
|
|
@@ -99,6 +100,42 @@ describe('validatePlanFile', () => {
|
|
|
99
100
|
const report = (0, validate_1.validatePlanFile)(v2Fixture(root, profile), root);
|
|
100
101
|
expect(report).toMatchObject({ state: 'valid', schema: 'compact-slices/v2', manifest: { slices: [expect.objectContaining({ implementerProfile: profile })] } });
|
|
101
102
|
});
|
|
103
|
+
test.each([
|
|
104
|
+
['proveedor-nativo', false],
|
|
105
|
+
['awm-routed', true],
|
|
106
|
+
])('accepts an explicit %s dispatch mode only with its matching schema', (mode, v2) => {
|
|
107
|
+
const plan = v2 ? v2Fixture(root) : fixture(root);
|
|
108
|
+
const before = fs_1.default.readFileSync(plan, 'utf8');
|
|
109
|
+
fs_1.default.writeFileSync(plan, `**Modo de despacho:** ${mode}\n${before}`);
|
|
110
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', dispatchMode: mode });
|
|
111
|
+
fs_1.default.writeFileSync(plan, `**Modo de despacho:** ${v2 ? 'proveedor-nativo' : 'awm-routed'}\n${before}`);
|
|
112
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE' }] });
|
|
113
|
+
});
|
|
114
|
+
test.each(['indefinido', 'proveedor-nativo\n**Modo de despacho:** proveedor-nativo'])('rejects an invalid or duplicated dispatch header: %s', value => {
|
|
115
|
+
const plan = fixture(root);
|
|
116
|
+
fs_1.default.writeFileSync(plan, `**Modo de despacho:** ${value}\n${fs_1.default.readFileSync(plan, 'utf8')}`);
|
|
117
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [{ code: 'PLAN_DISPATCH_MODE' }] });
|
|
118
|
+
});
|
|
119
|
+
test('binds an explicit dispatch choice into plan and execution identity', () => {
|
|
120
|
+
const plan = fixture(root);
|
|
121
|
+
const historical = (0, validate_1.validatePlanFile)(plan, root);
|
|
122
|
+
fs_1.default.writeFileSync(plan, `**Modo de despacho:** proveedor-nativo\n${fs_1.default.readFileSync(plan, 'utf8')}`);
|
|
123
|
+
const explicit = (0, validate_1.validatePlanFile)(plan, root);
|
|
124
|
+
expect(historical).toMatchObject({ state: 'valid' });
|
|
125
|
+
expect(explicit).toMatchObject({ state: 'valid', dispatchMode: 'proveedor-nativo' });
|
|
126
|
+
if (historical.state !== 'valid' || explicit.state !== 'valid')
|
|
127
|
+
throw new Error('both v1 plans must validate');
|
|
128
|
+
expect(explicit.planDigest).not.toBe(historical.planDigest);
|
|
129
|
+
expect(explicit.executionDigest).not.toBe(historical.executionDigest);
|
|
130
|
+
});
|
|
131
|
+
test('does not let v1 opt-in override an explicit provider-native plan', () => {
|
|
132
|
+
const plan = fixture(root);
|
|
133
|
+
fs_1.default.writeFileSync(plan, `**Modo de despacho:** proveedor-nativo\n${fs_1.default.readFileSync(plan, 'utf8')}`);
|
|
134
|
+
const report = (0, validate_1.validatePlanFile)(plan, root);
|
|
135
|
+
if (report.state !== 'valid')
|
|
136
|
+
throw new Error('provider-native fixture must validate');
|
|
137
|
+
expect((0, resolve_1.resolveV1Dispatch)({ plan: report, optInV1: true, role: 'controller', runtime: { target: 'codex', kind: 'native', version: '1.0.0', accountScopeDigest: 'a'.repeat(64) }, now: new Date() })).toMatchObject({ state: 'blocked', diagnostics: [{ code: 'ROUTING_DISPATCH_MODE' }] });
|
|
138
|
+
});
|
|
102
139
|
test('rejects v1 profile without weakening exact keys', () => {
|
|
103
140
|
expect((0, validate_1.validatePlanFile)(fixture(root, manifest => { (manifest.slices[0]).implementerProfile = 'mechanical'; }), root)).toMatchObject({ state: 'invalid' });
|
|
104
141
|
});
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|