agentic-workflow-manager 9.7.0 → 9.8.0
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/README.md +5 -1
- package/dist/src/commands/doctor.js +4 -2
- package/dist/src/commands/export.js +3 -0
- package/dist/src/commands/init.js +10 -2
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- package/dist/src/commands/plan/index.js +124 -6
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +7 -5
- package/dist/src/commands/registry/install-bundles.js +4 -4
- package/dist/src/commands/registry/status.js +4 -4
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/sync.js +2 -1
- package/dist/src/commands/update.js +3 -1
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +181 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/bundle-install.js +2 -2
- package/dist/src/core/bundles.js +92 -19
- package/dist/src/core/diagnostics/context.js +1 -1
- package/dist/src/core/export/index.js +1 -1
- package/dist/src/core/export/resolve.js +2 -2
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +35 -1
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/plan/identity.js +75 -0
- package/dist/src/core/plan/validate.js +178 -64
- package/dist/src/core/reconciliation.js +1 -1
- package/dist/src/index.js +46 -39
- package/dist/src/utils/grouping.js +1 -1
- package/dist/src/utils/registry-view.js +1 -1
- package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
- package/dist/tests/commands/export.test.js +28 -2
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
- package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +630 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +87 -0
- package/dist/tests/core/bundle-install.test.js +36 -0
- package/dist/tests/core/bundles-multiroot.test.js +24 -2
- package/dist/tests/core/bundles-overrides.test.js +10 -7
- package/dist/tests/core/bundles.test.js +75 -6
- package/dist/tests/core/diagnostics/context.test.js +2 -2
- package/dist/tests/core/export/engine.test.js +1 -1
- package/dist/tests/core/export/resolve.test.js +1 -1
- package/dist/tests/core/init/mutation-targets.test.js +1 -1
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +1 -1
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -0
- package/dist/tests/core/plan/validate.test.js +320 -6
- package/dist/tests/core/reconciliation.test.js +1 -1
- package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +16 -0
- package/dist/tests/utils/grouping.test.js +2 -2
- package/dist/tests/utils/registry-view.test.js +7 -7
- package/package.json +2 -2
- 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
|
@@ -25,11 +25,28 @@ const tracks_1 = require("../../../src/commands/watch/tracks");
|
|
|
25
25
|
const supervisor_1 = require("../../../src/commands/watch/supervisor");
|
|
26
26
|
const exec_wrapper_1 = require("../../../src/commands/job/exec-wrapper");
|
|
27
27
|
const init_1 = require("../../../src/commands/watch/init");
|
|
28
|
+
const validate_1 = require("../../../src/core/plan/validate");
|
|
28
29
|
const request_1 = require("../../../src/commands/job/request");
|
|
29
30
|
const requests_1 = require("../../../src/core/journal/requests");
|
|
30
31
|
const fingerprint_1 = require("../../../src/core/journal/fingerprint");
|
|
31
32
|
const store_1 = require("../../../src/core/journal/store");
|
|
32
33
|
const paths_1 = require("../../../src/core/journal/paths");
|
|
34
|
+
const admitted = async () => ({
|
|
35
|
+
state: 'admitted', planState: 'valid', executionMode: 'desatendido',
|
|
36
|
+
journal: 'current', currentness: 'current', sensors: 'pass', diagnostics: [],
|
|
37
|
+
});
|
|
38
|
+
function initCompactWatch(repoRoot, branch) {
|
|
39
|
+
const planPath = path_1.default.join(repoRoot, 'plans', 'fixture.md');
|
|
40
|
+
fs_1.default.mkdirSync(path_1.default.dirname(planPath), { recursive: true });
|
|
41
|
+
fs_1.default.writeFileSync(planPath, fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8'));
|
|
42
|
+
fs_1.default.writeFileSync(path_1.default.join(repoRoot, 'source.md'), '## Canonical source\nfixture source\n');
|
|
43
|
+
git(repoRoot, 'add', 'plans/fixture.md', 'source.md');
|
|
44
|
+
git(repoRoot, 'commit', '-qm', 'compact plan fixture');
|
|
45
|
+
const report = (0, validate_1.validatePlanFile)('plans/fixture.md', repoRoot);
|
|
46
|
+
if (report.state !== 'valid')
|
|
47
|
+
throw new Error(`compact fixture must validate: ${JSON.stringify(report)}`);
|
|
48
|
+
(0, init_1.initWatch)(repoRoot, branch, { path: 'plans/fixture.md', report });
|
|
49
|
+
}
|
|
33
50
|
function git(cwd, ...args) {
|
|
34
51
|
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, stdio: 'pipe' });
|
|
35
52
|
}
|
|
@@ -283,7 +300,7 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
283
300
|
payload: { verdictId, obligationId, result: 'pass', detail: 'ok', fingerprint: fp, argv, paths: [], cwd: '.' } });
|
|
284
301
|
}
|
|
285
302
|
function setUpGreenTrack() {
|
|
286
|
-
(
|
|
303
|
+
initCompactWatch(repo, BRANCH);
|
|
287
304
|
// `initWatch` ESCRIBE `.gitignore` (`.awm/`) pero no lo commitea — un
|
|
288
305
|
// `.gitignore` sin trackear es en sí mismo un path "untracked" para
|
|
289
306
|
// `git status --porcelain` (R6.4's chequeo de limpieza no distingue
|
|
@@ -328,7 +345,7 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
328
345
|
test('freeze completo: drena jobs vivos, exige gate local + worktree limpio, termina la generación propia, persiste frozen (R5.2/R6.3)', async () => {
|
|
329
346
|
setUpGreenTrack(); // sin freeze todavía: el track hace su trabajo real primero, como en el mundo real
|
|
330
347
|
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10, provider: 'codex' };
|
|
331
|
-
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, fakeSpawner);
|
|
348
|
+
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, fakeSpawner, undefined, admitted);
|
|
332
349
|
// Drenar el trabajo real (2 jobs) ANTES de que el plan pida el
|
|
333
350
|
// freeze — mismo orden que el mundo real (freeze llega cuando el
|
|
334
351
|
// track ya terminó su verificación).
|
|
@@ -351,7 +368,7 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
351
368
|
test('job varado en spawn-intent con refs muertas justo al pedirse el freeze SI se reintenta y drena — el freeze converge en vez de quedar en `continue` para siempre (regresión R6.3: `dispatch:false` no debe suprimir retry-same-intent)', async () => {
|
|
352
369
|
setUpGreenTrack();
|
|
353
370
|
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10, provider: 'codex' };
|
|
354
|
-
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, fakeSpawner);
|
|
371
|
+
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, fakeSpawner, undefined, admitted);
|
|
355
372
|
await tickUntil(sup, () => {
|
|
356
373
|
const s = (0, store_1.readJournal)(repo, BRANCH).state;
|
|
357
374
|
return Object.values(s.jobs).length === 2 && Object.values(s.jobs).every((j) => j.executionState === 'exited' && j.verdict === 'pass');
|
|
@@ -365,10 +382,11 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
365
382
|
// `setUpGreenTrack`) — lo unico que debe bloquear la convergencia
|
|
366
383
|
// del freeze es que siga contando como LIVE en `attemptFreeze`.
|
|
367
384
|
const dead = { pid: 999999, startTime: 'gone', spawnNonce: 'stuck-nonce', argvDigest: 'd', processGroup: 999999, psArgsDigest: 'x' };
|
|
385
|
+
const fingerprint = (0, fingerprint_1.computeFingerprint)(repo, ['node', '-e', 'process.exit(0)'], [], '.');
|
|
368
386
|
const s1 = (0, store_1.readJournal)(repo, BRANCH).state;
|
|
369
387
|
s1.jobs['stuck-job'] = {
|
|
370
|
-
id: 'stuck-job', fingerprint:
|
|
371
|
-
paths: [], expandedPaths:
|
|
388
|
+
id: 'stuck-job', fingerprint: fingerprint.fingerprint, commandDigest: fingerprint.commandDigest, argv: ['node', '-e', 'process.exit(0)'], cwd: '.',
|
|
389
|
+
paths: [], expandedPaths: fingerprint.expandedPaths, executionState: 'spawn-intent', observationState: 'progressing',
|
|
372
390
|
spawnNonce: 'stuck-nonce', processRef: dead, wrapperRef: dead,
|
|
373
391
|
phaseTimestamps: { received: new Date(Date.now() - 120000).toISOString(), 'spawn-intent': new Date(Date.now() - 120000).toISOString() },
|
|
374
392
|
};
|
|
@@ -389,7 +407,7 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
389
407
|
expect(final.frozen.headSha).toBe((0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim());
|
|
390
408
|
});
|
|
391
409
|
test('deja de despachar trabajo NUEVO mientras el freeze está pendiente: un job-request llega pero no se spawnea (paso 1)', async () => {
|
|
392
|
-
(
|
|
410
|
+
initCompactWatch(repo, BRANCH);
|
|
393
411
|
const s = (0, store_1.readJournal)(repo, BRANCH).state;
|
|
394
412
|
s.trackContext = { trackId: 'a', taskIds: [], planDigest: '', baseSha: 'seed', planJournalId: 'j-plan' };
|
|
395
413
|
(0, store_1.writeJournal)(repo, BRANCH, s);
|
|
@@ -397,7 +415,7 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
397
415
|
let spawnCalls = 0;
|
|
398
416
|
const countingSpawner = () => { spawnCalls++; };
|
|
399
417
|
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10, provider: 'codex' };
|
|
400
|
-
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, countingSpawner);
|
|
418
|
+
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, countingSpawner, undefined, admitted);
|
|
401
419
|
await sup.tick(); // consume el freeze-request => freezeRequested:true
|
|
402
420
|
(0, request_1.requestJob)(repo, BRANCH, 'g0', ['node', '-e', 'process.exit(0)'], [], '.');
|
|
403
421
|
await sup.tick();
|
|
@@ -407,36 +425,31 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
|
|
|
407
425
|
expect(after.freezeRequested).toBe(true);
|
|
408
426
|
expect(Object.values(after.jobs)[0].executionState).toBe('received'); // nunca avanzó a spawn-intent
|
|
409
427
|
});
|
|
410
|
-
test('worktree sucio
|
|
428
|
+
test('worktree sucio observado en modo compacto entra en custodia y no congela evidencia potencialmente stale', async () => {
|
|
411
429
|
setUpGreenTrack();
|
|
412
430
|
const cfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10, provider: 'codex' };
|
|
413
|
-
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, fakeSpawner);
|
|
431
|
+
const sup = new supervisor_1.Supervisor(repo, BRANCH, cfg, fakeSpawner, undefined, admitted);
|
|
414
432
|
await tickUntil(sup, () => {
|
|
415
433
|
const s = (0, store_1.readJournal)(repo, BRANCH).state;
|
|
416
434
|
return Object.values(s.jobs).length === 2 && Object.values(s.jobs).every((j) => j.executionState === 'exited' && j.verdict === 'pass');
|
|
417
435
|
});
|
|
418
436
|
requestFreeze();
|
|
437
|
+
// Los jobs ya terminados no participan del guard de árbol de recovery.
|
|
438
|
+
// Este intento vivo, fuera del plan de verificación, aísla el hecho
|
|
439
|
+
// relevante: el cambio posterior invalida SU fingerprint y debe
|
|
440
|
+
// bloquear antes de intentar congelar.
|
|
441
|
+
const fingerprint = (0, fingerprint_1.computeFingerprint)(repo, ['node', '-e', 'process.exit(0)'], [], '.');
|
|
442
|
+
const state = (0, store_1.readJournal)(repo, BRANCH).state;
|
|
443
|
+
state.jobs['fingerprint-sentinel'] = {
|
|
444
|
+
id: 'fingerprint-sentinel', fingerprint: fingerprint.fingerprint, commandDigest: fingerprint.commandDigest,
|
|
445
|
+
argv: ['node', '-e', 'process.exit(0)'], cwd: '.', paths: [], expandedPaths: fingerprint.expandedPaths,
|
|
446
|
+
executionState: 'spawn-intent', observationState: 'progressing', phaseTimestamps: { received: new Date().toISOString(), 'spawn-intent': new Date().toISOString() },
|
|
447
|
+
};
|
|
448
|
+
(0, store_1.writeJournal)(repo, BRANCH, state);
|
|
419
449
|
fs_1.default.writeFileSync(path_1.default.join(repo, 'dirty.txt'), 'sin commitear'); // untracked: worktree sucio
|
|
420
|
-
|
|
421
|
-
for (let i = 0; i < 20; i++) {
|
|
422
|
-
const outcome = await sup.tick();
|
|
423
|
-
if (outcome === 'frozen')
|
|
424
|
-
sawFrozenWhileDirty = true;
|
|
425
|
-
await new Promise((r) => setTimeout(r, 15));
|
|
426
|
-
}
|
|
427
|
-
expect(sawFrozenWhileDirty).toBe(false);
|
|
450
|
+
expect(await sup.tick()).toBe('custody');
|
|
428
451
|
expect((0, store_1.readJournal)(repo, BRANCH).state.frozen).toBeUndefined();
|
|
429
|
-
|
|
430
|
-
// de commitearlo): commitear un archivo NUEVO cambia el árbol que
|
|
431
|
-
// las evidencias ya persistidas (`v1`/`v-sensors`) fingerprintearon,
|
|
432
|
-
// invalidándolas de verdad (`stale-fingerprint`) — comportamiento
|
|
433
|
-
// CORRECTO del gate, pero un concern DISTINTO al que este test
|
|
434
|
-
// ejercita (paso 4, limpieza — no re-vigencia de evidencia, ya
|
|
435
|
-
// cubierta en `gate.test.ts`). Quitar el archivo deja el árbol
|
|
436
|
-
// BYTE A BYTE igual al que las evidencias ya certificaron.
|
|
437
|
-
fs_1.default.rmSync(path_1.default.join(repo, 'dirty.txt'));
|
|
438
|
-
const outcome = await tickUntil(sup, () => (0, store_1.readJournal)(repo, BRANCH).state.frozen !== undefined);
|
|
439
|
-
expect(outcome).toBe('frozen');
|
|
452
|
+
expect((0, store_1.readJournal)(repo, BRANCH).state.cycle.blockedReason).toContain('git-fingerprint-changed');
|
|
440
453
|
});
|
|
441
454
|
});
|
|
442
455
|
// --- Parte 3: loop end-to-end de DOS Supervisors reales ---------------------
|
|
@@ -512,7 +525,7 @@ describe('loop end-to-end: plan (reconcileTracks) + track (Supervisor.tick) cier
|
|
|
512
525
|
}
|
|
513
526
|
test('reconcileTracks real (plan) + Supervisor.tick real (track), en loop contra dos worktrees reales, cierran request -> drenaje -> persist -> observación -> FROZEN', async () => {
|
|
514
527
|
// --- lado TRACK: registrar el trabajo y drenarlo con un Supervisor REAL ---
|
|
515
|
-
(
|
|
528
|
+
initCompactWatch(trackWorktree, TRACK_BRANCH);
|
|
516
529
|
const ts0 = (0, store_1.readJournal)(trackWorktree, TRACK_BRANCH).state;
|
|
517
530
|
ts0.trackContext = { trackId: 'a', taskIds: ['T1'], planDigest: '', baseSha, planJournalId: 'j-plan' };
|
|
518
531
|
(0, store_1.writeJournal)(trackWorktree, TRACK_BRANCH, ts0);
|
|
@@ -525,7 +538,7 @@ describe('loop end-to-end: plan (reconcileTracks) + track (Supervisor.tick) cier
|
|
|
525
538
|
(0, requests_1.emitRequest)(trackWorktree, TRACK_BRANCH, { kind: 'register-entity', generationToken: 'g0', idempotencyKey: 'e3',
|
|
526
539
|
payload: { entity: 'task-status', taskId: 'T1', status: 'done' } });
|
|
527
540
|
const trackCfg = { ...supervisor_1.DEFAULT_SUPERVISOR_CONFIG, tickMs: 10, provider: 'codex' };
|
|
528
|
-
const trackSup = new supervisor_1.Supervisor(trackWorktree, TRACK_BRANCH, trackCfg, fakeSpawner);
|
|
541
|
+
const trackSup = new supervisor_1.Supervisor(trackWorktree, TRACK_BRANCH, trackCfg, fakeSpawner, undefined, admitted);
|
|
529
542
|
for (let i = 0; i < 400; i++) {
|
|
530
543
|
const s = (0, store_1.readJournal)(trackWorktree, TRACK_BRANCH).state;
|
|
531
544
|
if (Object.values(s.jobs).length === 2 && Object.values(s.jobs).every((j) => j.executionState === 'exited' && j.verdict === 'pass'))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const watchRoot = path_1.default.resolve(__dirname, '../../../src/commands/watch');
|
|
9
|
+
describe('watch state dependency boundary', () => {
|
|
10
|
+
test('teardown driver consumes shared state through the neutral track-state module', () => {
|
|
11
|
+
const source = fs_1.default.readFileSync(path_1.default.join(watchRoot, 'teardown-driver.ts'), 'utf8');
|
|
12
|
+
expect(source).toContain("from './track-state'");
|
|
13
|
+
expect(source).not.toMatch(/from '\.\/tracks';/);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -6,8 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const fs_1 = __importDefault(require("fs"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const commander_1 = require("commander");
|
|
9
11
|
const init_1 = require("../../../src/commands/watch/init");
|
|
12
|
+
const watch_1 = require("../../../src/commands/watch");
|
|
10
13
|
const store_1 = require("../../../src/core/journal/store");
|
|
14
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
15
|
+
const validate_1 = require("../../../src/core/plan/validate");
|
|
16
|
+
const git_fixture_1 = require("../../helpers/git-fixture");
|
|
11
17
|
describe('watch --init: plan-vs-repo mecanico', () => {
|
|
12
18
|
let repo;
|
|
13
19
|
beforeEach(() => { repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-')); });
|
|
@@ -32,6 +38,117 @@ describe('watch --init: plan-vs-repo mecanico', () => {
|
|
|
32
38
|
test('repo sin verificadores => lista vacia (el gate degrada por empty-cycle-plan igualmente, R3.6)', () => {
|
|
33
39
|
expect((0, init_1.detectRequiredVerifiers)(repo)).toEqual([]);
|
|
34
40
|
});
|
|
41
|
+
test('falla cerradamente ante un arbol de paquetes demasiado profundo o grande', () => {
|
|
42
|
+
let cursor = repo;
|
|
43
|
+
for (let depth = 0; depth < 65; depth++) {
|
|
44
|
+
cursor = path_1.default.join(cursor, `d${depth}`);
|
|
45
|
+
fs_1.default.mkdirSync(cursor);
|
|
46
|
+
}
|
|
47
|
+
expect(() => (0, init_1.detectRequiredVerifiers)(repo)).toThrow(/límite|limit/i);
|
|
48
|
+
});
|
|
49
|
+
test('falla cerradamente ante package.json que excede el límite de lectura', () => {
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), '{"scripts":{"test":"x"},"padding":"' + 'x'.repeat(1024 * 1024) + '"}');
|
|
51
|
+
expect(() => (0, init_1.detectRequiredVerifiers)(repo)).toThrow(/package\.json.*(?:límite|limit|oversized)/i);
|
|
52
|
+
});
|
|
53
|
+
test('falla cerradamente al superar el límite de entradas del escaneo', () => {
|
|
54
|
+
for (let index = 0; index <= 10000; index++)
|
|
55
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, `entry-${index}`), '');
|
|
56
|
+
expect(() => (0, init_1.detectRequiredVerifiers)(repo)).toThrow(/entry limit/i);
|
|
57
|
+
});
|
|
58
|
+
test('falla cerradamente ante un package.json ilegible o malformado', () => {
|
|
59
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), '{not json');
|
|
60
|
+
expect(() => (0, init_1.detectRequiredVerifiers)(repo)).toThrow(/rejected package\.json/i);
|
|
61
|
+
});
|
|
62
|
+
test.each(['growth', 'leaf-swap', 'parent-swap'])('rejects stale Dirent %s before journal or gitignore mutations', attack => {
|
|
63
|
+
const directory = attack === 'parent-swap' ? path_1.default.join(repo, 'child') : repo;
|
|
64
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
65
|
+
const packageFile = path_1.default.join(directory, 'package.json');
|
|
66
|
+
fs_1.default.writeFileSync(packageFile, '{"scripts":{"test":"jest"}}');
|
|
67
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-outside-'));
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(outside, 'package.json'), '{"scripts":{"test":"outside-command"}}');
|
|
69
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), 'existing-rule\n');
|
|
70
|
+
const nativeReaddir = fs_1.default.readdirSync;
|
|
71
|
+
const nativeStat = fs_1.default.statSync;
|
|
72
|
+
let attacked = false;
|
|
73
|
+
const stat = jest.spyOn(fs_1.default, 'statSync').mockImplementation(((...args) => {
|
|
74
|
+
if (String(args[0]) === packageFile && attack === 'growth')
|
|
75
|
+
return { size: 2 };
|
|
76
|
+
return Reflect.apply(nativeStat, fs_1.default, args);
|
|
77
|
+
}));
|
|
78
|
+
const readdir = jest.spyOn(fs_1.default, 'readdirSync').mockImplementation(((...args) => {
|
|
79
|
+
const entries = Reflect.apply(nativeReaddir, fs_1.default, args);
|
|
80
|
+
if (!attacked && String(args[0]) === directory) {
|
|
81
|
+
attacked = true;
|
|
82
|
+
if (attack === 'growth')
|
|
83
|
+
fs_1.default.writeFileSync(packageFile, JSON.stringify({ scripts: { test: 'jest' }, padding: 'x'.repeat(256 * 1024) }));
|
|
84
|
+
else if (attack === 'leaf-swap') {
|
|
85
|
+
fs_1.default.renameSync(packageFile, `${packageFile}.original`);
|
|
86
|
+
fs_1.default.symlinkSync(path_1.default.join(outside, 'package.json'), packageFile, 'file');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fs_1.default.renameSync(directory, `${directory}.original`);
|
|
90
|
+
fs_1.default.symlinkSync(outside, directory, 'dir');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return entries;
|
|
94
|
+
}));
|
|
95
|
+
try {
|
|
96
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama')).toThrow(/package\.json|bounded|regular|unsafe/i);
|
|
97
|
+
expect(attacked).toBe(true);
|
|
98
|
+
expect(fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8')).toBe('existing-rule\n');
|
|
99
|
+
expect((0, store_1.readJournal)(repo, 'rama').state).toBeNull();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
stat.mockRestore();
|
|
103
|
+
readdir.mockRestore();
|
|
104
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
test.each(['symlink', 'directory'])('rejects an explicitly declared nonregular package.json %s rather than treating it as absence', kind => {
|
|
108
|
+
const packageFile = path_1.default.join(repo, 'package.json');
|
|
109
|
+
if (kind === 'directory')
|
|
110
|
+
fs_1.default.mkdirSync(packageFile);
|
|
111
|
+
else {
|
|
112
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'outside-package'), '{}');
|
|
113
|
+
fs_1.default.symlinkSync(path_1.default.join(repo, 'outside-package'), packageFile, 'file');
|
|
114
|
+
}
|
|
115
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), 'unchanged\n');
|
|
116
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama')).toThrow(/package\.json|regular/i);
|
|
117
|
+
expect(fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8')).toBe('unchanged\n');
|
|
118
|
+
expect((0, store_1.readJournal)(repo, 'rama').state).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
test.each(['symlink', 'dangling-link', 'directory', 'oversized', 'malformed'])('rejects unsafe sibling sensors.json %s before init mutations', fault => {
|
|
121
|
+
fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'));
|
|
122
|
+
const sensors = path_1.default.join(repo, '.awm', 'sensors.json');
|
|
123
|
+
if (fault === 'directory')
|
|
124
|
+
fs_1.default.mkdirSync(sensors);
|
|
125
|
+
else if (fault === 'symlink' || fault === 'dangling-link') {
|
|
126
|
+
const destination = path_1.default.join(repo, 'outside-sensors');
|
|
127
|
+
if (fault === 'symlink')
|
|
128
|
+
fs_1.default.writeFileSync(destination, '{}');
|
|
129
|
+
fs_1.default.symlinkSync(destination, sensors, 'file');
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
fs_1.default.writeFileSync(sensors, fault === 'oversized' ? ' '.repeat(256 * 1024 + 1) : '{bad json');
|
|
133
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), 'unchanged\n');
|
|
134
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama')).toThrow(/sensors\.json/i);
|
|
135
|
+
expect(fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8')).toBe('unchanged\n');
|
|
136
|
+
expect((0, store_1.readJournal)(repo, 'rama').state).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
test('falla cerradamente cuando no puede leer un directorio durante el escaneo', () => {
|
|
139
|
+
const original = fs_1.default.readdirSync;
|
|
140
|
+
const readdir = jest.spyOn(fs_1.default, 'readdirSync').mockImplementation(((target, options) => {
|
|
141
|
+
if (String(target) === repo)
|
|
142
|
+
throw new Error('EACCES');
|
|
143
|
+
return original(target, options);
|
|
144
|
+
}));
|
|
145
|
+
try {
|
|
146
|
+
expect(() => (0, init_1.detectRequiredVerifiers)(repo)).toThrow(/cannot read/i);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
readdir.mockRestore();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
35
152
|
test('initWatch persiste requiredVerifiers y gitignorea el journal (R1.1/R1.4b)', () => {
|
|
36
153
|
fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ scripts: { test: 'jest' } }));
|
|
37
154
|
const out = (0, init_1.initWatch)(repo, 'rama');
|
|
@@ -40,4 +157,131 @@ describe('watch --init: plan-vs-repo mecanico', () => {
|
|
|
40
157
|
expect(fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8')).toContain('.awm/');
|
|
41
158
|
expect(() => (0, init_1.initWatch)(repo, 'rama')).not.toThrow(); // idempotente
|
|
42
159
|
});
|
|
160
|
+
test('no escribe .gitignore cuando es un symlink', () => {
|
|
161
|
+
const outside = path_1.default.join(repo, 'outside-gitignore');
|
|
162
|
+
fs_1.default.writeFileSync(outside, 'outside\n');
|
|
163
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(repo, '.gitignore'));
|
|
164
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama')).toThrow(/gitignore.*symlink|symlink.*gitignore/i);
|
|
165
|
+
expect(fs_1.default.readFileSync(outside, 'utf8')).toBe('outside\n');
|
|
166
|
+
});
|
|
167
|
+
test('watch --init --plan crea una sola vinculacion schema-2 desatendida', () => {
|
|
168
|
+
const plan = {
|
|
169
|
+
state: 'valid', schema: 'compact-slices/v1', planDigest: 'a'.repeat(64),
|
|
170
|
+
executionMode: 'desatendido',
|
|
171
|
+
manifest: { schema: 'compact-slices/v1', planId: 'fixture', requirements: [], sources: [], commands: [], slices: [], closureCommands: [] },
|
|
172
|
+
};
|
|
173
|
+
const out = (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: plan });
|
|
174
|
+
expect(out.planBinding).toEqual(expect.objectContaining({ path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido' }));
|
|
175
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
176
|
+
expect(state.schema).toBe(2);
|
|
177
|
+
expect(state.planBinding).toEqual(expect.objectContaining({ path: 'docs/plan.md', digest: 'a'.repeat(64) }));
|
|
178
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: plan })).toThrow(/sobrescribir|overwrite/i);
|
|
179
|
+
});
|
|
180
|
+
test('watch --init --plan bloquea un plan no valido antes de crear journal', () => {
|
|
181
|
+
const invalid = { state: 'invalid', diagnostics: [{ code: 'PLAN_SHAPE', message: 'bad' }] };
|
|
182
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: invalid })).toThrow(/válido/i);
|
|
183
|
+
expect((0, store_1.readJournal)(repo, 'rama').corrupt).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
test('watch --init --plan rechaza un plan interactivo antes de crear un binding desatendido', () => {
|
|
186
|
+
const interactive = { ...validPlan('a'), executionMode: 'interactivo' };
|
|
187
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: interactive })).toThrow(/desatendido/i);
|
|
188
|
+
expect((0, store_1.readJournal)(repo, 'rama').state).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
test('watch --init --plan rechaza paths con caracteres de control antes de persistir', () => {
|
|
191
|
+
const report = { state: 'valid', schema: 'compact-slices/v1', planDigest: 'a'.repeat(64), executionMode: 'desatendido', manifest: {} };
|
|
192
|
+
expect(() => (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan\u0000.md', report })).toThrow(/path inválido/);
|
|
193
|
+
expect((0, store_1.readJournal)(repo, 'rama').state).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
test('rebind no atribuye prueba nueva a un binding legacy aunque el ciclo diga COMPLETE', () => {
|
|
196
|
+
const original = validPlan('a');
|
|
197
|
+
(0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: original });
|
|
198
|
+
const completed = (0, store_1.readJournal)(repo, 'rama').state;
|
|
199
|
+
completed.cycle.status = 'COMPLETE';
|
|
200
|
+
delete completed.cycle.nextAction;
|
|
201
|
+
(0, store_1.writeJournal)(repo, 'rama', completed);
|
|
202
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
203
|
+
expect(() => (0, init_1.rebindWatchPlan)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('b') })).toThrow(/sin prueba/i);
|
|
204
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
205
|
+
expect(fs_1.default.existsSync((0, paths_1.supervisorLockPath)(repo))).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
test('rebind rechaza un ciclo en curso y conserva toda su evidencia ligada al digest anterior', () => {
|
|
208
|
+
(0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('a') });
|
|
209
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
210
|
+
expect(() => (0, init_1.rebindWatchPlan)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('b') })).toThrow(/ciclo.*curso/i);
|
|
211
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
212
|
+
});
|
|
213
|
+
test('rebind rechaza una ruta canónica distinta y deja el journal byte-a-byte intacto', () => {
|
|
214
|
+
(0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('a') });
|
|
215
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
216
|
+
expect(() => (0, init_1.rebindWatchPlan)(repo, 'rama', { path: 'docs/other.md', report: validPlan('b') })).toThrow(/misma ruta/i);
|
|
217
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
218
|
+
});
|
|
219
|
+
test('rebind rechaza un binding ya vigente y no inventa historia duplicada', () => {
|
|
220
|
+
(0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('a') });
|
|
221
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
222
|
+
expect(() => (0, init_1.rebindWatchPlan)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('a') })).toThrow(/ya está vigente/i);
|
|
223
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
224
|
+
});
|
|
225
|
+
test('rebind rechaza evidencia adversa durable y no reescribe el binding obsoleto', () => {
|
|
226
|
+
(0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('a') });
|
|
227
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
228
|
+
state.verdicts.push({ id: 'v-1', obligationId: 'o-1', result: 'fail', detail: 'adverso', receivedAt: new Date().toISOString(), fingerprint: '', argv: [], paths: [], cwd: '.' });
|
|
229
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
230
|
+
expect(() => (0, init_1.rebindWatchPlan)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('b') })).toThrow(/evidencia adversa/i);
|
|
231
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.planBinding.digest).toBe('a'.repeat(64));
|
|
232
|
+
});
|
|
233
|
+
test('rebind rechaza jobs no terminales y conserva el estado recuperable', () => {
|
|
234
|
+
(0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('a') });
|
|
235
|
+
const state = (0, store_1.readJournal)(repo, 'rama').state;
|
|
236
|
+
state.jobs.pending = { id: 'pending', fingerprint: '', commandDigest: '', argv: [], cwd: '.', paths: [], expandedPaths: [], executionState: 'received', observationState: 'progressing', phaseTimestamps: {} };
|
|
237
|
+
(0, store_1.writeJournal)(repo, 'rama', state);
|
|
238
|
+
const before = (0, store_1.readJournal)(repo, 'rama').raw;
|
|
239
|
+
expect(() => (0, init_1.rebindWatchPlan)(repo, 'rama', { path: 'docs/plan.md', report: validPlan('b') })).toThrow(/no terminales/i);
|
|
240
|
+
expect((0, store_1.readJournal)(repo, 'rama').raw).toBe(before);
|
|
241
|
+
expect(fs_1.default.existsSync((0, paths_1.supervisorLockPath)(repo))).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
test('help declara rebind como ruta intencional separada de --init', () => {
|
|
244
|
+
const program = new commander_1.Command();
|
|
245
|
+
(0, watch_1.registerWatchCommand)(program);
|
|
246
|
+
const watch = program.commands.find(command => command.name() === 'watch');
|
|
247
|
+
expect(watch.helpInformation()).toContain('rebind');
|
|
248
|
+
expect(watch.commands.find(command => command.name() === 'rebind').description()).toMatch(/reconcilia/i);
|
|
249
|
+
});
|
|
250
|
+
test('CLI compilado acepta exactamente `watch rebind --plan` y reconcilia el digest', () => {
|
|
251
|
+
const cliRepo = (0, git_fixture_1.initRepo)();
|
|
252
|
+
try {
|
|
253
|
+
const planPath = path_1.default.join(cliRepo, 'docs', 'plan.md');
|
|
254
|
+
fs_1.default.mkdirSync(path_1.default.dirname(planPath), { recursive: true });
|
|
255
|
+
const plan = `**Modo de ejecución:** desatendido\n\n${fs_1.default.readFileSync(path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v1/valid.md'), 'utf8')}`;
|
|
256
|
+
fs_1.default.writeFileSync(planPath, plan);
|
|
257
|
+
fs_1.default.writeFileSync(path_1.default.join(cliRepo, 'source.md'), '## Canonical source\nfixture source\n');
|
|
258
|
+
(0, init_1.initWatch)(cliRepo, 'main', { path: 'docs/plan.md', report: validatePlan(cliRepo, 'docs/plan.md') });
|
|
259
|
+
fs_1.default.appendFileSync(planPath, '<!-- awm-qa-complete: 2026-09-16 -->\n');
|
|
260
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [path_1.default.resolve(__dirname, '../../../dist/src/index.js'), 'watch', 'rebind', '--plan', 'docs/plan.md'], {
|
|
261
|
+
cwd: cliRepo, encoding: 'utf8', env: { ...process.env, AWM_NO_UPDATE_CHECK: '1' },
|
|
262
|
+
});
|
|
263
|
+
expect(result.status).toBe(0);
|
|
264
|
+
expect(result.stderr).not.toContain('required option');
|
|
265
|
+
expect((0, store_1.readJournal)(cliRepo, 'main').state.planBinding.digest).toBe(validatePlan(cliRepo, 'docs/plan.md').planDigest);
|
|
266
|
+
expect((0, store_1.readJournal)(cliRepo, 'main').state.cycle.status).toBe('IN_PROGRESS');
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
fs_1.default.rmSync(cliRepo, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
43
272
|
});
|
|
273
|
+
function validPlan(digestCharacter) {
|
|
274
|
+
return {
|
|
275
|
+
state: 'valid', schema: 'compact-slices/v1', planDigest: digestCharacter.repeat(64),
|
|
276
|
+
executionMode: 'desatendido',
|
|
277
|
+
manifest: { schema: 'compact-slices/v1', planId: 'fixture', requirements: [], sources: [], commands: [], slices: [], closureCommands: [] },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function validatePlan(repo, relativePath) {
|
|
281
|
+
// Keep the external process test tied to the real compact validator, not a
|
|
282
|
+
// hand-built report that could diverge from a lifecycle edit on disk.
|
|
283
|
+
const report = (0, validate_1.validatePlanFile)(relativePath, repo);
|
|
284
|
+
if (report.state !== 'valid')
|
|
285
|
+
throw new Error(`fixture plan unexpectedly invalid: ${JSON.stringify(report)}`);
|
|
286
|
+
return report;
|
|
287
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const admission_1 = require("../../../src/core/admission");
|
|
4
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
5
|
+
const valid = {
|
|
6
|
+
state: 'valid', schema: 'compact-slices/v1', planDigest: 'a'.repeat(64),
|
|
7
|
+
manifest: {
|
|
8
|
+
schema: 'compact-slices/v1', planId: 'admission-fixture', requirements: ['RF-2.2'],
|
|
9
|
+
sources: [], commands: [], closureCommands: [],
|
|
10
|
+
slices: [{ id: 'S1', title: 'one', requirements: ['RF-2.2'], dependsOn: [], sectionAnchor: 's1', sources: [], redCommands: [], greenCommands: [], reviewEvidence: ['specification', 'code-quality'], risk: 'bounded', fallback: [] }],
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
describe('admitPlan', () => {
|
|
14
|
+
it('rejects a forecast whose total or role topology does not match its slices', () => {
|
|
15
|
+
const forecast = { kind: 'topology', slices: 2, roles: { implementer: 2, 'specification-reviewer': 2, 'code-quality-reviewer': 2, 'final-reviewer': 1, 'track-a-qa': 1, 'track-b-qa': 1, documentation: 1, retro: 1, finishing: 1 }, total: 99 };
|
|
16
|
+
expect(() => (0, admission_1.sanitizeAdmissionReport)({ state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-required', diagnostics: [], forecast })).toThrow(/invalid report/);
|
|
17
|
+
forecast.total = 12;
|
|
18
|
+
forecast.roles.implementer = 1;
|
|
19
|
+
expect(() => (0, admission_1.sanitizeAdmissionReport)({ state: 'admitted', planState: 'valid', journal: 'not-required', currentness: 'not-checked', sensors: 'not-required', diagnostics: [], forecast })).toThrow(/invalid report/);
|
|
20
|
+
});
|
|
21
|
+
it('preserves the distinct unsupported plan state and bounded validator diagnostic', async () => {
|
|
22
|
+
const report = await (0, admission_1.admitPlan)({ plan: { state: 'unsupported', schema: 'compact-slices/v9', diagnostics: [{ code: 'PLAN_UNSUPPORTED_SCHEMA', message: `future\u001b${'x'.repeat(5000)}` }] }, provider: 'codex', cwd: process.cwd() });
|
|
23
|
+
expect(report).toMatchObject({ state: 'blocked', planState: 'unsupported', currentness: 'not-checked', sensors: 'not-required' });
|
|
24
|
+
expect(report.diagnostics.map(diagnostic => diagnostic.code)).toEqual(['ADMISSION_PLAN_UNSUPPORTED', 'PLAN_UNSUPPORTED_SCHEMA']);
|
|
25
|
+
expect(report.diagnostics[1].message).toHaveLength(4096);
|
|
26
|
+
expect(report.diagnostics[1].message).not.toMatch(/[\u0000-\u001f\u007f-\u009f]/);
|
|
27
|
+
});
|
|
28
|
+
it('does not block a private registry only when declared plan provenance proves it unconsumed', async () => {
|
|
29
|
+
const report = await (0, admission_1.admitPlan)({
|
|
30
|
+
plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], requireCurrent: true,
|
|
31
|
+
currentness: { checkedAt: '2026-01-01T00:00:00.000Z', compatibility: { status: 'not-checked' }, components: [
|
|
32
|
+
{ component: 'cli', installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'npm', checkedAt: '2026-01-01T00:00:00.000Z', status: 'current', detail: 'ok', remedy: 'none' },
|
|
33
|
+
{ component: 'registry:private', installed: null, latest: null, channel: 'stable', source: 'private', checkedAt: '2026-01-01T00:00:00.000Z', status: 'unverifiable', detail: 'no', remedy: 'none' },
|
|
34
|
+
] },
|
|
35
|
+
consumedRegistryComponents: [], provenance: 'proven',
|
|
36
|
+
});
|
|
37
|
+
expect(report).toMatchObject({ state: 'admitted', currentness: 'current', sensors: 'not-required' });
|
|
38
|
+
});
|
|
39
|
+
it('blocks currentness when registry-contract provenance is not sufficient to prove irrelevance', async () => {
|
|
40
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], requireCurrent: true, currentness: { checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [] } });
|
|
41
|
+
expect(report).toMatchObject({ state: 'blocked', currentness: 'unverifiable', sensors: 'not-required' });
|
|
42
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED');
|
|
43
|
+
});
|
|
44
|
+
it('blocks when a proven consumed registry has no currentness component', async () => {
|
|
45
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], requireCurrent: true, provenance: 'proven', consumedRegistryComponents: ['registry:private'], currentness: { checkedAt: 'x', compatibility: { status: 'not-checked' }, components: [{ component: 'cli', installed: '1.0.0', latest: '1.0.0', channel: 'stable', source: 'npm', checkedAt: 'x', status: 'current', detail: 'ok', remedy: 'none' }] } });
|
|
46
|
+
expect(report).toMatchObject({ state: 'blocked', currentness: 'unverifiable' });
|
|
47
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_CURRENTNESS_MISSING_COMPONENT');
|
|
48
|
+
});
|
|
49
|
+
it('blocks a bogus execution mode instead of treating it as interactive', async () => {
|
|
50
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], executionMode: 'background' });
|
|
51
|
+
expect(report).toMatchObject({ state: 'blocked', executionMode: 'interactivo' });
|
|
52
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_EXECUTION_MODE_INVALID');
|
|
53
|
+
});
|
|
54
|
+
it('stops at an unmarked plan before checking any later boundary', async () => {
|
|
55
|
+
const report = await (0, admission_1.admitPlan)({ plan: { state: 'migration-required', reason: 'unmarked-plan' }, provider: 'codex', cwd: process.cwd() });
|
|
56
|
+
expect(report).toMatchObject({ state: 'blocked', planState: 'migration-required', journal: 'not-required' });
|
|
57
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_PLAN_MIGRATION_REQUIRED');
|
|
58
|
+
expect(report.currentness).toBe('not-checked');
|
|
59
|
+
expect(report.sensors).toBe('not-required');
|
|
60
|
+
});
|
|
61
|
+
it('blocks a disabled provider before remote currentness or sensors', async () => {
|
|
62
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['claude-code'] });
|
|
63
|
+
expect(report).toMatchObject({ state: 'blocked', planDigest: valid.planDigest, provider: 'codex' });
|
|
64
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_PROVIDER_DISABLED');
|
|
65
|
+
expect(report.currentness).toBe('not-checked');
|
|
66
|
+
});
|
|
67
|
+
it('does not let an unverified execution capability admit interactive work', async () => {
|
|
68
|
+
const report = await (0, admission_1.admitPlan)({ plan: valid, provider: 'cursor', cwd: process.cwd(), enabledAgents: ['cursor'] });
|
|
69
|
+
expect(report).toMatchObject({ state: 'blocked', executionMode: 'interactivo', provider: 'cursor', journal: 'not-required' });
|
|
70
|
+
expect(report.diagnostics[0].code).toBe('ADMISSION_CAPABILITY_UNVERIFIED');
|
|
71
|
+
});
|
|
72
|
+
it('reports the schema-2 journal prerequisite for otherwise admissible unattended work', async () => {
|
|
73
|
+
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
|
74
|
+
const report = await (0, admission_1.admitPlan)({ plan: unattended, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'] });
|
|
75
|
+
expect(report).toMatchObject({ state: 'blocked', executionMode: 'desatendido', journal: 'missing' });
|
|
76
|
+
expect(report.diagnostics[0]).toMatchObject({ code: 'ADMISSION_JOURNAL_BINDING_REQUIRED' });
|
|
77
|
+
expect(report.forecast).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
it('admits unattended work only with an exact schema-2 plan binding', async () => {
|
|
80
|
+
const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
|
|
81
|
+
const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
|
|
82
|
+
const admitted = await (0, admission_1.admitPlan)({ plan: unattended, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], journalState: journal, planPath: 'docs/plan.md' });
|
|
83
|
+
expect(admitted).toMatchObject({ state: 'admitted', journal: 'current', executionMode: 'desatendido' });
|
|
84
|
+
const stale = await (0, admission_1.admitPlan)({ plan: unattended, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], journalState: { ...journal, planBinding: { ...journal.planBinding, digest: 'b'.repeat(64) } }, planPath: 'docs/plan.md' });
|
|
85
|
+
expect(stale).toMatchObject({ state: 'blocked', journal: 'stale' });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -41,6 +41,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
41
41
|
const path_1 = __importDefault(require("path"));
|
|
42
42
|
const bundles_1 = require("../../src/core/bundles");
|
|
43
43
|
const bundle_install_1 = require("../../src/core/bundle-install");
|
|
44
|
+
const providers_1 = require("../../src/providers");
|
|
44
45
|
const profile_1 = require("../../src/core/profile");
|
|
45
46
|
const executor_1 = require("../../src/core/executor");
|
|
46
47
|
const executor = __importStar(require("../../src/core/executor"));
|
|
@@ -139,6 +140,41 @@ function makeFixture() {
|
|
|
139
140
|
return { content, projectRoot, bundles: (0, bundles_1.discoverBundles)(content) };
|
|
140
141
|
}
|
|
141
142
|
describe('installBundle', () => {
|
|
143
|
+
it('expands canonical and legacy manifests into byte-identical artifacts for every agent target', () => {
|
|
144
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-binstall-compat-'));
|
|
145
|
+
const content = path_1.default.join(root, 'registry');
|
|
146
|
+
const writeManifests = (legacy) => {
|
|
147
|
+
const skills = (names) => legacy
|
|
148
|
+
? names.map((name, index) => ({ name, onSignal: index % 2 === 0 }))
|
|
149
|
+
: names;
|
|
150
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'bundles/base/bundle.json'), JSON.stringify({
|
|
151
|
+
name: 'base', scope: 'project', dependsOn: [],
|
|
152
|
+
skills: skills(['shared', 'base-only']), workflows: [], agents: [],
|
|
153
|
+
}));
|
|
154
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'bundles/extension/bundle.json'), JSON.stringify({
|
|
155
|
+
name: 'extension', scope: 'project', dependsOn: ['base'],
|
|
156
|
+
skills: skills(['shared', 'extension-only']), workflows: [], agents: [],
|
|
157
|
+
}));
|
|
158
|
+
};
|
|
159
|
+
for (const bundle of ['base', 'extension'])
|
|
160
|
+
fs_1.default.mkdirSync(path_1.default.join(content, 'bundles', bundle), { recursive: true });
|
|
161
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({ bundles: [{ name: 'base', source: './bundles/base' }, { name: 'extension', source: './bundles/extension' }] }));
|
|
162
|
+
writeManifests(false);
|
|
163
|
+
const canonicalWarnings = [];
|
|
164
|
+
const legacyWarnings = [];
|
|
165
|
+
const canonicalReporter = (0, bundles_1.createBundleDiagnosticReporter)((message) => canonicalWarnings.push(message));
|
|
166
|
+
const legacyReporter = (0, bundles_1.createBundleDiagnosticReporter)((message) => legacyWarnings.push(message));
|
|
167
|
+
const canonicalBundles = (0, bundles_1.discoverBundles)(content, canonicalReporter);
|
|
168
|
+
writeManifests(true);
|
|
169
|
+
const legacyBundles = (0, bundles_1.discoverBundles)(content, legacyReporter);
|
|
170
|
+
for (const agent of providers_1.AGENT_TARGETS) {
|
|
171
|
+
const canonicalArtifacts = (0, bundle_install_1.expandBundleArtifacts)({ bundleName: 'extension', bundles: canonicalBundles, agents: [agent], method: 'copy', projectRoot: root, contentDir: content });
|
|
172
|
+
const legacyArtifacts = (0, bundle_install_1.expandBundleArtifacts)({ bundleName: 'extension', bundles: legacyBundles, agents: [agent], method: 'copy', projectRoot: root, contentDir: content });
|
|
173
|
+
expect(Buffer.from(JSON.stringify(canonicalArtifacts))).toEqual(Buffer.from(JSON.stringify(legacyArtifacts)));
|
|
174
|
+
}
|
|
175
|
+
expect(canonicalWarnings).toEqual([]);
|
|
176
|
+
expect(legacyWarnings).toHaveLength(2);
|
|
177
|
+
});
|
|
142
178
|
it('materializes the bundle closure as local symlinks (deps + own skills)', () => {
|
|
143
179
|
const { content, projectRoot, bundles } = makeFixture();
|
|
144
180
|
const result = (0, bundle_install_1.installBundle)({
|