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.
- 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/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
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 (R5-T8, Step 5): claim exact-once + identity + readiness sidecars +
|
|
7
|
+
// espera read-only al propio TrackRef del plan. Ningún proceso real se
|
|
8
|
+
// spawnea (launchWatch es inyectado); el único I/O real es el journal del
|
|
9
|
+
// plan y los sidecars bajo `.awm/` del propio worktree (tmpdirs).
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const supervisor_wrapper_1 = require("../../../src/commands/track/supervisor-wrapper");
|
|
15
|
+
const tracks_1 = require("../../../src/commands/watch/tracks");
|
|
16
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
17
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
18
|
+
const fixtures_1 = require("./fixtures");
|
|
19
|
+
const NONCE = 'the-real-supervisor-intent-nonce';
|
|
20
|
+
function git(repo, args) {
|
|
21
|
+
(0, child_process_1.execFileSync)('git', args, { cwd: repo });
|
|
22
|
+
}
|
|
23
|
+
function gitInit(repo, branch) {
|
|
24
|
+
git(repo, ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', branch]);
|
|
25
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
26
|
+
git(repo, ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.']);
|
|
27
|
+
git(repo, ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'c']);
|
|
28
|
+
}
|
|
29
|
+
describe('runSupervisorWrapper — claim exact-once + espera read-only del ACTIVE (R4.7-R4.10, C11)', () => {
|
|
30
|
+
let planRoot;
|
|
31
|
+
let worktreePath;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrapper-plan-'));
|
|
34
|
+
worktreePath = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wrapper-wt-'));
|
|
35
|
+
gitInit(planRoot, 'main');
|
|
36
|
+
gitInit(worktreePath, 'awm-track/cli');
|
|
37
|
+
(0, store_1.initJournal)(planRoot, 'main');
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
fs_1.default.rmSync(planRoot, { recursive: true, force: true });
|
|
41
|
+
fs_1.default.rmSync(worktreePath, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
function seedTrackRef(phase) {
|
|
44
|
+
const s = (0, store_1.readJournal)(planRoot, 'main').state;
|
|
45
|
+
s.tracks = [
|
|
46
|
+
(0, fixtures_1.trackRefFixture)({ trackId: 'cli', phase, readinessNonce: 'nonce-cli'.padEnd(32, '0') }, worktreePath),
|
|
47
|
+
(0, fixtures_1.trackRefFixture)({ trackId: 'docs', phase: 'ACTIVE' }, worktreePath),
|
|
48
|
+
];
|
|
49
|
+
(0, store_1.writeJournal)(planRoot, 'main', s);
|
|
50
|
+
}
|
|
51
|
+
test('claim exact-once: identity + readiness sidecars, espera ACTIVE, lanza watch y espera el lock (R4.7, R4.8)', async () => {
|
|
52
|
+
seedTrackRef('ARMED');
|
|
53
|
+
let launched = null;
|
|
54
|
+
const wrapperPromise = (0, supervisor_wrapper_1.runSupervisorWrapper)({
|
|
55
|
+
worktreePath, trackId: 'cli', nonce: NONCE, readinessNonce: 'nonce-cli'.padEnd(32, '0'), fencingToken: 'fence',
|
|
56
|
+
planRoot, planBranch: 'main', pollMs: 5,
|
|
57
|
+
launchWatch: (wt) => {
|
|
58
|
+
launched = wt;
|
|
59
|
+
fs_1.default.mkdirSync(path_1.default.dirname((0, paths_1.supervisorLockPath)(wt)), { recursive: true });
|
|
60
|
+
fs_1.default.writeFileSync((0, paths_1.supervisorLockPath)(wt), '{}');
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Todavía ARMED: el wrapper debe seguir esperando (no lanza watch).
|
|
64
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
65
|
+
expect(launched).toBeNull();
|
|
66
|
+
seedTrackRef('ACTIVE');
|
|
67
|
+
const outcome = await wrapperPromise;
|
|
68
|
+
expect(outcome).toBe('active');
|
|
69
|
+
expect(launched).toBe(worktreePath);
|
|
70
|
+
expect(fs_1.default.existsSync(path_1.default.join(worktreePath, '.awm', 'supervisor.claim'))).toBe(true);
|
|
71
|
+
const identity = JSON.parse(fs_1.default.readFileSync(path_1.default.join(worktreePath, '.awm', 'supervisor.identity.json'), 'utf8'));
|
|
72
|
+
expect(identity.trackId).toBe('cli');
|
|
73
|
+
const ready = JSON.parse(fs_1.default.readFileSync(path_1.default.join(worktreePath, '.awm', 'supervisor.ready.json'), 'utf8'));
|
|
74
|
+
expect(ready.readinessNonce).toBe('nonce-cli'.padEnd(32, '0'));
|
|
75
|
+
});
|
|
76
|
+
test('un segundo wrapper con el mismo claim detecta el claim existente y sale sin lanzar otro supervisor (C11)', async () => {
|
|
77
|
+
seedTrackRef('ACTIVE');
|
|
78
|
+
const first = await (0, supervisor_wrapper_1.runSupervisorWrapper)({
|
|
79
|
+
worktreePath, trackId: 'cli', nonce: NONCE, readinessNonce: 'nonce-cli'.padEnd(32, '0'), fencingToken: 'fence',
|
|
80
|
+
planRoot, planBranch: 'main', pollMs: 5,
|
|
81
|
+
launchWatch: () => {
|
|
82
|
+
fs_1.default.mkdirSync(path_1.default.dirname((0, paths_1.supervisorLockPath)(worktreePath)), { recursive: true });
|
|
83
|
+
fs_1.default.writeFileSync((0, paths_1.supervisorLockPath)(worktreePath), '{}');
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(first).toBe('active');
|
|
87
|
+
let secondLaunched = false;
|
|
88
|
+
const second = await (0, supervisor_wrapper_1.runSupervisorWrapper)({
|
|
89
|
+
worktreePath, trackId: 'cli', nonce: NONCE, readinessNonce: 'nonce-cli'.padEnd(32, '0'), fencingToken: 'fence',
|
|
90
|
+
planRoot, planBranch: 'main', pollMs: 5,
|
|
91
|
+
launchWatch: () => { secondLaunched = true; },
|
|
92
|
+
});
|
|
93
|
+
expect(second).toBe('already-claimed');
|
|
94
|
+
expect(secondLaunched).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
test('si el plan bloquea el track, el wrapper jamás lanza `awm watch` (C1, R4.9)', async () => {
|
|
97
|
+
seedTrackRef('BLOCKED');
|
|
98
|
+
let launched = false;
|
|
99
|
+
const outcome = await (0, supervisor_wrapper_1.runSupervisorWrapper)({
|
|
100
|
+
worktreePath, trackId: 'cli', nonce: NONCE, readinessNonce: 'nonce-cli'.padEnd(32, '0'), fencingToken: 'fence',
|
|
101
|
+
planRoot, planBranch: 'main', pollMs: 5,
|
|
102
|
+
launchWatch: () => { launched = true; },
|
|
103
|
+
});
|
|
104
|
+
expect(outcome).toBe('blocked');
|
|
105
|
+
expect(launched).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
test('BLOCKER fix: el nonce recibido viaja al identity sidecar y observeSupervisorFromDisk jamás clasifica un wrapper legítimo como foreign (R4.7, R4.9)', async () => {
|
|
108
|
+
seedTrackRef('ARMED');
|
|
109
|
+
const wrapperPromise = (0, supervisor_wrapper_1.runSupervisorWrapper)({
|
|
110
|
+
worktreePath, trackId: 'cli', nonce: NONCE, readinessNonce: 'nonce-cli'.padEnd(32, '0'), fencingToken: 'fence',
|
|
111
|
+
planRoot, planBranch: 'main', pollMs: 5,
|
|
112
|
+
launchWatch: (wt) => {
|
|
113
|
+
fs_1.default.mkdirSync(path_1.default.dirname((0, paths_1.supervisorLockPath)(wt)), { recursive: true });
|
|
114
|
+
fs_1.default.writeFileSync((0, paths_1.supervisorLockPath)(wt), '{}');
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
// El claim/identity/ready sidecar se escriben ANTES del loop de
|
|
118
|
+
// espera por ACTIVE — no hace falta llegar a ACTIVE para probar el
|
|
119
|
+
// eco del nonce.
|
|
120
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
121
|
+
const identity = JSON.parse(fs_1.default.readFileSync(path_1.default.join(worktreePath, '.awm', 'supervisor.identity.json'), 'utf8'));
|
|
122
|
+
expect(identity.nonce).toBe(NONCE); // el wrapper NUNCA genera el suyo propio
|
|
123
|
+
const claimPath = path_1.default.join(worktreePath, '.awm', 'supervisor.claim');
|
|
124
|
+
const matchingRef = (0, fixtures_1.trackRefFixture)({ trackId: 'cli', phase: 'SUPERVISOR_STARTING', supervisorIntent: { nonce: NONCE, argv: [], claimPath } }, worktreePath);
|
|
125
|
+
expect((0, tracks_1.observeSupervisorFromDisk)(matchingRef)).toEqual({ kind: 'ready', readinessNonce: 'nonce-cli'.padEnd(32, '0') });
|
|
126
|
+
const foreignRef = (0, fixtures_1.trackRefFixture)({ trackId: 'cli', phase: 'SUPERVISOR_STARTING', supervisorIntent: { nonce: 'a-completely-different-nonce', argv: [], claimPath } }, worktreePath);
|
|
127
|
+
expect((0, tracks_1.observeSupervisorFromDisk)(foreignRef).kind).toBe('foreign');
|
|
128
|
+
seedTrackRef('ACTIVE');
|
|
129
|
+
await wrapperPromise;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
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 commander_1 = require("commander");
|
|
11
|
+
const track_1 = require("../../../src/commands/track");
|
|
12
|
+
const store_1 = require("../../../src/core/journal/store");
|
|
13
|
+
const paths_1 = require("../../../src/core/journal/paths");
|
|
14
|
+
const requests_1 = require("../../../src/core/journal/requests");
|
|
15
|
+
const descriptor_1 = require("../../../src/core/tracks/descriptor");
|
|
16
|
+
const fixtures_1 = require("./fixtures");
|
|
17
|
+
// Superficie `awm track` (R5.10, R6.1, R9.5, R9.6): los verbos mutantes
|
|
18
|
+
// (add/join/remove) SOLO emiten una request inmutable — jamas tocan Git ni
|
|
19
|
+
// state.json (single-writer, R6.1); los verbos read-only (list/status/
|
|
20
|
+
// verify-independence) jamas aceptan --generation ni emiten nada.
|
|
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
|
+
function readPendingKinds(repo, branch) {
|
|
38
|
+
return (0, requests_1.listPendingRequests)(repo, branch).filter((p) => !p.corrupt).map((p) => p.envelope.kind);
|
|
39
|
+
}
|
|
40
|
+
async function runCli(repo, argv) {
|
|
41
|
+
const prog = new commander_1.Command();
|
|
42
|
+
prog.exitOverride();
|
|
43
|
+
(0, track_1.registerTrackCommand)(prog);
|
|
44
|
+
const outSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
45
|
+
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
46
|
+
const cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(repo);
|
|
47
|
+
let exitCode = 0;
|
|
48
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
49
|
+
exitCode = code ?? 0;
|
|
50
|
+
throw new ExitSignal(exitCode);
|
|
51
|
+
}));
|
|
52
|
+
try {
|
|
53
|
+
await prog.parseAsync(['node', 'awm', ...argv]);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e instanceof ExitSignal) {
|
|
57
|
+
// ya capturado por el spy de process.exit
|
|
58
|
+
}
|
|
59
|
+
else if (typeof e.exitCode === 'number') {
|
|
60
|
+
// exitOverride(): errores/--help propios de commander (opcion
|
|
61
|
+
// desconocida, --generation faltante, --help) nunca llaman a
|
|
62
|
+
// process.exit — lanzan un CommanderError con exitCode propio.
|
|
63
|
+
exitCode = e.exitCode;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
cwdSpy.mockRestore();
|
|
71
|
+
exitSpy.mockRestore();
|
|
72
|
+
}
|
|
73
|
+
const out = outSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
74
|
+
const err = errSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
75
|
+
outSpy.mockRestore();
|
|
76
|
+
errSpy.mockRestore();
|
|
77
|
+
return { out, err, exitCode };
|
|
78
|
+
}
|
|
79
|
+
describe('awm track — verbos mutantes son request-only (R6.1)', () => {
|
|
80
|
+
let repo;
|
|
81
|
+
const branch = 'main';
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-verbs-'));
|
|
84
|
+
gitInit(repo, branch);
|
|
85
|
+
(0, store_1.initJournal)(repo, branch);
|
|
86
|
+
});
|
|
87
|
+
afterEach(() => { fs_1.default.rmSync(repo, { recursive: true, force: true }); });
|
|
88
|
+
test.each([
|
|
89
|
+
['add', 'track-prepare-request'],
|
|
90
|
+
['join', 'track-join-request'],
|
|
91
|
+
['remove', 'track-teardown-request'],
|
|
92
|
+
])('%s emite %s y no muta Git/state (R6.1)', async (verb, kind) => {
|
|
93
|
+
const beforeHead = git(repo, ['rev-parse', 'HEAD']);
|
|
94
|
+
const beforeState = fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8');
|
|
95
|
+
const out = await runCli(repo, ['track', verb, 'cli', '--generation', 'g1']);
|
|
96
|
+
expect(out.exitCode).toBe(0);
|
|
97
|
+
expect(readPendingKinds(repo, branch)).toContain(kind);
|
|
98
|
+
expect(git(repo, ['rev-parse', 'HEAD'])).toBe(beforeHead);
|
|
99
|
+
expect(fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8')).toBe(beforeState);
|
|
100
|
+
});
|
|
101
|
+
test.each(['add', 'join', 'remove'])('%s exige --generation (Commander valida antes de emitir)', async (verb) => {
|
|
102
|
+
const out = await runCli(repo, ['track', verb, 'cli']);
|
|
103
|
+
expect(out.exitCode).not.toBe(0);
|
|
104
|
+
expect(readPendingKinds(repo, branch)).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
test.each(['add', 'join', 'remove'])('%s sin argumento <trackId> falla en commander, no emite nada', async (verb) => {
|
|
107
|
+
const out = await runCli(repo, ['track', verb, '--generation', 'g1']);
|
|
108
|
+
expect(out.exitCode).not.toBe(0);
|
|
109
|
+
expect(readPendingKinds(repo, branch)).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('awm track list/status — read-only, sin --generation (R9.5)', () => {
|
|
113
|
+
let repo;
|
|
114
|
+
const branch = 'main';
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-list-'));
|
|
117
|
+
gitInit(repo, branch);
|
|
118
|
+
(0, store_1.initJournal)(repo, branch);
|
|
119
|
+
});
|
|
120
|
+
afterEach(() => { fs_1.default.rmSync(repo, { recursive: true, force: true }); });
|
|
121
|
+
test('list no acepta --generation y no emite requests', async () => {
|
|
122
|
+
const out = await runCli(repo, ['track', 'list', '--generation', 'g1']);
|
|
123
|
+
expect(out.exitCode).not.toBe(0); // commander: opcion desconocida
|
|
124
|
+
expect(readPendingKinds(repo, branch)).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
test('list imprime los tracks del journal (vacio por defecto) sin mutar nada', async () => {
|
|
127
|
+
const beforeState = fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8');
|
|
128
|
+
const out = await runCli(repo, ['track', 'list']);
|
|
129
|
+
expect(out.exitCode).toBe(0);
|
|
130
|
+
expect(JSON.parse(out.out)).toEqual({ tracks: [] });
|
|
131
|
+
expect(fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8')).toBe(beforeState);
|
|
132
|
+
});
|
|
133
|
+
test('status no acepta --generation', async () => {
|
|
134
|
+
const out = await runCli(repo, ['track', 'status', '--generation', 'g1']);
|
|
135
|
+
expect(out.exitCode).not.toBe(0);
|
|
136
|
+
expect(readPendingKinds(repo, branch)).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
test('status con journal sin tracks agrega cohort "sin tracks declarados" y sale 0', async () => {
|
|
139
|
+
const out = await runCli(repo, ['track', 'status']);
|
|
140
|
+
expect(out.exitCode).toBe(0);
|
|
141
|
+
expect(JSON.parse(out.out)).toEqual({ cohort: 'sin tracks declarados', tracks: {} });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('awm track verify-independence — R5.10', () => {
|
|
145
|
+
const fixture = (name) => path_1.default.join(__dirname, '../../fixtures/tracks', name);
|
|
146
|
+
test('plan con tracks disjuntos: parallel:true, sale 0', async () => {
|
|
147
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', fixture('two-independent.md')]);
|
|
148
|
+
expect(out.exitCode).toBe(0);
|
|
149
|
+
expect(JSON.parse(out.out)).toEqual({ parallel: true, reasons: [] });
|
|
150
|
+
});
|
|
151
|
+
test('plan sin membresia de tracks degrada a serial: parallel:false, sale 1', async () => {
|
|
152
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', fixture('legacy-serial.md')]);
|
|
153
|
+
expect(out.exitCode).toBe(1);
|
|
154
|
+
expect(JSON.parse(out.out)).toEqual({ parallel: false, reasons: ['no-tracks'] });
|
|
155
|
+
});
|
|
156
|
+
test('tracks con ownership superpuesto: parallel:false, sale 1, nombra el path (R5.1)', async () => {
|
|
157
|
+
const overlapping = fs_1.default.readFileSync(fixture('two-independent.md'), 'utf8')
|
|
158
|
+
.replace('docs/a.md', 'cli/src/a.ts');
|
|
159
|
+
const tmp = path_1.default.join(os_1.default.tmpdir(), `awm-overlap-${Date.now()}.md`);
|
|
160
|
+
fs_1.default.writeFileSync(tmp, overlapping);
|
|
161
|
+
try {
|
|
162
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', tmp]);
|
|
163
|
+
expect(out.exitCode).toBe(1);
|
|
164
|
+
const parsed = JSON.parse(out.out);
|
|
165
|
+
expect(parsed.parallel).toBe(false);
|
|
166
|
+
expect(parsed.reasons).toContain('path:cli/src/a.ts');
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
fs_1.default.rmSync(tmp, { force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
test('plan con violacion a nivel de PARSER (id de track peligroso) reporta {parallel:false} y sale 1 — jamas un stack trace crudo', async () => {
|
|
173
|
+
// parseTrackPlan lanza sincronicamente ante un id de track peligroso
|
|
174
|
+
// (ver tests/core/tracks/plan-parser.test.ts) — antes del fix, este
|
|
175
|
+
// throw escapaba sin capturar y tumbaba el proceso entero.
|
|
176
|
+
const broken = fs_1.default.readFileSync(fixture('two-independent.md'), 'utf8').replaceAll('cli', '..');
|
|
177
|
+
const tmp = path_1.default.join(os_1.default.tmpdir(), `awm-badid-${Date.now()}.md`);
|
|
178
|
+
fs_1.default.writeFileSync(tmp, broken);
|
|
179
|
+
try {
|
|
180
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', tmp]);
|
|
181
|
+
expect(out.exitCode).toBe(1);
|
|
182
|
+
const parsed = JSON.parse(out.out);
|
|
183
|
+
expect(parsed.parallel).toBe(false);
|
|
184
|
+
expect(parsed.reasons).toHaveLength(1);
|
|
185
|
+
expect(parsed.reasons[0]).toMatch(/^parse-error:.*track id inválido/);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
fs_1.default.rmSync(tmp, { force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
test('archivo de plan inexistente tambien reporta {parallel:false} estructurado, no un ENOENT crudo', async () => {
|
|
192
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', '/no/existe/plan.md']);
|
|
193
|
+
expect(out.exitCode).toBe(1);
|
|
194
|
+
const parsed = JSON.parse(out.out);
|
|
195
|
+
expect(parsed).toEqual({ parallel: false, reasons: [expect.stringMatching(/^parse-error:/)] });
|
|
196
|
+
});
|
|
197
|
+
test('shared resource sin forma <clase>:<valor> (canonicalResource, ownership.ts) tambien reporta JSON estructurado, no un stack trace (bug hermano post-fix)', async () => {
|
|
198
|
+
// El parser NUNCA valida la forma de "Shared resources" — solo revisa
|
|
199
|
+
// que la celda no este vacia (plan-parser.ts). `canonicalResource`
|
|
200
|
+
// lanza recien DENTRO de assessDeclaredIndependence, un paso despues
|
|
201
|
+
// del parseo: el primer fix de este verbo envolvia solo
|
|
202
|
+
// read+parseTrackPlan y dejaba escapar exactamente este throw.
|
|
203
|
+
const malformed = fs_1.default.readFileSync(fixture('two-independent.md'), 'utf8')
|
|
204
|
+
.replace('| cli | none | [] |', '| cli | none | badresource |');
|
|
205
|
+
const tmp = path_1.default.join(os_1.default.tmpdir(), `awm-badresource-${Date.now()}.md`);
|
|
206
|
+
fs_1.default.writeFileSync(tmp, malformed);
|
|
207
|
+
try {
|
|
208
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', tmp]);
|
|
209
|
+
expect(out.exitCode).toBe(1);
|
|
210
|
+
const parsed = JSON.parse(out.out);
|
|
211
|
+
expect(parsed.parallel).toBe(false);
|
|
212
|
+
expect(parsed.reasons).toHaveLength(1);
|
|
213
|
+
expect(parsed.reasons[0]).toMatch(/^parse-error:.*recurso debe usar/);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
fs_1.default.rmSync(tmp, { force: true });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
test('verify-independence no emite requests ni exige journal/branch', async () => {
|
|
220
|
+
// deliberadamente NO se llama gitInit/initJournal en process.cwd(): el
|
|
221
|
+
// verbo es puro sobre el archivo de plan, no toca .awm/journal.
|
|
222
|
+
const out = await runCli(process.cwd(), ['track', 'verify-independence', '--plan', fixture('two-independent.md')]);
|
|
223
|
+
expect(out.exitCode).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('awm track list/status — guard de contexto de PLAN (distinto de R9.4)', () => {
|
|
227
|
+
let planRoot;
|
|
228
|
+
let trackRoot;
|
|
229
|
+
beforeEach(() => {
|
|
230
|
+
planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-plan-'));
|
|
231
|
+
trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-worktree-'));
|
|
232
|
+
gitInit(planRoot, 'main');
|
|
233
|
+
gitInit(trackRoot, 'track/cli');
|
|
234
|
+
(0, store_1.initJournal)(planRoot, 'main');
|
|
235
|
+
const planState = (0, store_1.readJournal)(planRoot, 'main').state;
|
|
236
|
+
planState.journalId = 'j-1';
|
|
237
|
+
planState.tracks = [(0, fixtures_1.trackRefFixture)({}, trackRoot)];
|
|
238
|
+
(0, store_1.writeJournal)(planRoot, 'main', planState);
|
|
239
|
+
(0, store_1.initJournal)(trackRoot, 'track/cli');
|
|
240
|
+
const trackState = (0, store_1.readJournal)(trackRoot, 'track/cli').state;
|
|
241
|
+
trackState.trackContext = { trackId: 'cli', taskIds: [], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
242
|
+
(0, store_1.writeJournal)(trackRoot, 'track/cli', trackState);
|
|
243
|
+
(0, descriptor_1.writeDescriptor)(trackRoot, (0, fixtures_1.descriptorFixture)({ planJournalId: 'j-1' }, planRoot));
|
|
244
|
+
});
|
|
245
|
+
afterEach(() => {
|
|
246
|
+
fs_1.default.rmSync(planRoot, { recursive: true, force: true });
|
|
247
|
+
fs_1.default.rmSync(trackRoot, { recursive: true, force: true });
|
|
248
|
+
});
|
|
249
|
+
test.each(['list', 'status'])('`track %s` desde la RAIZ DEL PLAN funciona con normalidad', async (verb) => {
|
|
250
|
+
const out = await runCli(planRoot, ['track', verb]);
|
|
251
|
+
expect(out.err).toBe('');
|
|
252
|
+
// "sin tracks declarados"/{tracks:[...]} — lo que sea, pero SIN
|
|
253
|
+
// reject por contexto (ese es el punto de este guard especifico).
|
|
254
|
+
expect(out.exitCode).not.toBeNull();
|
|
255
|
+
});
|
|
256
|
+
test.each(['list', 'status'])('`track %s` desde el WORKTREE DE UN TRACK rechaza con mensaje claro, no "sin tracks declarados" silencioso', async (verb) => {
|
|
257
|
+
const out = await runCli(trackRoot, ['track', verb]);
|
|
258
|
+
expect(out.exitCode).toBe(1);
|
|
259
|
+
expect(out.out).toBe(''); // nunca imprime un agregado vacio enganoso
|
|
260
|
+
expect(out.err).toContain('worktree del track');
|
|
261
|
+
expect(out.err).toContain('cli');
|
|
262
|
+
expect(out.err).not.toContain('cwd no autenticado'); // no es un fallo de R9.4, es de contexto
|
|
263
|
+
});
|
|
264
|
+
test('`track status` sigue rechazando un cwd que NO autentica en absoluto (R9.4 sigue vigente)', async () => {
|
|
265
|
+
const otherRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-other-'));
|
|
266
|
+
try {
|
|
267
|
+
gitInit(otherRoot, 'track/cli');
|
|
268
|
+
(0, store_1.initJournal)(otherRoot, 'track/cli');
|
|
269
|
+
const otherState = (0, store_1.readJournal)(otherRoot, 'track/cli').state;
|
|
270
|
+
otherState.trackContext = { trackId: 'cli', taskIds: [], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
271
|
+
(0, store_1.writeJournal)(otherRoot, 'track/cli', otherState);
|
|
272
|
+
// mismo descriptor "logico" que trackRoot, pero su realpath JAMAS
|
|
273
|
+
// coincide con el TrackRef del plan — resolveCommandContext debe
|
|
274
|
+
// rechazar ANTES de que el guard de plan-vs-track entre en juego.
|
|
275
|
+
(0, descriptor_1.writeDescriptor)(otherRoot, (0, fixtures_1.descriptorFixture)({ planJournalId: 'j-1' }, planRoot));
|
|
276
|
+
const out = await runCli(otherRoot, ['track', 'status']);
|
|
277
|
+
expect(out.exitCode).toBe(1);
|
|
278
|
+
expect(out.err).toContain('cwd no autenticado');
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
fs_1.default.rmSync(otherRoot, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
describe('awm track status — sale 1 con gate rojo a nivel CLI (Gap: solo se probaba la funcion pura)', () => {
|
|
286
|
+
let planRoot;
|
|
287
|
+
let trackRoot;
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
planRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-status-plan-'));
|
|
290
|
+
trackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-track-status-track-'));
|
|
291
|
+
gitInit(planRoot, 'main');
|
|
292
|
+
gitInit(trackRoot, 'track/cli');
|
|
293
|
+
(0, store_1.initJournal)(planRoot, 'main');
|
|
294
|
+
(0, store_1.initJournal)(trackRoot, 'track/cli');
|
|
295
|
+
});
|
|
296
|
+
afterEach(() => {
|
|
297
|
+
fs_1.default.rmSync(planRoot, { recursive: true, force: true });
|
|
298
|
+
fs_1.default.rmSync(trackRoot, { recursive: true, force: true });
|
|
299
|
+
});
|
|
300
|
+
test('un track con tarea pending hace que `track status` (CLI) salga 1, no solo la funcion pura', async () => {
|
|
301
|
+
// trackContext presente + una tarea 'pending' basta para que
|
|
302
|
+
// computeTrackGate falle (categoria pending-task) — sin necesidad de
|
|
303
|
+
// reconstruir un journal que pase entero, al reves del test de
|
|
304
|
+
// status.test.ts que si cubre el camino verde.
|
|
305
|
+
const trackState = (0, store_1.readJournal)(trackRoot, 'track/cli').state;
|
|
306
|
+
trackState.trackContext = { trackId: 'cli', taskIds: ['T1'], planDigest: 'x', baseSha: 'y', planJournalId: 'j-1' };
|
|
307
|
+
trackState.tasks.push({ id: 'T1', title: 't', status: 'pending', attempts: 0, verificationPlan: [], reviewObligations: [] });
|
|
308
|
+
(0, store_1.writeJournal)(trackRoot, 'track/cli', trackState);
|
|
309
|
+
const planState = (0, store_1.readJournal)(planRoot, 'main').state;
|
|
310
|
+
planState.tracks = [(0, fixtures_1.trackRefFixture)({}, trackRoot)];
|
|
311
|
+
(0, store_1.writeJournal)(planRoot, 'main', planState);
|
|
312
|
+
const out = await runCli(planRoot, ['track', 'status']);
|
|
313
|
+
expect(out.exitCode).toBe(1);
|
|
314
|
+
const parsed = JSON.parse(out.out);
|
|
315
|
+
expect(parsed.tracks.cli.gate.pass).toBe(false);
|
|
316
|
+
expect(parsed.tracks.cli.gate.reasons.some((r) => r.category === 'pending-task')).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('awm track --help lista exactamente los 6 verbos', () => {
|
|
320
|
+
test('help incluye add, list, status, verify-independence, join, remove', async () => {
|
|
321
|
+
const out = await runCli(process.cwd(), ['track', '--help']);
|
|
322
|
+
for (const verb of ['add', 'list', 'status', 'verify-independence', 'join', 'remove']) {
|
|
323
|
+
expect(out.out).toContain(verb);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -39,7 +39,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
const fs_1 = __importDefault(require("fs"));
|
|
40
40
|
const path_1 = __importDefault(require("path"));
|
|
41
41
|
const os_1 = __importDefault(require("os"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
42
43
|
const apply_1 = require("../../../src/commands/watch/apply");
|
|
44
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
43
45
|
const requests_1 = require("../../../src/core/journal/requests");
|
|
44
46
|
const store_1 = require("../../../src/core/journal/store");
|
|
45
47
|
const paths_1 = require("../../../src/core/journal/paths");
|
|
@@ -51,6 +53,57 @@ function jobPayload(argv) {
|
|
|
51
53
|
function verdictPayload(verdictId, obligationId, result, detail) {
|
|
52
54
|
return { verdictId, obligationId, result, detail, fingerprint: 'fp-review', argv: ['awm-review', obligationId], paths: [], cwd: '.' };
|
|
53
55
|
}
|
|
56
|
+
function trackRef(trackId) {
|
|
57
|
+
return {
|
|
58
|
+
trackId, worktreePath: `/tmp/${trackId}`, branch: `awm-track/${trackId}`,
|
|
59
|
+
ownership: [], sharedResources: [], dependsOn: [],
|
|
60
|
+
fencingToken: `fence-${trackId}`.padEnd(32, '0'), phase: 'MERGED_UNVERIFIED', readinessNonce: `ready-${trackId}`.padEnd(32, '0'),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
describe('applyCyclePlanAppend / registerTrackIntegrationItems (R7.2/R7.3, Task 12)', () => {
|
|
64
|
+
function stateWithCycleItems(items) {
|
|
65
|
+
const s = (0, types_1.emptyState)('rama');
|
|
66
|
+
s.cycleVerificationPlan = items;
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
function stateWithTracks(trackIds) {
|
|
70
|
+
const s = (0, types_1.emptyState)('rama');
|
|
71
|
+
s.tracks = trackIds.map(trackRef);
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
test('cycle plan agrega items por id sin borrar satisfiedBy (R7.2)', () => {
|
|
75
|
+
const state = stateWithCycleItems([{ id: 'qa', kind: 'qa', satisfiedBy: 'job-qa' }]);
|
|
76
|
+
(0, apply_1.applyCyclePlanAppend)(state, [
|
|
77
|
+
{ id: 'qa', kind: 'qa' },
|
|
78
|
+
{ id: 'track-integration:a', kind: 'track-integration' },
|
|
79
|
+
]);
|
|
80
|
+
expect(state.cycleVerificationPlan).toEqual([
|
|
81
|
+
{ id: 'qa', kind: 'qa', satisfiedBy: 'job-qa' },
|
|
82
|
+
{ id: 'track-integration:a', kind: 'track-integration' },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
test('una colision de kind para el mismo id lanza (rechaza el request completo)', () => {
|
|
86
|
+
const state = stateWithCycleItems([{ id: 'qa', kind: 'qa' }]);
|
|
87
|
+
expect(() => (0, apply_1.applyCyclePlanAppend)(state, [{ id: 'qa', kind: 'interlock' }])).toThrow(/cambia kind/);
|
|
88
|
+
expect(state.cycleVerificationPlan).toEqual([{ id: 'qa', kind: 'qa' }]); // sin mutar ante el throw
|
|
89
|
+
});
|
|
90
|
+
test('registrar tracks deja gate rojo desde el inicio (R7.3)', () => {
|
|
91
|
+
const s = stateWithTracks(['a', 'b']);
|
|
92
|
+
(0, apply_1.registerTrackIntegrationItems)(s);
|
|
93
|
+
expect(s.cycleVerificationPlan.filter((x) => x.kind === 'track-integration'))
|
|
94
|
+
.toEqual([
|
|
95
|
+
{ id: 'track-integration:a', kind: 'track-integration' },
|
|
96
|
+
{ id: 'track-integration:b', kind: 'track-integration' },
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
test('registrar tracks es idempotente y aditivo: un track nuevo se agrega sin perder los previos', () => {
|
|
100
|
+
const s = stateWithTracks(['a']);
|
|
101
|
+
(0, apply_1.registerTrackIntegrationItems)(s);
|
|
102
|
+
s.tracks = [...s.tracks, trackRef('b')];
|
|
103
|
+
(0, apply_1.registerTrackIntegrationItems)(s);
|
|
104
|
+
expect(s.cycleVerificationPlan.map((x) => x.id).sort()).toEqual(['track-integration:a', 'track-integration:b']);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
54
107
|
describe('aplicacion transaccional de requests', () => {
|
|
55
108
|
let repo;
|
|
56
109
|
beforeEach(() => { repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-apply-')); (0, store_1.initJournal)(repo, 'rama'); });
|
|
@@ -131,7 +184,7 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
131
184
|
const s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
132
185
|
const jobId = Object.keys(s.jobs)[0];
|
|
133
186
|
expect(s.tasks[0].verificationPlan[0].satisfiedBy).toBe(jobId);
|
|
134
|
-
expect(s.jobs[jobId].satisfies).
|
|
187
|
+
expect(s.jobs[jobId].satisfies).toEqual(['v1']); // R7 Task 12: satisfies migró a array
|
|
135
188
|
});
|
|
136
189
|
test('fencing: token de generacion vieja => rejected-stale-generation auditado (R4.6)', () => {
|
|
137
190
|
(0, requests_1.emitRequest)(repo, 'rama', { kind: 'controller-heartbeat', generationToken: 'g-vieja', idempotencyKey: 'hb1', payload: {} });
|
|
@@ -295,7 +348,7 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
295
348
|
g = (0, gate_1.computeGate)(s, false, fpNull);
|
|
296
349
|
expect(g.reasons.some((r) => r.category === 'open-fix')).toBe(false); // ya no bloquea por open-fix
|
|
297
350
|
});
|
|
298
|
-
test('Fix4: register-entity cycle-plan es
|
|
351
|
+
test('Fix4/R7.2 (Task 12): register-entity cycle-plan es un append transaccional — un re-registro NUNCA pisa un item existente ni pierde satisfiedBy ya enlazado, pero SÍ agrega ids nuevos', () => {
|
|
299
352
|
(0, requests_1.emitRequest)(repo, 'rama', { kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'e1', payload: { entity: 'cycle-plan', items: [{ id: 'cv1', kind: 'qa' }] } });
|
|
300
353
|
(0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
301
354
|
(0, requests_1.emitRequest)(repo, 'rama', { kind: 'job-request', generationToken: 'g1', idempotencyKey: 'k1', payload: { ...jobPayload(['npm', 'run', 'qa']), satisfies: 'cv1' } });
|
|
@@ -303,11 +356,17 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
303
356
|
let s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
304
357
|
const jobId = Object.keys(s.jobs)[0];
|
|
305
358
|
expect(s.cycleVerificationPlan[0].satisfiedBy).toBe(jobId);
|
|
306
|
-
// re-registro defensivo (ej. tras crash sin memoria de si ya se registro),
|
|
359
|
+
// re-registro defensivo (ej. tras crash sin memoria de si ya se registro), con un id NUEVO
|
|
307
360
|
(0, requests_1.emitRequest)(repo, 'rama', { kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'e2', payload: { entity: 'cycle-plan', items: [{ id: 'cv-otro', kind: 'interlock' }] } });
|
|
308
361
|
(0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
309
362
|
s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
310
|
-
|
|
363
|
+
// el item original conserva su satisfiedBy Y el nuevo id se agregó (R7.2: union, nunca reemplazo)
|
|
364
|
+
expect(s.cycleVerificationPlan).toEqual([{ id: 'cv1', kind: 'qa', satisfiedBy: jobId }, { id: 'cv-otro', kind: 'interlock' }]);
|
|
365
|
+
// re-registrar el MISMO id con un kind DISTINTO se rechaza (colisión de kind rechaza el request completo)
|
|
366
|
+
const badKind = (0, requests_1.emitRequest)(repo, 'rama', { kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'e3', payload: { entity: 'cycle-plan', items: [{ id: 'cv1', kind: 'interlock' }] } });
|
|
367
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').rejectedInvalid).toBe(1);
|
|
368
|
+
expect(fs_1.default.existsSync(`${badKind.file}.rejected`)).toBe(true);
|
|
369
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.cycleVerificationPlan).toEqual([{ id: 'cv1', kind: 'qa', satisfiedBy: jobId }, { id: 'cv-otro', kind: 'interlock' }]);
|
|
311
370
|
});
|
|
312
371
|
test('Fix5: verdict --detail se redacta antes de persistir en state.json (R2.3)', () => {
|
|
313
372
|
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
@@ -394,4 +453,57 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
394
453
|
expect(s.generations[0].state).toBe('superseded');
|
|
395
454
|
expect(s.custodyDecisions).toEqual([expect.objectContaining({ decision: 'resume', reason: 'operador verifico que no hay llamada en vuelo', generationToken: 'g1' })]);
|
|
396
455
|
});
|
|
456
|
+
// ---- R7/C3/C4 (Task 12): register-entity track-integration, track-finalize-request ----
|
|
457
|
+
test('register --entity track-integration crea el contrato canonico UNA sola vez (creacion-unica, como cycle-plan)', () => {
|
|
458
|
+
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
459
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'ti1',
|
|
460
|
+
payload: { entity: 'track-integration', argv: ['npm', 'test', '--', '--runInBand'], paths: ['cli/src/**'], planDigest: 'd1' },
|
|
461
|
+
});
|
|
462
|
+
(0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
463
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.trackIntegration).toEqual({ argv: ['npm', 'test', '--', '--runInBand'], paths: ['cli/src/**'], planDigest: 'd1' });
|
|
464
|
+
// un segundo registro con OTRO contrato NUNCA lo pisa
|
|
465
|
+
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
466
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'ti2',
|
|
467
|
+
payload: { entity: 'track-integration', argv: ['npm', 'run', 'otro'], paths: [], planDigest: 'd2' },
|
|
468
|
+
});
|
|
469
|
+
(0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
470
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.trackIntegration).toEqual({ argv: ['npm', 'test', '--', '--runInBand'], paths: ['cli/src/**'], planDigest: 'd1' });
|
|
471
|
+
});
|
|
472
|
+
test('register --entity track-integration rechaza argv vacio', () => {
|
|
473
|
+
const r = (0, requests_1.emitRequest)(repo, 'rama', {
|
|
474
|
+
kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'ti-bad',
|
|
475
|
+
payload: { entity: 'track-integration', argv: [], paths: [], planDigest: 'd1' },
|
|
476
|
+
});
|
|
477
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').rejectedInvalid).toBe(1);
|
|
478
|
+
expect(fs_1.default.existsSync(`${r.file}.rejected`)).toBe(true);
|
|
479
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.trackIntegration).toBeUndefined();
|
|
480
|
+
});
|
|
481
|
+
test('track-finalize-request persiste el autoreporte del controller (qaFinalizeRequested) — puramente declarativo (R7.2)', () => {
|
|
482
|
+
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
483
|
+
kind: 'track-finalize-request', generationToken: 'g1', idempotencyKey: 'finalize-1',
|
|
484
|
+
payload: { qaHeadSha: 'sha-h3' },
|
|
485
|
+
});
|
|
486
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(1);
|
|
487
|
+
const s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
488
|
+
expect(s.qaFinalizeRequested).toEqual({ headSha: 'sha-h3', at: expect.any(String) });
|
|
489
|
+
});
|
|
490
|
+
test('declarar un track (track-prepare-request) agrega de inmediato su track-integration:* al cycle plan (R7.3)', () => {
|
|
491
|
+
// El segundo track dispara `headSha(repoRoot)` (armado de la cohorte,
|
|
492
|
+
// R4.1) — a diferencia del resto de esta suite, este repo SÍ necesita
|
|
493
|
+
// ser un repo git real para llegar a ese punto.
|
|
494
|
+
(0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'rama'], { cwd: repo });
|
|
495
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '--allow-empty', '-qm', 'seed'], { cwd: repo });
|
|
496
|
+
(0, requests_1.emitRequest)(repo, 'rama', { kind: 'track-prepare-request', generationToken: 'g1', idempotencyKey: 'tp-a', payload: { trackId: 'a' } });
|
|
497
|
+
(0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
498
|
+
let s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
499
|
+
expect(s.cycleVerificationPlan).toEqual([{ id: 'track-integration:a', kind: 'track-integration' }]);
|
|
500
|
+
(0, requests_1.emitRequest)(repo, 'rama', { kind: 'track-prepare-request', generationToken: 'g1', idempotencyKey: 'tp-b', payload: { trackId: 'b' } });
|
|
501
|
+
(0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
502
|
+
s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
503
|
+
expect(s.cycleVerificationPlan.map((i) => i.id).sort()).toEqual(['track-integration:a', 'track-integration:b']);
|
|
504
|
+
// R3.6/R7.3: el gate global ya está rojo por estos items sin satisfacer, desde el momento en que se declaran
|
|
505
|
+
const gate = (0, gate_1.computeGate)(s, false, () => 'fp-1');
|
|
506
|
+
expect(gate.pass).toBe(false);
|
|
507
|
+
expect(gate.reasons.some((r) => r.category === 'unsatisfied-plan' && /track-integration:a/.test(r.detail))).toBe(true);
|
|
508
|
+
});
|
|
397
509
|
});
|