agentic-workflow-manager 6.2.1 → 6.3.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.
Files changed (69) hide show
  1. package/README.md +17 -0
  2. package/dist/src/commands/job/gate.js +37 -13
  3. package/dist/src/commands/job/index.js +48 -8
  4. package/dist/src/commands/job/request.js +44 -6
  5. package/dist/src/commands/track/emit.js +26 -0
  6. package/dist/src/commands/track/index.js +194 -0
  7. package/dist/src/commands/track/status.js +63 -0
  8. package/dist/src/commands/track/supervisor-wrapper.js +89 -0
  9. package/dist/src/commands/watch/apply.js +201 -13
  10. package/dist/src/commands/watch/index.js +14 -0
  11. package/dist/src/commands/watch/runner.js +20 -1
  12. package/dist/src/commands/watch/supervisor.js +251 -27
  13. package/dist/src/commands/watch/teardown-driver.js +189 -0
  14. package/dist/src/commands/watch/tracks.js +1100 -0
  15. package/dist/src/core/journal/adapter.js +46 -6
  16. package/dist/src/core/journal/paths.js +9 -0
  17. package/dist/src/core/journal/process.js +101 -1
  18. package/dist/src/core/journal/requests.js +5 -1
  19. package/dist/src/core/journal/store.js +34 -2
  20. package/dist/src/core/journal/types.js +93 -4
  21. package/dist/src/core/paths.js +51 -0
  22. package/dist/src/core/tracks/concurrency.js +85 -0
  23. package/dist/src/core/tracks/context.js +89 -0
  24. package/dist/src/core/tracks/descriptor.js +40 -0
  25. package/dist/src/core/tracks/git.js +318 -0
  26. package/dist/src/core/tracks/join.js +185 -0
  27. package/dist/src/core/tracks/ownership.js +100 -0
  28. package/dist/src/core/tracks/plan-parser.js +108 -0
  29. package/dist/src/core/tracks/protocol.js +466 -0
  30. package/dist/src/core/tracks/teardown.js +34 -0
  31. package/dist/src/core/tracks/types.js +14 -0
  32. package/dist/src/index.js +2 -0
  33. package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
  34. package/dist/tests/commands/track/fixtures.js +13 -0
  35. package/dist/tests/commands/track/status.test.js +157 -0
  36. package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
  37. package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
  38. package/dist/tests/commands/track/verbs.test.js +326 -0
  39. package/dist/tests/commands/watch/apply.test.js +116 -4
  40. package/dist/tests/commands/watch/runner.test.js +22 -0
  41. package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
  42. package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
  43. package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
  44. package/dist/tests/commands/watch/track-finalize.test.js +643 -0
  45. package/dist/tests/commands/watch/track-freeze.test.js +591 -0
  46. package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
  47. package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
  48. package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
  49. package/dist/tests/core/journal/adapter-override.test.js +51 -0
  50. package/dist/tests/core/journal/process.test.js +37 -0
  51. package/dist/tests/core/journal/requests.test.js +21 -0
  52. package/dist/tests/core/journal/store.test.js +28 -0
  53. package/dist/tests/core/journal/types.test.js +89 -0
  54. package/dist/tests/core/same-existing-path.test.js +48 -0
  55. package/dist/tests/core/tracks/concurrency.test.js +134 -0
  56. package/dist/tests/core/tracks/context.test.js +177 -0
  57. package/dist/tests/core/tracks/descriptor.test.js +76 -0
  58. package/dist/tests/core/tracks/git.test.js +132 -0
  59. package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
  60. package/dist/tests/core/tracks/join.test.js +197 -0
  61. package/dist/tests/core/tracks/ownership.test.js +96 -0
  62. package/dist/tests/core/tracks/plan-parser.test.js +94 -0
  63. package/dist/tests/core/tracks/protocol.test.js +416 -0
  64. package/dist/tests/core/tracks/teardown.test.js +62 -0
  65. package/dist/tests/helpers/git-fixture.js +35 -0
  66. package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
  67. package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
  68. package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
  69. package/package.json +1 -1
@@ -75,4 +75,93 @@ describe('journal types', () => {
75
75
  badStatus.cycle.status = 'FINISHED';
76
76
  expect((0, types_1.isWellFormedState)(badStatus)).toBe(false);
77
77
  });
