agentic-workflow-manager 6.2.1 → 6.4.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 +17 -0
- package/dist/src/commands/job/gate.js +37 -13
- package/dist/src/commands/job/index.js +48 -8
- package/dist/src/commands/job/request.js +44 -6
- package/dist/src/commands/track/emit.js +26 -0
- package/dist/src/commands/track/index.js +194 -0
- package/dist/src/commands/track/status.js +63 -0
- package/dist/src/commands/track/supervisor-wrapper.js +89 -0
- package/dist/src/commands/watch/apply.js +201 -13
- package/dist/src/commands/watch/index.js +14 -0
- package/dist/src/commands/watch/runner.js +20 -1
- package/dist/src/commands/watch/supervisor.js +251 -27
- package/dist/src/commands/watch/teardown-driver.js +189 -0
- package/dist/src/commands/watch/tracks.js +1100 -0
- package/dist/src/core/journal/adapter.js +46 -6
- package/dist/src/core/journal/paths.js +9 -0
- package/dist/src/core/journal/process.js +101 -1
- package/dist/src/core/journal/requests.js +5 -1
- package/dist/src/core/journal/store.js +34 -2
- package/dist/src/core/journal/types.js +93 -4
- package/dist/src/core/paths.js +51 -0
- package/dist/src/core/tracks/concurrency.js +85 -0
- package/dist/src/core/tracks/context.js +89 -0
- package/dist/src/core/tracks/descriptor.js +40 -0
- package/dist/src/core/tracks/git.js +318 -0
- package/dist/src/core/tracks/join.js +185 -0
- package/dist/src/core/tracks/ownership.js +100 -0
- package/dist/src/core/tracks/plan-parser.js +108 -0
- package/dist/src/core/tracks/protocol.js +466 -0
- package/dist/src/core/tracks/teardown.js +34 -0
- package/dist/src/core/tracks/types.js +14 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
- package/dist/tests/commands/track/fixtures.js +13 -0
- package/dist/tests/commands/track/status.test.js +157 -0
- package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
- package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
- package/dist/tests/commands/track/verbs.test.js +326 -0
- package/dist/tests/commands/watch/apply.test.js +116 -4
- package/dist/tests/commands/watch/runner.test.js +22 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
- package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
- package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
- package/dist/tests/commands/watch/track-finalize.test.js +643 -0
- package/dist/tests/commands/watch/track-freeze.test.js +591 -0
- package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
- package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
- package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
- package/dist/tests/core/journal/adapter-override.test.js +51 -0
- package/dist/tests/core/journal/process.test.js +37 -0
- package/dist/tests/core/journal/requests.test.js +21 -0
- package/dist/tests/core/journal/store.test.js +28 -0
- package/dist/tests/core/journal/types.test.js +89 -0
- package/dist/tests/core/same-existing-path.test.js +48 -0
- package/dist/tests/core/tracks/concurrency.test.js +134 -0
- package/dist/tests/core/tracks/context.test.js +177 -0
- package/dist/tests/core/tracks/descriptor.test.js +76 -0
- package/dist/tests/core/tracks/git.test.js +132 -0
- package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
- package/dist/tests/core/tracks/join.test.js +197 -0
- package/dist/tests/core/tracks/ownership.test.js +96 -0
- package/dist/tests/core/tracks/plan-parser.test.js +94 -0
- package/dist/tests/core/tracks/protocol.test.js +416 -0
- package/dist/tests/core/tracks/teardown.test.js +62 -0
- package/dist/tests/helpers/git-fixture.js +35 -0
- package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
- package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
- package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
- package/dist/tests/structural/sensor-configs-are-present.test.js +49 -0
- package/package.json +1 -1
|
@@ -6,10 +6,16 @@ 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 gate_1 = require("../../../src/commands/job/gate");
|
|
10
12
|
const reconcile_1 = require("../../../src/commands/job/reconcile");
|
|
11
13
|
const reap_1 = require("../../../src/commands/job/reap");
|
|
14
|
+
const job_1 = require("../../../src/commands/job");
|
|
15
|
+
const watch_1 = require("../../../src/commands/watch");
|
|
12
16
|
const types_1 = require("../../../src/core/journal/types");
|
|
17
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
18
|
+
const descriptor_1 = require("../../../src/core/tracks/descriptor");
|
|
13
19
|
function job(partial) {
|
|
14
20
|
return {
|
|
15
21
|
id: 'j1', fingerprint: 'fp', commandDigest: 'cd', argv: ['npm', 'test'], cwd: '.',
|
|
@@ -168,6 +174,137 @@ describe('gate', () => {
|
|
|
168
174
|
expect(g2.pass).toBe(false);
|
|
169
175
|
expect(g2.reasons.some((r) => r.category === 'adverse-verdict')).toBe(true);
|
|
170
176
|
});
|
|
177
|
+
test('computeGate produce el mismo conjunto de categorias que antes de la extraccion de evaluateEvidence (regresion R1)', () => {
|
|
178
|
+
const s = passingState();
|
|
179
|
+
s.cycle.status = 'BLOCKED';
|
|
180
|
+
s.cycle.blockedReason = 'custodia';
|
|
181
|
+
s.cycleVerificationPlan = [];
|
|
182
|
+
s.tasks[0].status = 'pending';
|
|
183
|
+
s.tasks[0].reviewObligations = [];
|
|
184
|
+
s.jobs['live'] = job({ id: 'live', executionState: 'running' });
|
|
185
|
+
const g = (0, gate_1.computeGate)(s, false, fpCurrent);
|
|
186
|
+
const categories = [...new Set(g.reasons.map((r) => r.category))].sort();
|
|
187
|
+
expect(categories).toEqual(['cycle-blocked', 'empty-cycle-plan', 'live-job', 'missing-verifier', 'open-obligation', 'pending-task'].sort());
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
/** Task 12 (R7/C4) — brecha de scope cerrada: el file-list del plan
|
|
191
|
+
* requeria tocar `gate.ts` para el nuevo `VerificationKind` 'track-integration',
|
|
192
|
+
* pero `evaluateEvidence` (arriba, linea ~78-111) ya es generico sobre TODO
|
|
193
|
+
* kind salvo 'review' — cae al mismo lookup `state.jobs[item.satisfiedBy]` +
|
|
194
|
+
* chequeo de verdict/vigencia de fingerprint sin importar el kind. Como
|
|
195
|
+
* `apply.ts` (`linkSatisfies`, llamado una vez por id en el bucle de
|
|
196
|
+
* job-request) enlaza VARIOS items `track-integration:*` al MISMO jobId
|
|
197
|
+
* cuando un solo job canonico de integracion satisface a toda la cohorte
|
|
198
|
+
* (C4 — un unico job, nunca uno por track), el bucle generico por-item ya
|
|
199
|
+
* evalua ese escenario correctamente: cada item hace su propio lookup
|
|
200
|
+
* independiente del MISMO job, así que ambos certifican juntos cuando pasa
|
|
201
|
+
* con fingerprint vigente, y ambos caen a 'stale-fingerprint' juntos cuando
|
|
202
|
+
* ese UNICO job deja de estar vigente. Esta suite documenta esa conclusion
|
|
203
|
+
* con evidencia real — CERO cambios de logica en gate.ts eran necesarios. */
|
|
204
|
+
describe('track-integration: un unico job satisface a MULTIPLES items de cohorte (R7/C4, Task 12 — scope gap)', () => {
|
|
205
|
+
function stateWithSharedIntegrationJob() {
|
|
206
|
+
const s = (0, types_1.emptyState)('r');
|
|
207
|
+
s.requiredVerifiers = ['test', 'sensors'];
|
|
208
|
+
s.cycleVerificationPlan = [
|
|
209
|
+
{ id: 'qa', kind: 'qa', satisfiedBy: 'job-qa' },
|
|
210
|
+
{ id: 'interlock', kind: 'interlock', satisfiedBy: 'job-interlock' },
|
|
211
|
+
{ id: 'test', kind: 'test', satisfiedBy: 'job-test' },
|
|
212
|
+
{ id: 'sensors', kind: 'sensors', satisfiedBy: 'job-sensors' },
|
|
213
|
+
// Dos tracks ('a', 'b'), UN solo job canonico de integracion
|
|
214
|
+
// (`job-integration`) satisface a AMBOS items — exactamente lo
|
|
215
|
+
// que `runRequestFinalIntegration` (watch/tracks.ts) produce vía
|
|
216
|
+
// `requestJob({ satisfies: ids, ... })` con `ids` = el conjunto
|
|
217
|
+
// COMPLETO y ordenado de `track-integration:*` de la cohorte.
|
|
218
|
+
{ id: 'track-integration:a', kind: 'track-integration', satisfiedBy: 'job-integration' },
|
|
219
|
+
{ id: 'track-integration:b', kind: 'track-integration', satisfiedBy: 'job-integration' },
|
|
220
|
+
];
|
|
221
|
+
for (const id of ['job-qa', 'job-interlock', 'job-test', 'job-sensors', 'job-integration']) {
|
|
222
|
+
s.jobs[id] = job({ id, fingerprint: 'fp', executionState: 'exited', verdict: 'pass' });
|
|
223
|
+
}
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
226
|
+
test('un job pasando con fingerprint vigente certifica AMBOS items de track-integration a la vez, sin cambios en gate.ts', () => {
|
|
227
|
+
const s = stateWithSharedIntegrationJob();
|
|
228
|
+
const g = (0, gate_1.computeGate)(s, false, fpCurrent);
|
|
229
|
+
expect(g.pass).toBe(true);
|
|
230
|
+
expect(g.reasons).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
test('cuando ESE UNICO job pierde vigencia, AMBOS items de track-integration caen a stale-fingerprint juntos (nunca uno sin el otro)', () => {
|
|
233
|
+
const s = stateWithSharedIntegrationJob();
|
|
234
|
+
const g = (0, gate_1.computeGate)(s, false, fpStale);
|
|
235
|
+
expect(g.pass).toBe(false);
|
|
236
|
+
const staleTrackIntegration = g.reasons.filter((r) => r.category === 'stale-fingerprint' && /track-integration:/.test(r.detail));
|
|
237
|
+
expect(staleTrackIntegration).toHaveLength(2);
|
|
238
|
+
expect(staleTrackIntegration.some((r) => /track-integration:a/.test(r.detail))).toBe(true);
|
|
239
|
+
expect(staleTrackIntegration.some((r) => /track-integration:b/.test(r.detail))).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
test('si el job compartido falla (verdict no-pass), AMBOS items de track-integration bloquean por adverse-verdict', () => {
|
|
242
|
+
const s = stateWithSharedIntegrationJob();
|
|
243
|
+
s.jobs['job-integration'].verdict = 'fail';
|
|
244
|
+
const g = (0, gate_1.computeGate)(s, false, fpCurrent);
|
|
245
|
+
expect(g.pass).toBe(false);
|
|
246
|
+
const adverse = g.reasons.filter((r) => r.category === 'adverse-verdict' && /track-integration:/.test(r.detail));
|
|
247
|
+
expect(adverse).toHaveLength(2);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
/** Estado de journal de un TRACK individual (C6, R3.5): trackContext con la
|
|
251
|
+
* tarea asignada ya `done`, cycleVerificationPlan VACIO (un track no exige
|
|
252
|
+
* QA/interlock de ambito de plan), y evidencia local completa (item de
|
|
253
|
+
* test/sensors satisfecho, obligaciones spec/quality con verdict pass) —
|
|
254
|
+
* todo con el MISMO fingerprint que el gate va a recomputar ('same'). */
|
|
255
|
+
function stateWithCompletedTrackTask() {
|
|
256
|
+
const s = (0, types_1.emptyState)('track/cli');
|
|
257
|
+
s.trackContext = { trackId: 'cli', taskIds: ['T1'], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
258
|
+
s.requiredVerifiers = ['test', 'sensors'];
|
|
259
|
+
s.tasks.push({
|
|
260
|
+
id: 'T1', title: 't', status: 'done', attempts: 1,
|
|
261
|
+
verificationPlan: [
|
|
262
|
+
{ id: 'v1', kind: 'test', satisfiedBy: 'j1' },
|
|
263
|
+
{ id: 'v-sensors', kind: 'sensors', satisfiedBy: 'j2' },
|
|
264
|
+
],
|
|
265
|
+
reviewObligations: [
|
|
266
|
+
{ id: 'o-spec', taskId: 'T1', kind: 'spec', verdictId: 'verd-spec' },
|
|
267
|
+
{ id: 'o-quality', taskId: 'T1', kind: 'quality', verdictId: 'verd-quality' },
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
s.jobs['j1'] = job({ id: 'j1', fingerprint: 'same', executionState: 'exited', verdict: 'pass' });
|
|
271
|
+
s.jobs['j2'] = job({ id: 'j2', fingerprint: 'same', executionState: 'exited', verdict: 'pass' });
|
|
272
|
+
s.verdicts.push({ id: 'verd-spec', obligationId: 'o-spec', result: 'pass', detail: 'ok', receivedAt: 'now', fingerprint: 'same', argv: ['review', 'o-spec'], paths: [], cwd: '.' }, { id: 'verd-quality', obligationId: 'o-quality', result: 'pass', detail: 'ok', receivedAt: 'now', fingerprint: 'same', argv: ['review', 'o-quality'], paths: [], cwd: '.' });
|
|
273
|
+
return s;
|
|
274
|
+
}
|
|
275
|
+
describe('computeTrackGate — gate local de un track (C6, R3.5)', () => {
|
|
276
|
+
test('computeTrackGate no exige qa/interlock global (R3.5, C6)', () => {
|
|
277
|
+
const s = stateWithCompletedTrackTask();
|
|
278
|
+
expect((0, gate_1.computeTrackGate)(s, false, () => 'same')).toEqual({ pass: true, reasons: [] });
|
|
279
|
+
expect((0, gate_1.computeGate)(s, false, () => 'same').pass).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
test('corrupcion o journal ausente bloquea con categoria propia (corrupt-state)', () => {
|
|
282
|
+
const g = (0, gate_1.computeTrackGate)(null, true, () => 'same');
|
|
283
|
+
expect(g.pass).toBe(false);
|
|
284
|
+
expect(g.reasons).toEqual([{ category: 'corrupt-state', detail: expect.any(String) }]);
|
|
285
|
+
});
|
|
286
|
+
test('journal sin trackContext no es un journal de track: bloquea con wrong-context', () => {
|
|
287
|
+
const s = stateWithCompletedTrackTask();
|
|
288
|
+
delete s.trackContext;
|
|
289
|
+
const g = (0, gate_1.computeTrackGate)(s, false, () => 'same');
|
|
290
|
+
expect(g.pass).toBe(false);
|
|
291
|
+
expect(g.reasons).toEqual([{ category: 'wrong-context', detail: expect.any(String) }]);
|
|
292
|
+
});
|
|
293
|
+
test('una tarea ajena colada en el journal del track se rechaza, jamas certifica por ella (R2.3)', () => {
|
|
294
|
+
const s = stateWithCompletedTrackTask();
|
|
295
|
+
s.tasks.push({ id: 'ajena', title: 'x', status: 'done', attempts: 1, verificationPlan: [], reviewObligations: [] });
|
|
296
|
+
const g = (0, gate_1.computeTrackGate)(s, false, () => 'same');
|
|
297
|
+
expect(g.pass).toBe(false);
|
|
298
|
+
expect(g.reasons).toEqual([{ category: 'foreign-task', detail: expect.stringContaining('ajena') }]);
|
|
299
|
+
});
|
|
300
|
+
test('sigue exigiendo verificadores mecanicos y fingerprint vigente localmente (no es un gate vacio)', () => {
|
|
301
|
+
const s = stateWithCompletedTrackTask();
|
|
302
|
+
s.jobs['j1'].fingerprint = 'cambiado';
|
|
303
|
+
expect((0, gate_1.computeTrackGate)(s, false, () => 'same').reasons.some((r) => r.category === 'stale-fingerprint')).toBe(true);
|
|
304
|
+
const s2 = stateWithCompletedTrackTask();
|
|
305
|
+
s2.requiredVerifiers = ['test', 'sensors', 'lint'];
|
|
306
|
+
expect((0, gate_1.computeTrackGate)(s2, false, () => 'same').reasons.some((r) => r.category === 'missing-verifier' && /lint/.test(r.detail))).toBe(true);
|
|
307
|
+
});
|
|
171
308
|
});
|
|
172
309
|
describe('reconcile — matriz unica R1.8 (R3.3)', () => {
|
|
173
310
|
let logs;
|
|
@@ -321,3 +458,133 @@ describe('reap — limpieza explicita con identidad validada (R2.2)', () => {
|
|
|
321
458
|
child.kill('SIGKILL');
|
|
322
459
|
});
|
|
323
460
|
});
|
|
461
|
+
// El guard de contexto (R9.4) se prueba a nivel de comando REGISTRADO: `job
|
|
462
|
+
// gate` y `watch --init` deben rechazar un cwd cuyo descriptor no autentica,
|
|
463
|
+
// y deben proceder EXACTAMENTE igual que antes cuando no hay descriptor
|
|
464
|
+
// (caso comun) o cuando el descriptor si autentica.
|
|
465
|
+
class ExitSignal extends Error {
|
|
466
|
+
code;
|
|
467
|
+
constructor(code) {
|
|
468
|
+
super(`process.exit(${code})`);
|
|
469
|
+
this.code = code;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function gitInitAt(repo, branch) {
|
|
473
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', branch], { cwd: repo });
|
|
474
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
475
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: repo });
|
|
476
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'c'], { cwd: repo });
|
|
477
|
+
}
|
|
478
|
+
function trackRefFixture(overrides, worktreePath) {
|
|
479
|
+
return {
|
|
480
|
+
trackId: 'cli', worktreePath, branch: 'track/cli', ownership: [], sharedResources: [], dependsOn: [],
|
|
481
|
+
fencingToken: 'f'.repeat(32), phase: 'ACTIVE', readinessNonce: 'n'.repeat(8), ...overrides,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function descriptorFixture(overrides, planRoot) {
|
|
485
|
+
return { schema: 1, planRoot, planBranch: 'main', trackId: 'cli', planJournalId: 'j-1', fencingToken: 'f'.repeat(32), ...overrides };
|
|
486
|
+
}
|
|
487
|
+
async function runCommand(register, argv, cwd) {
|
|
488
|
+
const prog = new commander_1.Command();
|
|
489
|
+
prog.exitOverride();
|
|
490
|
+
register(prog);
|
|
491
|
+
const outSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
492
|
+
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
493
|
+
const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(cwd);
|
|
494
|
+
let exitCode = null;
|
|
495
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
496
|
+
exitCode = code ?? 0;
|
|
497
|
+
throw new ExitSignal(exitCode);
|
|
498
|
+
}));
|
|
499
|
+
try {
|
|
500
|
+
await prog.parseAsync(['node', 'awm', ...argv]);
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
if (!(e instanceof ExitSignal))
|
|
504
|
+
throw e;
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
cwdSpy.mockRestore();
|
|
508
|
+
exitSpy.mockRestore();
|
|
509
|
+
}
|
|
510
|
+
const out = outSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
511
|
+
const err = errSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
512
|
+
outSpy.mockRestore();
|
|
513
|
+
errSpy.mockRestore();
|
|
514
|
+
return { out, err, exitCode };
|
|
515
|
+
}
|
|
516
|
+
const runJob = (argv, cwd) => runCommand(job_1.registerJobCommand, ['job', ...argv], cwd);
|
|
517
|
+
const runWatch = (argv, cwd) => runCommand(watch_1.registerWatchCommand, ['watch', ...argv], cwd);
|
|
518
|
+
describe('guard de contexto en awm job / awm watch (R9.4)', () => {
|
|
519
|
+
let planRoot;
|
|
520
|
+
let trackRoot;
|
|
521
|
+
let otherRoot;
|
|
522
|
+
beforeEach(() => {
|
|
523
|
+
planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-guard-plan-'));
|
|
524
|
+
trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-guard-track-'));
|
|
525
|
+
otherRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-guard-other-'));
|
|
526
|
+
gitInitAt(planRoot, 'main');
|
|
527
|
+
gitInitAt(trackRoot, 'track/cli');
|
|
528
|
+
gitInitAt(otherRoot, 'track/cli');
|
|
529
|
+
(0, store_1.initJournal)(planRoot, 'main');
|
|
530
|
+
const planState = (0, store_1.readJournal)(planRoot, 'main').state;
|
|
531
|
+
planState.journalId = 'j-1';
|
|
532
|
+
planState.tracks = [trackRefFixture({ fencingToken: 'f'.repeat(32) }, trackRoot)];
|
|
533
|
+
(0, store_1.writeJournal)(planRoot, 'main', planState);
|
|
534
|
+
(0, store_1.initJournal)(trackRoot, 'track/cli');
|
|
535
|
+
const trackState = (0, store_1.readJournal)(trackRoot, 'track/cli').state;
|
|
536
|
+
trackState.trackContext = { trackId: 'cli', taskIds: [], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
537
|
+
(0, store_1.writeJournal)(trackRoot, 'track/cli', trackState);
|
|
538
|
+
(0, descriptor_1.writeDescriptor)(trackRoot, descriptorFixture({ planJournalId: 'j-1', fencingToken: 'f'.repeat(32) }, planRoot));
|
|
539
|
+
// otherRoot: mismo descriptor (mismo plan/trackId/fencing) pero NO es
|
|
540
|
+
// el worktree que el TrackRef del plan declara — su realpath jamas
|
|
541
|
+
// puede autenticar (R9.4), aunque el resto del descriptor "calce".
|
|
542
|
+
(0, store_1.initJournal)(otherRoot, 'track/cli');
|
|
543
|
+
const otherState = (0, store_1.readJournal)(otherRoot, 'track/cli').state;
|
|
544
|
+
otherState.trackContext = { trackId: 'cli', taskIds: [], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
545
|
+
(0, store_1.writeJournal)(otherRoot, 'track/cli', otherState);
|
|
546
|
+
(0, descriptor_1.writeDescriptor)(otherRoot, descriptorFixture({ planJournalId: 'j-1', fencingToken: 'f'.repeat(32) }, planRoot));
|
|
547
|
+
});
|
|
548
|
+
afterEach(() => {
|
|
549
|
+
for (const dir of [planRoot, trackRoot, otherRoot])
|
|
550
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
551
|
+
});
|
|
552
|
+
test('`job gate` desde un cwd cuyo descriptor no autentica (realpath no coincide con el TrackRef) rechaza con "cwd no autenticado"', async () => {
|
|
553
|
+
const { err, exitCode } = await runJob(['gate'], otherRoot);
|
|
554
|
+
expect(exitCode).toBe(1);
|
|
555
|
+
expect(err).toContain('cwd no autenticado');
|
|
556
|
+
});
|
|
557
|
+
test('`job gate` desde el cwd autenticado (trackRoot) atraviesa el guard sin bloquear por autenticacion', async () => {
|
|
558
|
+
const { out, err, exitCode } = await runJob(['gate'], trackRoot);
|
|
559
|
+
// el guard NO bloqueo (nunca imprime "cwd no autenticado"); computeGate
|
|
560
|
+
// (todavia global, sin distinguir modo track — eso es de otra task)
|
|
561
|
+
// bloquea por otras razones, lo cual es esperado y no es objeto de este test.
|
|
562
|
+
expect(err).not.toContain('cwd no autenticado');
|
|
563
|
+
expect(exitCode).toBe(1);
|
|
564
|
+
expect(JSON.parse(out).pass).toBe(false);
|
|
565
|
+
});
|
|
566
|
+
test('`watch --init` desde un cwd SIN descriptor (repo comun, sin tracks) procede exactamente como antes de esta task', async () => {
|
|
567
|
+
const plainRepo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-guard-plain-'));
|
|
568
|
+
try {
|
|
569
|
+
gitInitAt(plainRepo, 'main');
|
|
570
|
+
const { out, err, exitCode } = await runWatch(['--init'], plainRepo);
|
|
571
|
+
expect(exitCode).toBeNull();
|
|
572
|
+
expect(err).toBe('');
|
|
573
|
+
expect(out).toContain('journal inicializado');
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
fs_1.default.rmSync(plainRepo, { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
test('`watch --init` desde el cwd autenticado (trackRoot) procede con normalidad', async () => {
|
|
580
|
+
const { out, err, exitCode } = await runWatch(['--init'], trackRoot);
|
|
581
|
+
expect(err).not.toContain('cwd no autenticado');
|
|
582
|
+
expect(exitCode).toBeNull();
|
|
583
|
+
expect(out).toContain('journal inicializado');
|
|
584
|
+
});
|
|
585
|
+
test('`watch --init` desde un cwd cuyo descriptor no autentica rechaza con "cwd no autenticado"', async () => {
|
|
586
|
+
const { err, exitCode } = await runWatch(['--init'], otherRoot);
|
|
587
|
+
expect(exitCode).toBe(1);
|
|
588
|
+
expect(err).toContain('cwd no autenticado');
|
|
589
|
+
});
|
|
590
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackRefFixture = trackRefFixture;
|
|
4
|
+
exports.descriptorFixture = descriptorFixture;
|
|
5
|
+
function trackRefFixture(overrides, worktreePath) {
|
|
6
|
+
return {
|
|
7
|
+
trackId: 'cli', worktreePath, branch: 'track/cli', ownership: [], sharedResources: [], dependsOn: [],
|
|
8
|
+
fencingToken: 'f'.repeat(32), phase: 'ACTIVE', readinessNonce: 'n'.repeat(8), ...overrides,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function descriptorFixture(overrides, planRoot) {
|
|
12
|
+
return { schema: 1, planRoot, planBranch: 'main', trackId: 'cli', planJournalId: 'j-1', fencingToken: 'f'.repeat(32), ...overrides };
|
|
13
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const status_1 = require("../../../src/commands/track/status");
|
|
11
|
+
const fingerprint_1 = require("../../../src/core/journal/fingerprint");
|
|
12
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
13
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
14
|
+
const fixtures_1 = require("./fixtures");
|
|
15
|
+
// R9.5/R9.6: `aggregateTrackStatus` COMPONE el gate de cada journal de track
|
|
16
|
+
// leyendolo al momento de necesitarlo — jamas escribe de vuelta al journal
|
|
17
|
+
// del plan ni persiste el resultado en el TrackRef.
|
|
18
|
+
function git(cwd, ...args) {
|
|
19
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd });
|
|
20
|
+
}
|
|
21
|
+
function gitInitAt(repo, branch) {
|
|
22
|
+
fs_1.default.mkdirSync(repo, { recursive: true });
|
|
23
|
+
git(repo, 'init', '-q', '-b', branch);
|
|
24
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
25
|
+
git(repo, 'add', '.');
|
|
26
|
+
git(repo, 'commit', '-qm', 'c');
|
|
27
|
+
}
|
|
28
|
+
describe('aggregateTrackStatus — agregado read-only (R9.5, R9.6)', () => {
|
|
29
|
+
let planRoot;
|
|
30
|
+
let trackRoot;
|
|
31
|
+
const planBranch = 'main';
|
|
32
|
+
const trackBranch = 'track/cli';
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-plan-'));
|
|
35
|
+
trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-track-'));
|
|
36
|
+
gitInitAt(planRoot, planBranch);
|
|
37
|
+
gitInitAt(trackRoot, trackBranch);
|
|
38
|
+
(0, store_1.initJournal)(planRoot, planBranch);
|
|
39
|
+
(0, store_1.initJournal)(trackRoot, trackBranch);
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
fs_1.default.rmSync(planRoot, { recursive: true, force: true });
|
|
43
|
+
fs_1.default.rmSync(trackRoot, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
/** Journal de track que satisface computeTrackGate integramente, con
|
|
46
|
+
* fingerprints REALES de `trackRoot` (fingerprintFor usa computeFingerprint
|
|
47
|
+
* de verdad, no un stub inyectado). */
|
|
48
|
+
function writeCompletedTrackJournal() {
|
|
49
|
+
const fp = (0, fingerprint_1.computeFingerprint)(trackRoot, ['npm', 'test'], [], '.');
|
|
50
|
+
const s = (0, store_1.readJournal)(trackRoot, trackBranch).state;
|
|
51
|
+
s.trackContext = { trackId: 'cli', taskIds: ['T1'], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
52
|
+
s.requiredVerifiers = ['test', 'sensors'];
|
|
53
|
+
s.tasks.push({
|
|
54
|
+
id: 'T1', title: 't', status: 'done', attempts: 1,
|
|
55
|
+
verificationPlan: [
|
|
56
|
+
{ id: 'v1', kind: 'test', satisfiedBy: 'j1' },
|
|
57
|
+
{ id: 'v-sensors', kind: 'sensors', satisfiedBy: 'j2' },
|
|
58
|
+
],
|
|
59
|
+
reviewObligations: [
|
|
60
|
+
{ id: 'o-spec', taskId: 'T1', kind: 'spec', verdictId: 'verd-spec' },
|
|
61
|
+
{ id: 'o-quality', taskId: 'T1', kind: 'quality', verdictId: 'verd-quality' },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
const job = (id) => ({
|
|
65
|
+
id, fingerprint: fp.fingerprint, commandDigest: fp.commandDigest, argv: ['npm', 'test'], cwd: '.',
|
|
66
|
+
paths: [], expandedPaths: fp.expandedPaths, executionState: 'exited',
|
|
67
|
+
observationState: 'progressing', verdict: 'pass', phaseTimestamps: {},
|
|
68
|
+
});
|
|
69
|
+
s.jobs['j1'] = job('j1');
|
|
70
|
+
s.jobs['j2'] = job('j2');
|
|
71
|
+
s.verdicts.push({ id: 'verd-spec', obligationId: 'o-spec', result: 'pass', detail: 'ok', receivedAt: 'now', fingerprint: fp.fingerprint, argv: ['npm', 'test'], paths: [], cwd: '.' }, { id: 'verd-quality', obligationId: 'o-quality', result: 'pass', detail: 'ok', receivedAt: 'now', fingerprint: fp.fingerprint, argv: ['npm', 'test'], paths: [], cwd: '.' });
|
|
72
|
+
(0, store_1.writeJournal)(trackRoot, trackBranch, s);
|
|
73
|
+
}
|
|
74
|
+
test('compone journals al leer y no los espeja (R9.5, R9.6)', () => {
|
|
75
|
+
writeCompletedTrackJournal();
|
|
76
|
+
const planState = (0, store_1.readJournal)(planRoot, planBranch).state;
|
|
77
|
+
planState.tracks = [(0, fixtures_1.trackRefFixture)({}, trackRoot)];
|
|
78
|
+
(0, store_1.writeJournal)(planRoot, planBranch, planState);
|
|
79
|
+
const planBefore = fs_1.default.readFileSync((0, paths_1.statePath)(planRoot, planBranch), 'utf8');
|
|
80
|
+
const currentPlanState = (0, store_1.readJournal)(planRoot, planBranch).state;
|
|
81
|
+
const out = (0, status_1.aggregateTrackStatus)(planRoot, currentPlanState);
|
|
82
|
+
expect(out.tracks.cli.gate.pass).toBe(true);
|
|
83
|
+
expect(currentPlanState.tracks[0]).not.toHaveProperty('gate');
|
|
84
|
+
expect(fs_1.default.readFileSync((0, paths_1.statePath)(planRoot, planBranch), 'utf8')).toBe(planBefore);
|
|
85
|
+
});
|
|
86
|
+
test('journal de track corrupto/ausente nunca se descarta en silencio: gate rojo con corrupt-state', () => {
|
|
87
|
+
// ni siquiera se inicializa el journal del segundo track: readJournal
|
|
88
|
+
// sobre un directorio sin state.json => corrupt:true (R1.6).
|
|
89
|
+
const missingTrackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-missing-'));
|
|
90
|
+
try {
|
|
91
|
+
const planState = (0, store_1.readJournal)(planRoot, planBranch).state;
|
|
92
|
+
planState.tracks = [(0, fixtures_1.trackRefFixture)({ trackId: 'ghost', branch: 'track/ghost' }, missingTrackRoot)];
|
|
93
|
+
(0, store_1.writeJournal)(planRoot, planBranch, planState);
|
|
94
|
+
const out = (0, status_1.aggregateTrackStatus)(planRoot, (0, store_1.readJournal)(planRoot, planBranch).state);
|
|
95
|
+
expect(out.tracks.ghost.gate.pass).toBe(false);
|
|
96
|
+
expect(out.tracks.ghost.gate.reasons).toEqual([{ category: 'corrupt-state', detail: expect.any(String) }]);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
fs_1.default.rmSync(missingTrackRoot, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
test('journal de track roto (fingerprint stale) reporta gate rojo, no lo esconde', () => {
|
|
103
|
+
writeCompletedTrackJournal();
|
|
104
|
+
// el worktree del track cambia DESPUES de que el job registro su
|
|
105
|
+
// fingerprint: la evidencia queda historica.
|
|
106
|
+
fs_1.default.writeFileSync(path_1.default.join(trackRoot, 'f.txt'), 'cambiado');
|
|
107
|
+
const planState = (0, store_1.readJournal)(planRoot, planBranch).state;
|
|
108
|
+
planState.tracks = [(0, fixtures_1.trackRefFixture)({}, trackRoot)];
|
|
109
|
+
(0, store_1.writeJournal)(planRoot, planBranch, planState);
|
|
110
|
+
const out = (0, status_1.aggregateTrackStatus)(planRoot, (0, store_1.readJournal)(planRoot, planBranch).state);
|
|
111
|
+
expect(out.tracks.cli.gate.pass).toBe(false);
|
|
112
|
+
expect(out.tracks.cli.gate.reasons.some((r) => r.category === 'stale-fingerprint')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
test('sin tracks declarados, el agregado es vacio y el cohort lo dice explicitamente', () => {
|
|
115
|
+
const out = (0, status_1.aggregateTrackStatus)(planRoot, (0, store_1.readJournal)(planRoot, planBranch).state);
|
|
116
|
+
expect(out).toEqual({ cohort: 'sin tracks declarados', tracks: {} });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('deriveCohortPhase — string de display, no decide protocolo', () => {
|
|
120
|
+
function ref(trackId, phase) {
|
|
121
|
+
return (0, fixtures_1.trackRefFixture)({ trackId, phase }, `/tmp/${trackId}`);
|
|
122
|
+
}
|
|
123
|
+
test('sin tracks: mensaje explicito', () => {
|
|
124
|
+
expect((0, status_1.deriveCohortPhase)([])).toBe('sin tracks declarados');
|
|
125
|
+
});
|
|
126
|
+
test('cualquier BLOCKED domina el mensaje, nombrando los ids ordenados', () => {
|
|
127
|
+
const out = (0, status_1.deriveCohortPhase)([ref('b', 'ACTIVE'), ref('a', 'BLOCKED'), ref('c', 'BLOCKED')]);
|
|
128
|
+
expect(out).toBe('BLOCKED: a, c');
|
|
129
|
+
});
|
|
130
|
+
test('sin BLOCKED, reporta la fase MENOS avanzada como cuello de botella', () => {
|
|
131
|
+
// ARMED antecede a ACTIVE en TRACK_PHASES: el track menos avanzado
|
|
132
|
+
// marca el ritmo de la cohorte, no el mas adelantado.
|
|
133
|
+
const out = (0, status_1.deriveCohortPhase)([ref('a', 'ACTIVE'), ref('b', 'ARMED')]);
|
|
134
|
+
expect(out).toBe('ARMED (1/2)');
|
|
135
|
+
});
|
|
136
|
+
test('todos en la misma fase: cuenta el total', () => {
|
|
137
|
+
expect((0, status_1.deriveCohortPhase)([ref('a', 'ACTIVE'), ref('b', 'ACTIVE')])).toBe('ACTIVE (2/2)');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('fingerprintFor — espejo de realFingerprintNow cerrado sobre el worktree observado', () => {
|
|
141
|
+
let worktree;
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
worktree = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-fpfor-'));
|
|
144
|
+
gitInitAt(worktree, 'main');
|
|
145
|
+
});
|
|
146
|
+
afterEach(() => { fs_1.default.rmSync(worktree, { recursive: true, force: true }); });
|
|
147
|
+
test('coincide con computeFingerprint contra el MISMO worktree que cierra', () => {
|
|
148
|
+
const expected = (0, fingerprint_1.computeFingerprint)(worktree, ['npm', 'test'], [], '.').fingerprint;
|
|
149
|
+
expect((0, status_1.fingerprintFor)(worktree)(['npm', 'test'], [], '.')).toBe(expected);
|
|
150
|
+
});
|
|
151
|
+
test('argv invalido (vacio) nunca lanza — se degrada a null, nunca certifica', () => {
|
|
152
|
+
expect((0, status_1.fingerprintFor)(worktree)([], [], '.')).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
test('cwd fuera del repo tambien se degrada a null en vez de propagar la excepcion', () => {
|
|
155
|
+
expect((0, status_1.fingerprintFor)(worktree)(['npm', 'test'], [], '../fuera')).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
// Task 8 post-review item 3: CLI-level coverage de `awm track supervisor-wrapper`
|
|
7
|
+
// — el registro de flags requeridos y el guard de descriptor, invocados vía
|
|
8
|
+
// Commander (mismo patrón que verbs.test.ts), no llamando a
|
|
9
|
+
// `runSupervisorWrapper` directamente. El único escenario que llega a
|
|
10
|
+
// ejecutar el wrapper real deja el track BLOCKED en el journal del plan para
|
|
11
|
+
// que retorne sin nunca spawnear `awm watch` de verdad.
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
const commander_1 = require("commander");
|
|
17
|
+
const track_1 = require("../../../src/commands/track");
|
|
18
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
19
|
+
const descriptor_1 = require("../../../src/core/tracks/descriptor");
|
|
20
|
+
const fixtures_1 = require("./fixtures");
|
|
21
|
+
class ExitSignal extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(code) {
|
|
24
|
+
super(`process.exit(${code})`);
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function git(repo, args) {
|
|
29
|
+
return (0, child_process_1.execFileSync)('git', args, { cwd: repo, encoding: 'utf8' }).trim();
|
|
30
|
+
}
|
|
31
|
+
function gitInit(repo, branch) {
|
|
32
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', branch], { cwd: repo });
|
|
33
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
34
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: repo });
|
|
35
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'c'], { cwd: repo });
|
|
36
|
+
}
|
|
37
|
+
async function runCli(repo, argv) {
|
|
38
|
+
const prog = new commander_1.Command();
|
|
39
|
+
prog.exitOverride();
|
|
40
|
+
(0, track_1.registerTrackCommand)(prog);
|
|
41
|
+
const outSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
42
|
+
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
43
|
+
const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(repo);
|
|
44
|
+
let exitCode = 0;
|
|
45
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
46
|
+
exitCode = code ?? 0;
|
|
47
|
+
throw new ExitSignal(exitCode);
|
|
48
|
+
}));
|
|
49
|
+
try {
|
|
50
|
+
await prog.parseAsync(['node', 'awm', ...argv]);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (e instanceof ExitSignal) {
|
|
54
|
+
// ya capturado por el spy de process.exit
|
|
55
|
+
}
|
|
56
|
+
else if (typeof e.exitCode === 'number') {
|
|
57
|
+
exitCode = e.exitCode;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
cwdSpy.mockRestore();
|
|
65
|
+
exitSpy.mockRestore();
|
|
66
|
+
}
|
|
67
|
+
const out = outSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
68
|
+
const err = errSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
69
|
+
outSpy.mockRestore();
|
|
70
|
+
errSpy.mockRestore();
|
|
71
|
+
return { out, err, exitCode };
|
|
72
|
+
}
|
|
73
|
+
describe('awm track supervisor-wrapper — CLI (R4.7, R9.4, post-review item 3)', () => {
|
|
74
|
+
let planRoot;
|
|
75
|
+
let trackRoot;
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrapper-cli-plan-'));
|
|
78
|
+
trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrapper-cli-track-'));
|
|
79
|
+
gitInit(planRoot, 'main');
|
|
80
|
+
gitInit(trackRoot, 'track/cli');
|
|
81
|
+
(0, store_1.initJournal)(planRoot, 'main');
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
fs_1.default.rmSync(planRoot, { recursive: true, force: true });
|
|
85
|
+
fs_1.default.rmSync(trackRoot, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
test.each([
|
|
88
|
+
['sin --track', ['track', 'supervisor-wrapper', '--readiness', 'r', '--fence', 'f', '--nonce', 'n']],
|
|
89
|
+
['sin --readiness', ['track', 'supervisor-wrapper', '--track', 'cli', '--fence', 'f', '--nonce', 'n']],
|
|
90
|
+
['sin --fence', ['track', 'supervisor-wrapper', '--track', 'cli', '--readiness', 'r', '--nonce', 'n']],
|
|
91
|
+
['sin --nonce', ['track', 'supervisor-wrapper', '--track', 'cli', '--readiness', 'r', '--fence', 'f']],
|
|
92
|
+
])('%s: commander rechaza antes de tocar ningún descriptor/journal', async (_label, argv) => {
|
|
93
|
+
const out = await runCli(trackRoot, argv);
|
|
94
|
+
expect(out.exitCode).not.toBe(0);
|
|
95
|
+
});
|
|
96
|
+
test('sin descriptor de track en el cwd: rechaza sin autenticar (R9.4)', async () => {
|
|
97
|
+
const out = await runCli(trackRoot, ['track', 'supervisor-wrapper', '--track', 'cli', '--readiness', 'r', '--fence', 'f', '--nonce', 'n']);
|
|
98
|
+
expect(out.exitCode).toBe(1);
|
|
99
|
+
expect(out.err).toContain('sin descriptor de track');
|
|
100
|
+
});
|
|
101
|
+
test('descriptor presente pero trackId no coincide con --track: aborta antes de reclamar nada', async () => {
|
|
102
|
+
(0, descriptor_1.writeDescriptor)(trackRoot, (0, fixtures_1.descriptorFixture)({ trackId: 'cli' }, planRoot));
|
|
103
|
+
const out = await runCli(trackRoot, ['track', 'supervisor-wrapper', '--track', 'otro-track', '--readiness', 'r', '--fence', 'f'.repeat(32), '--nonce', 'n']);
|
|
104
|
+
expect(out.exitCode).toBe(1);
|
|
105
|
+
expect(out.err).toContain('no coincide');
|
|
106
|
+
expect(fs_1.default.existsSync(path_1.default.join(trackRoot, '.awm', 'supervisor.claim'))).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
test('descriptor presente pero fencingToken no coincide con --fence: aborta antes de reclamar nada', async () => {
|
|
109
|
+
(0, descriptor_1.writeDescriptor)(trackRoot, (0, fixtures_1.descriptorFixture)({ trackId: 'cli', fencingToken: 'f'.repeat(32) }, planRoot));
|
|
110
|
+
const out = await runCli(trackRoot, ['track', 'supervisor-wrapper', '--track', 'cli', '--readiness', 'r', '--fence', 'g'.repeat(32), '--nonce', 'n']);
|
|
111
|
+
expect(out.exitCode).toBe(1);
|
|
112
|
+
expect(out.err).toContain('no coincide');
|
|
113
|
+
expect(fs_1.default.existsSync(path_1.default.join(trackRoot, '.awm', 'supervisor.claim'))).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
test('descriptor autenticado: reclama, escribe sidecars, y sale 0 sin lanzar `awm watch` cuando el plan bloquea el track', async () => {
|
|
116
|
+
(0, descriptor_1.writeDescriptor)(trackRoot, (0, fixtures_1.descriptorFixture)({ trackId: 'cli', planBranch: 'main', fencingToken: 'f'.repeat(32) }, planRoot));
|
|
117
|
+
const planState = (0, store_1.readJournal)(planRoot, 'main').state;
|
|
118
|
+
planState.tracks = [(0, fixtures_1.trackRefFixture)({ trackId: 'cli', phase: 'BLOCKED', readinessNonce: 'nonce-cli'.padEnd(32, '0') }, trackRoot)];
|
|
119
|
+
(0, store_1.writeJournal)(planRoot, 'main', planState);
|
|
120
|
+
const out = await runCli(trackRoot, [
|
|
121
|
+
'track', 'supervisor-wrapper', '--track', 'cli',
|
|
122
|
+
'--readiness', 'nonce-cli'.padEnd(32, '0'), '--fence', 'f'.repeat(32), '--nonce', 'real-nonce',
|
|
123
|
+
]);
|
|
124
|
+
expect(out.exitCode).toBe(0);
|
|
125
|
+
expect(fs_1.default.existsSync(path_1.default.join(trackRoot, '.awm', 'supervisor.claim'))).toBe(true);
|
|
126
|
+
const identity = JSON.parse(fs_1.default.readFileSync(path_1.default.join(trackRoot, '.awm', 'supervisor.identity.json'), 'utf8'));
|
|
127
|
+
expect(identity.nonce).toBe('real-nonce'); // el eco end-to-end del nonce también se ejercita vía CLI
|
|
128
|
+
});
|
|
129
|
+
});
|