78
+ test('emptyState genera journalId estable y único (R9.1)', () => {
79
+ const a = (0, types_1.emptyState)('main');
80
+ const b = (0, types_1.emptyState)('main');
81
+ expect(a.journalId).toMatch(/^j-[0-9a-f-]{36}$/);
82
+ expect(b.journalId).not.toBe(a.journalId);
83
+ });
84
+ test('isWellFormedState exige journalId no vacio (R9.1)', () => {
85
+ const bad = (0, types_1.emptyState)('x');
86
+ delete bad.journalId;
87
+ expect((0, types_1.isWellFormedState)(bad)).toBe(false);
88
+ bad.journalId = '';
89
+ expect((0, types_1.isWellFormedState)(bad)).toBe(false);
90
+ });
91
+ test('TrackRef exige intents y nonces con shape completa (R9.2, R9.7)', () => {
92
+ const state = (0, types_1.emptyState)('main');
93
+ state.tracks = [{
94
+ trackId: 'cli', worktreePath: '/tmp/wt', branch: 'awm-track/cli', ownership: ['cli/'],
95
+ sharedResources: [], dependsOn: [], fencingToken: 'f'.repeat(32), phase: 'DECLARED',
96
+ readinessNonce: 'r'.repeat(32),
97
+ }];
98
+ expect((0, types_1.isWellFormedState)(state)).toBe(true);
99
+ state.tracks[0].fencingToken = '';
100
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
101
+ state.tracks[0].fencingToken = 'f'.repeat(32);
102
+ state.tracks[0].readinessNonce = '';
103
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
104
+ state.tracks[0].readinessNonce = 'r'.repeat(32);
105
+ state.tracks[0].phase = 'NOT_A_PHASE';
106
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
107
+ });
108
+ test('TrackRef y TrackContext rechazan una key requerida AUSENTE, no solo vaciada (R9.2, R9.7)', () => {
109
+ const fullTrack = {
110
+ trackId: 'cli', worktreePath: '/tmp/wt', branch: 'awm-track/cli', ownership: ['cli/'],
111
+ sharedResources: [], dependsOn: [], fencingToken: 'f'.repeat(32), phase: 'DECLARED',
112
+ readinessNonce: 'r'.repeat(32),
113
+ };
114
+ // sanity: la shape completa sigue siendo valida por si misma
115
+ const sane = (0, types_1.emptyState)('main');
116
+ sane.tracks = [{ ...fullTrack }];
117
+ expect((0, types_1.isWellFormedState)(sane)).toBe(true);
118
+ for (const key of Object.keys(fullTrack)) {
119
+ const partial = { ...fullTrack };
120
+ delete partial[key]; // key AUSENTE por completo, no ''
121
+ const state = (0, types_1.emptyState)('main');
122
+ state.tracks = [partial];
123
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
124
+ }
125
+ const fullContext = { trackId: 'cli', taskIds: ['t1'], planDigest: 'd', baseSha: 'abc', planJournalId: 'j-plan' };
126
+ const saneContext = (0, types_1.emptyState)('main');
127
+ saneContext.trackContext = { ...fullContext };
128
+ expect((0, types_1.isWellFormedState)(saneContext)).toBe(true);
129
+ for (const key of Object.keys(fullContext)) {
130
+ const partial = { ...fullContext };
131
+ delete partial[key];
132
+ const state = (0, types_1.emptyState)('main');
133
+ state.trackContext = partial;
134
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
135
+ }
136
+ });
137
+ test('TrackRef valida intents opcionales cuando presentes (R9.2, R9.7)', () => {
138
+ const state = (0, types_1.emptyState)('main');
139
+ state.tracks = [{
140
+ trackId: 'cli', worktreePath: '/tmp/wt', branch: 'awm-track/cli', ownership: ['cli/'],
141
+ sharedResources: [], dependsOn: [], fencingToken: 'f'.repeat(32), phase: 'ARMED',
142
+ readinessNonce: 'r'.repeat(32),
143
+ supervisorIntent: { nonce: 'n', argv: ['awm', 'watch'], claimPath: '/tmp/claim' },
144
+ joinIntent: { expectedPlanHeadSha: 'a', expectedTrackHeadSha: 'b', strategy: 'no-ff' },
145
+ teardownIntent: { worktreePath: '/tmp/wt', branch: 'awm-track/cli' },
146
+ }];
147
+ expect((0, types_1.isWellFormedState)(state)).toBe(true);
148
+ state.tracks[0].supervisorIntent.nonce = '';
149
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
150
+ });
151
+ test('trackContext, cohortPhase y trackIntegration validan shape cuando presentes (R9.1, R9.3)', () => {
152
+ const state = (0, types_1.emptyState)('main');
153
+ state.trackContext = { trackId: 'cli', taskIds: ['t1'], planDigest: 'd', baseSha: 'abc', planJournalId: 'j-plan' };
154
+ state.cohortPhase = 'ACTIVE';
155
+ state.cohortBaseSha = 'abc';
156
+ state.trackIntegration = { argv: ['npm', 'test'], paths: ['**/*.ts'], planDigest: 'd' };
157
+ expect((0, types_1.isWellFormedState)(state)).toBe(true);
158
+ state.cohortPhase = 'NOT_A_PHASE';
159
+ expect((0, types_1.isWellFormedState)(state)).toBe(false);
160
+ });
161
+ test('requiredVerifiers y VerificationItem aceptan el nuevo kind track-integration (C8)', () => {
162
+ const state = (0, types_1.emptyState)('main');
163
+ state.requiredVerifiers = ['track-integration'];
164
+ state.cycleVerificationPlan = [{ id: 'v1', kind: 'track-integration' }];
165
+ expect((0, types_1.isWellFormedState)(state)).toBe(true);
166
+ });
78
167
  });
@@ -0,0 +1,48 @@
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
+ // El bug que motiva esto costó 18 tests en Windows y NINGUNO en Linux/macOS: comparar
7
+ // `fs.realpathSync(a) !== fs.realpathSync(b)` como strings. En Windows el mismo directorio
8
+ // tiene dos grafías legítimas (8.3 `RUNNER~1` vs nombre largo, backslash vs slash) y
9
+ // `realpathSync` no las reconcilia, así que `ownedWorktreeExists` negaba un worktree propio.
10
+ //
11
+ // El caso 8.3 no se puede reproducir en Linux. Lo que SÍ se puede probar acá es el contrato
12
+ // del helper — identidad por filesystem, no por texto — que es lo que hace correcto el fix.
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const paths_1 = require("../../src/core/paths");
17
+ describe('sameExistingPath', () => {
18
+ let root;
19
+ beforeEach(() => { root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-samepath-'))); });
20
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
21
+ it('dos grafías del mismo directorio son la misma ruta', () => {
22
+ const real = path_1.default.join(root, 'destino');
23
+ fs_1.default.mkdirSync(real);
24
+ const alias = path_1.default.join(root, 'alias');
25
+ fs_1.default.symlinkSync(real, alias, 'dir');
26
+ // Strings distintos, mismo inode. Esta es la propiedad que el fix compra.
27
+ expect(alias).not.toBe(real);
28
+ expect((0, paths_1.sameExistingPath)(alias, real)).toBe(true);
29
+ });
30
+ it('directorios distintos NO son la misma ruta', () => {
31
+ const a = path_1.default.join(root, 'a');
32
+ const b = path_1.default.join(root, 'b');
33
+ fs_1.default.mkdirSync(a);
34
+ fs_1.default.mkdirSync(b);
35
+ expect((0, paths_1.sameExistingPath)(a, b)).toBe(false);
36
+ });
37
+ it('una ruta inexistente nunca "coincide" — la identidad no se afirma sin prueba', () => {
38
+ const a = path_1.default.join(root, 'a');
39
+ fs_1.default.mkdirSync(a);
40
+ expect((0, paths_1.sameExistingPath)(a, path_1.default.join(root, 'fantasma'))).toBe(false);
41
+ expect((0, paths_1.sameExistingPath)(path_1.default.join(root, 'fantasma'), path_1.default.join(root, 'fantasma'))).toBe(false);
42
+ });
43
+ it('sobrevive a separadores y segmentos redundantes', () => {
44
+ const a = path_1.default.join(root, 'a');
45
+ fs_1.default.mkdirSync(a);
46
+ expect((0, paths_1.sameExistingPath)(a, path_1.default.join(root, '.', 'a', '..', 'a'))).toBe(true);
47
+ });
48
+ });
@@ -0,0 +1,134 @@
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 concurrency_1 = require("../../../src/core/tracks/concurrency");
10
+ describe('deriveDefaultParallelism (R10.3)', () => {
11
+ test('elige el mayor N con p95 <= 1.5x y costo <=20% del tick', () => {
12
+ expect((0, concurrency_1.deriveDefaultParallelism)({
13
+ cpuCount: 8, tickMs: 5000,
14
+ samples: [
15
+ { supervisors: 1, p95Ms: 100 }, { supervisors: 2, p95Ms: 130 },
16
+ { supervisors: 3, p95Ms: 149 }, { supervisors: 4, p95Ms: 170 },
17
+ ],
18
+ })).toBe(3);
19
+ });
20
+ test('si ninguna medición habilita N>1 queda serial (R10.2)', () => {
21
+ expect((0, concurrency_1.deriveDefaultParallelism)({
22
+ cpuCount: 2, tickMs: 100,
23
+ samples: [{ supervisors: 1, p95Ms: 90 }, { supervisors: 2, p95Ms: 180 }],
24
+ })).toBe(1);
25
+ });
26
+ test('cpuCount:1 acota el tope aunque el costo esté dentro de budget', () => {
27
+ expect((0, concurrency_1.deriveDefaultParallelism)({
28
+ cpuCount: 1, tickMs: 5000,
29
+ samples: [{ supervisors: 1, p95Ms: 100 }, { supervisors: 2, p95Ms: 105 }],
30
+ })).toBe(1);
31
+ });
32
+ test('ignora muestras que excedan cpuCount', () => {
33
+ expect((0, concurrency_1.deriveDefaultParallelism)({
34
+ cpuCount: 2, tickMs: 5000,
35
+ samples: [
36
+ { supervisors: 1, p95Ms: 100 }, { supervisors: 2, p95Ms: 110 },
37
+ { supervisors: 3, p95Ms: 115 },
38
+ ],
39
+ })).toBe(2);
40
+ });
41
+ test('lanza sobre budget sin baseline N=1', () => {
42
+ expect(() => (0, concurrency_1.deriveDefaultParallelism)({ cpuCount: 4, tickMs: 5000, samples: [{ supervisors: 2, p95Ms: 10 }] }))
43
+ .toThrow(/baseline/);
44
+ });
45
+ test('lanza sobre budget inválido (cpuCount no entero o sin samples)', () => {
46
+ expect(() => (0, concurrency_1.deriveDefaultParallelism)({ cpuCount: 0, tickMs: 5000, samples: [{ supervisors: 1, p95Ms: 10 }] }))
47
+ .toThrow(/inválido/);
48
+ expect(() => (0, concurrency_1.deriveDefaultParallelism)({ cpuCount: 4, tickMs: 5000, samples: [] })).toThrow(/inválido/);
49
+ });
50
+ });
51
+ describe('parseMaxParallel', () => {
52
+ test('acepta enteros >= 1', () => {
53
+ expect((0, concurrency_1.parseMaxParallel)('1')).toBe(1);
54
+ expect((0, concurrency_1.parseMaxParallel)('4')).toBe(4);
55
+ });
56
+ test('rechaza 0, negativos, no-enteros y no-numéricos', () => {
57
+ expect(() => (0, concurrency_1.parseMaxParallel)('0')).toThrow(/entero >= 1/);
58
+ expect(() => (0, concurrency_1.parseMaxParallel)('-1')).toThrow(/entero >= 1/);
59
+ expect(() => (0, concurrency_1.parseMaxParallel)('1.5')).toThrow(/entero >= 1/);
60
+ expect(() => (0, concurrency_1.parseMaxParallel)('abc')).toThrow(/entero >= 1/);
61
+ });
62
+ });
63
+ describe('scheduleTracks', () => {
64
+ test('arranca hasta la capacidad libre, el resto espera', () => {
65
+ expect((0, concurrency_1.scheduleTracks)(['a', 'b', 'c'], new Set(['a']), 2)).toEqual({ start: ['b'], waiting: ['c'] });
66
+ });
67
+ test('sin capacidad libre todos quedan waiting', () => {
68
+ expect((0, concurrency_1.scheduleTracks)(['a', 'b', 'c'], new Set(['a', 'b']), 2)).toEqual({ start: [], waiting: ['c'] });
69
+ });
70
+ test('capacidad de sobra arranca todos los candidatos', () => {
71
+ expect((0, concurrency_1.scheduleTracks)(['a', 'b', 'c'], new Set(), 5)).toEqual({ start: ['a', 'b', 'c'], waiting: [] });
72
+ });
73
+ test('rechaza maxParallel invalido', () => {
74
+ expect(() => (0, concurrency_1.scheduleTracks)(['a'], new Set(), 0)).toThrow(/entero >= 1/);
75
+ });
76
+ });
77
+ describe('loadDefaultParallelism', () => {
78
+ // startDir es un tmpdir aislado en cada test — nunca toca ~/.awm ni depende
79
+ // del árbol real del repo (patrón de tmpdirs aislados de este repo, ver
80
+ // cli/tests/core/tracks/context.test.ts).
81
+ let tmp;
82
+ beforeEach(() => {
83
+ tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-concurrency-load-'));
84
+ });
85
+ afterEach(() => {
86
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
87
+ });
88
+ function artifactPathUnder(dir) {
89
+ return path_1.default.join(dir, 'docs', 'research', 'r5', 'fingerprint-budget.json');
90
+ }
91
+ test('artefacto ausente en todo el árbol de directorios -> serial (1)', () => {
92
+ expect((0, concurrency_1.loadDefaultParallelism)(tmp)).toBe(1);
93
+ });
94
+ test('artefacto con JSON inválido/corrupto -> serial (1)', () => {
95
+ const p = artifactPathUnder(tmp);
96
+ fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
97
+ fs_1.default.writeFileSync(p, '{ esto no es json');
98
+ expect((0, concurrency_1.loadDefaultParallelism)(tmp)).toBe(1);
99
+ });
100
+ test('derivedDefault ausente -> serial (1)', () => {
101
+ const p = artifactPathUnder(tmp);
102
+ fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
103
+ fs_1.default.writeFileSync(p, JSON.stringify({ cpuCount: 4, tickMs: 5000, samples: [] }));
104
+ expect((0, concurrency_1.loadDefaultParallelism)(tmp)).toBe(1);
105
+ });
106
+ test('derivedDefault no entero -> serial (1)', () => {
107
+ const p = artifactPathUnder(tmp);
108
+ fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
109
+ fs_1.default.writeFileSync(p, JSON.stringify({ derivedDefault: 2.5 }));
110
+ expect((0, concurrency_1.loadDefaultParallelism)(tmp)).toBe(1);
111
+ });
112
+ test('derivedDefault fuera de rango (< 1) -> serial (1)', () => {
113
+ const p = artifactPathUnder(tmp);
114
+ fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
115
+ fs_1.default.writeFileSync(p, JSON.stringify({ derivedDefault: 0 }));
116
+ expect((0, concurrency_1.loadDefaultParallelism)(tmp)).toBe(1);
117
+ });
118
+ test('artefacto válido -> devuelve el derivedDefault real, buscando hacia arriba desde un subdirectorio', () => {
119
+ const p = artifactPathUnder(tmp);
120
+ fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
121
+ fs_1.default.writeFileSync(p, JSON.stringify({
122
+ sourceHead: 'a'.repeat(40), node: process.version, platform: process.platform,
123
+ cpuCount: 4, tickMs: 5000, samples: [{ supervisors: 1, p50Ms: 10, p95Ms: 12 }], derivedDefault: 3,
124
+ }));
125
+ const nested = path_1.default.join(tmp, 'cli', 'dist', 'src', 'core', 'tracks');
126
+ fs_1.default.mkdirSync(nested, { recursive: true });
127
+ expect((0, concurrency_1.loadDefaultParallelism)(nested)).toBe(3);
128
+ });
129
+ test('sin override, resuelve desde __dirname del propio módulo (integración real)', () => {
130
+ const n = (0, concurrency_1.loadDefaultParallelism)();
131
+ expect(Number.isInteger(n)).toBe(true);
132
+ expect(n).toBeGreaterThanOrEqual(1);
133
+ });
134
+ });
@@ -0,0 +1,177 @@
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 context_1 = require("../../../src/core/tracks/context");
11
+ const descriptor_1 = require("../../../src/core/tracks/descriptor");
12
+ const store_1 = require("../../../src/core/journal/store");
13
+ const types_1 = require("../../../src/core/journal/types");
14
+ function gitInit(repo) {
15
+ (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', 'main'], { cwd: repo });
16
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
17
+ (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: repo });
18
+ (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 });
19
+ }
20
+ function descriptorFor(overrides, planRoot) {
21
+ return { schema: 1, planRoot, planBranch: 'main', trackId: 'cli', planJournalId: 'j-1', fencingToken: 'f'.repeat(32), ...overrides };
22
+ }
23
+ function trackRefFor(overrides, worktreePath) {
24
+ return {
25
+ trackId: 'cli', worktreePath, branch: 'track/cli', ownership: [], sharedResources: [], dependsOn: [],
26
+ fencingToken: 'f'.repeat(32), phase: 'ACTIVE', readinessNonce: 'n'.repeat(8), ...overrides,
27
+ };
28
+ }
29
+ function planStateWith(journalId, fencingToken, worktreePath) {
30
+ const s = (0, types_1.emptyState)('main');
31
+ s.journalId = journalId;
32
+ s.tracks = [trackRefFor({ fencingToken }, worktreePath)];
33
+ return s;
34
+ }
35
+ function trackStateWith(overrides = {}) {
36
+ const s = (0, types_1.emptyState)('track/cli');
37
+ s.trackContext = { trackId: 'cli', taskIds: ['1'], planDigest: 'expected', baseSha: 'sha', planJournalId: 'j-1', ...overrides };
38
+ return s;
39
+ }
40
+ function trackContextFixture(overrides = {}) {
41
+ return { trackId: 'cli', taskIds: ['1', '2'], planDigest: 'expected', baseSha: 'sha', planJournalId: 'j-1', ...overrides };
42
+ }
43
+ describe('resolveAuthenticatedContext (R2.5, R2.6, R9.4)', () => {
44
+ let trackRoot;
45
+ let otherRoot;
46
+ let planRoot;
47
+ beforeEach(() => {
48
+ trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-track-'));
49
+ otherRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-other-'));
50
+ planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-plan-'));
51
+ });
52
+ afterEach(() => {
53
+ for (const dir of [trackRoot, otherRoot, planRoot])
54
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
55
+ });
56
+ test('sin descriptor => modo plan, devuelve el journal local tal cual', () => {
57
+ const local = (0, types_1.emptyState)('main');
58
+ expect((0, context_1.resolveAuthenticatedContext)(trackRoot, local)).toEqual({ mode: 'plan', repoRoot: fs_1.default.realpathSync(trackRoot), journal: local });
59
+ });
60
+ test('descriptor debe coincidir por realpath, journalId y fencing (R2.5, R2.6, R9.4)', () => {
61
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ planJournalId: 'j-1', fencingToken: 'f'.repeat(32) }, planRoot));
62
+ const local = trackStateWith({ planJournalId: 'j-1', trackId: 'cli' });
63
+ expect((0, context_1.resolveAuthenticatedContext)(trackRoot, local, planStateWith('j-1', 'f'.repeat(32), trackRoot))).toMatchObject({ mode: 'track' });
64
+ expect(() => (0, context_1.resolveAuthenticatedContext)(trackRoot, local, planStateWith('j-1', 'x'.repeat(32), trackRoot))).toThrow(/fencingToken/);
65
+ (0, descriptor_1.writeDescriptor)(otherRoot, descriptorFor({ planJournalId: 'j-1', fencingToken: 'f'.repeat(32) }, planRoot));
66
+ // el TrackRef del plan sigue apuntando a trackRoot: otherRoot nunca puede autenticarse contra el (R9.4)
67
+ expect(() => (0, context_1.resolveAuthenticatedContext)(otherRoot, local, planStateWith('j-1', 'f'.repeat(32), trackRoot))).toThrow(/realpath/);
68
+ });
69
+ test('descriptor presente sin trackContext en el journal local lanza', () => {
70
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({}, planRoot));
71
+ const local = (0, types_1.emptyState)('track/cli'); // sin trackContext
72
+ expect(() => (0, context_1.resolveAuthenticatedContext)(trackRoot, local, planStateWith('j-1', 'f'.repeat(32), trackRoot)))
73
+ .toThrow(/trackContext/);
74
+ });
75
+ test('journalId del plan divergente del descriptor lanza (planJournalId)', () => {
76
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ planJournalId: 'j-1' }, planRoot));
77
+ const local = trackStateWith({ planJournalId: 'j-1' });
78
+ expect(() => (0, context_1.resolveAuthenticatedContext)(trackRoot, local, planStateWith('j-OTRO', 'f'.repeat(32), trackRoot)))
79
+ .toThrow(/planJournalId/);
80
+ });
81
+ test('planJournalId del trackContext local divergente del descriptor tambien lanza', () => {
82
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ planJournalId: 'j-1' }, planRoot));
83
+ const local = trackStateWith({ planJournalId: 'j-DISTINTO' });
84
+ expect(() => (0, context_1.resolveAuthenticatedContext)(trackRoot, local, planStateWith('j-1', 'f'.repeat(32), trackRoot)))
85
+ .toThrow(/planJournalId/);
86
+ });
87
+ test('TrackRef ausente en el journal del plan lanza', () => {
88
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ trackId: 'fantasma', planJournalId: 'j-1' }, planRoot));
89
+ const local = trackStateWith({ planJournalId: 'j-1', trackId: 'fantasma' });
90
+ const plan = planStateWith('j-1', 'f'.repeat(32), trackRoot); // tracks=[{trackId:'cli',...}], no 'fantasma'
91
+ expect(() => (0, context_1.resolveAuthenticatedContext)(trackRoot, local, plan)).toThrow(/TrackRef ausente/);
92
+ });
93
+ test('planRoot == cwd (un track no puede autenticarse contra si mismo) lanza como realpath', () => {
94
+ (0, descriptor_1.writeDescriptor)(planRoot, descriptorFor({ planJournalId: 'j-1' }, planRoot));
95
+ const local = trackStateWith({ planJournalId: 'j-1' });
96
+ expect(() => (0, context_1.resolveAuthenticatedContext)(planRoot, local, planStateWith('j-1', 'f'.repeat(32), planRoot)))
97
+ .toThrow(/realpath/);
98
+ });
99
+ test('trackId del trackContext local distinto del descriptor lanza (trackId no coincide)', () => {
100
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ trackId: 'cli', planJournalId: 'j-1' }, planRoot));
101
+ // el TrackRef existe para 'cli' y fencing/realpath calzan, pero el trackContext local declara OTRO trackId
102
+ const local = trackStateWith({ planJournalId: 'j-1', trackId: 'otro-track' });
103
+ expect(() => (0, context_1.resolveAuthenticatedContext)(trackRoot, local, planStateWith('j-1', 'f'.repeat(32), trackRoot)))
104
+ .toThrow(/trackId no coincide/);
105
+ });
106
+ });
107
+ describe('assertTrackTask (R2.3, R2.4)', () => {
108
+ test('planDigest divergente bloquea y task ajena se rechaza (R2.3, R2.4)', () => {
109
+ const ctx = trackContextFixture({ taskIds: ['1', '2'], planDigest: 'expected' });
110
+ expect(() => (0, context_1.assertTrackTask)(ctx, '3', 'expected')).toThrow(/fuera de la asignación/);
111
+ expect(() => (0, context_1.assertTrackTask)(ctx, '1', 'actual')).toThrow(/planDigest/);
112
+ });
113
+ test('task asignada con planDigest vigente no lanza', () => {
114
+ const ctx = trackContextFixture({ taskIds: ['1', '2'], planDigest: 'expected' });
115
+ expect(() => (0, context_1.assertTrackTask)(ctx, '1', 'expected')).not.toThrow();
116
+ });
117
+ });
118
+ describe('planDigest', () => {
119
+ test('normaliza CRLF antes de hashear, para que checkouts con distinto line-ending no diverjan', () => {
120
+ expect((0, context_1.planDigest)('a\r\nb\n')).toBe((0, context_1.planDigest)('a\nb\n'));
121
+ expect((0, context_1.planDigest)('a\nb\n')).not.toBe((0, context_1.planDigest)('a\nc\n'));
122
+ });
123
+ });
124
+ describe('resolveCommandContext (R9.4): guard de entrada para awm job/watch', () => {
125
+ let trackRoot;
126
+ let planRoot;
127
+ beforeEach(() => {
128
+ trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cmdctx-track-'));
129
+ planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cmdctx-plan-'));
130
+ gitInit(trackRoot);
131
+ gitInit(planRoot);
132
+ });
133
+ afterEach(() => {
134
+ fs_1.default.rmSync(trackRoot, { recursive: true, force: true });
135
+ fs_1.default.rmSync(planRoot, { recursive: true, force: true });
136
+ });
137
+ test('sin descriptor => modo plan, SIN leer ningun journal (no lanza aunque no exista journal alguno)', () => {
138
+ expect((0, context_1.resolveCommandContext)(trackRoot, 'main')).toEqual({ mode: 'plan' });
139
+ });
140
+ test('descriptor autentica correctamente => modo track', () => {
141
+ (0, store_1.initJournal)(planRoot, 'main');
142
+ const planState = { ...(readState(planRoot, 'main')) };
143
+ planState.journalId = 'j-1';
144
+ planState.tracks = [trackRefFor({ fencingToken: 'f'.repeat(32) }, trackRoot)];
145
+ (0, store_1.writeJournal)(planRoot, 'main', planState);
146
+ (0, store_1.initJournal)(trackRoot, 'track/cli');
147
+ const localState = { ...(readState(trackRoot, 'track/cli')) };
148
+ localState.trackContext = { trackId: 'cli', taskIds: ['1'], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
149
+ (0, store_1.writeJournal)(trackRoot, 'track/cli', localState);
150
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ planJournalId: 'j-1', fencingToken: 'f'.repeat(32) }, planRoot));
151
+ const out = (0, context_1.resolveCommandContext)(trackRoot, 'track/cli');
152
+ expect(out.mode).toBe('track');
153
+ });
154
+ test('descriptor presente pero fencingToken no coincide => rechaza con "cwd no autenticado"', () => {
155
+ (0, store_1.initJournal)(planRoot, 'main');
156
+ const planState = { ...(readState(planRoot, 'main')) };
157
+ planState.journalId = 'j-1';
158
+ planState.tracks = [trackRefFor({ fencingToken: 'f'.repeat(32) }, trackRoot)];
159
+ (0, store_1.writeJournal)(planRoot, 'main', planState);
160
+ (0, store_1.initJournal)(trackRoot, 'track/cli');
161
+ const localState = { ...(readState(trackRoot, 'track/cli')) };
162
+ localState.trackContext = { trackId: 'cli', taskIds: ['1'], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
163
+ (0, store_1.writeJournal)(trackRoot, 'track/cli', localState);
164
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ planJournalId: 'j-1', fencingToken: 'x'.repeat(32) }, planRoot));
165
+ expect(() => (0, context_1.resolveCommandContext)(trackRoot, 'track/cli')).toThrow(/cwd no autenticado/);
166
+ });
167
+ test('descriptor presente pero el journal local aun no existe => rechaza con "cwd no autenticado" (fail-closed)', () => {
168
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptorFor({ planJournalId: 'j-1', fencingToken: 'f'.repeat(32) }, planRoot));
169
+ expect(() => (0, context_1.resolveCommandContext)(trackRoot, 'track/cli')).toThrow(/cwd no autenticado/);
170
+ });
171
+ });
172
+ function readState(repo, branch) {
173
+ const r = (0, store_1.readJournal)(repo, branch);
174
+ if (r.state === null)
175
+ throw new Error('journal no inicializado en el fixture de test');
176
+ return r.state;
177
+ }
@@ -0,0 +1,76 @@
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 descriptor_1 = require("../../../src/core/tracks/descriptor");
10
+ function descriptor(overrides = {}, planRoot) {
11
+ return {
12
+ schema: 1, planRoot, planBranch: 'main', trackId: 'cli',
13
+ planJournalId: 'j-1', fencingToken: 'f'.repeat(32), ...overrides,
14
+ };
15
+ }
16
+ describe('descriptor de track (R2.5)', () => {
17
+ let trackRoot;
18
+ let planRoot;
19
+ beforeEach(() => {
20
+ trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-desc-track-'));
21
+ planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-desc-plan-'));
22
+ });
23
+ afterEach(() => {
24
+ fs_1.default.rmSync(trackRoot, { recursive: true, force: true });
25
+ fs_1.default.rmSync(planRoot, { recursive: true, force: true });
26
+ });
27
+ test('sin descriptor, readDescriptor devuelve null (caso comun)', () => {
28
+ expect((0, descriptor_1.readDescriptor)(trackRoot)).toBeNull();
29
+ });
30
+ test('write + read hace roundtrip; planRoot se normaliza a realpath', () => {
31
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({}, planRoot));
32
+ const out = (0, descriptor_1.readDescriptor)(trackRoot);
33
+ expect(out).toEqual({
34
+ schema: 1, planRoot: fs_1.default.realpathSync(planRoot), planBranch: 'main',
35
+ trackId: 'cli', planJournalId: 'j-1', fencingToken: 'f'.repeat(32),
36
+ });
37
+ });
38
+ test('el archivo se escribe con permisos 0600 (R1.2)', () => {
39
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({}, planRoot));
40
+ const mode = fs_1.default.statSync((0, descriptor_1.descriptorPath)(trackRoot)).mode & 0o777;
41
+ // win32 no sintetiza el bit de grupo/otros: mismo tratamiento que el resto de la
42
+ // suite tras el barrido de R6 (`atomic-file-durable`, `journal/store`,
43
+ // `install-transaction`). Este archivo nació en la rama R5, anterior al barrido.
44
+ expect(mode).toBe(process.platform === 'win32' ? 0o666 : 0o600);
45
+ });
46
+ test('rechaza schema distinto de 1', () => {
47
+ expect(() => (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({ schema: 2 }, planRoot))).toThrow(/inválido/);
48
+ });
49
+ test('rechaza planRoot relativo', () => {
50
+ expect(() => (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({}, 'relative/path'))).toThrow(/inválido/);
51
+ });
52
+ test('rechaza trackId vacio', () => {
53
+ expect(() => (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({ trackId: '' }, planRoot))).toThrow(/inválido/);
54
+ });
55
+ test('rechaza planJournalId vacio', () => {
56
+ expect(() => (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({ planJournalId: '' }, planRoot))).toThrow(/inválido/);
57
+ });
58
+ test('rechaza fencingToken corto (< 32 chars)', () => {
59
+ expect(() => (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({ fencingToken: 'short' }, planRoot))).toThrow(/inválido/);
60
+ });
61
+ test('un track.json con forma invalida lanza en vez de autenticar por omision (R1.6)', () => {
62
+ fs_1.default.mkdirSync(path_1.default.dirname((0, descriptor_1.descriptorPath)(trackRoot)), { recursive: true });
63
+ fs_1.default.writeFileSync((0, descriptor_1.descriptorPath)(trackRoot), JSON.stringify({ schema: 1, planRoot, planBranch: 'main' }));
64
+ expect(() => (0, descriptor_1.readDescriptor)(trackRoot)).toThrow(/corrupto/);
65
+ });
66
+ test('un track.json con JSON sintacticamente invalido lanza (no crashea silenciosamente en null)', () => {
67
+ fs_1.default.mkdirSync(path_1.default.dirname((0, descriptor_1.descriptorPath)(trackRoot)), { recursive: true });
68
+ fs_1.default.writeFileSync((0, descriptor_1.descriptorPath)(trackRoot), '{ esto no es json');
69
+ expect(() => (0, descriptor_1.readDescriptor)(trackRoot)).toThrow();
70
+ });
71
+ test('writeDescriptor es durable: sobrevive a una segunda escritura (idempotente en forma)', () => {
72
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({}, planRoot));
73
+ (0, descriptor_1.writeDescriptor)(trackRoot, descriptor({ fencingToken: 'g'.repeat(32) }, planRoot));
74
+ expect((0, descriptor_1.readDescriptor)(trackRoot).fencingToken).toBe('g'.repeat(32));
75
+ });
76
+ });