agentic-workflow-manager 6.4.0 → 6.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/add.js +18 -1
- package/dist/src/commands/init.js +11 -2
- package/dist/src/commands/job/gate.js +22 -8
- package/dist/src/commands/track/emit.js +40 -0
- package/dist/src/commands/track/index.js +26 -1
- package/dist/src/commands/track/supervisor-wrapper.js +17 -1
- package/dist/src/commands/update.js +62 -8
- package/dist/src/commands/watch/apply.js +39 -0
- package/dist/src/commands/watch/tracks.js +51 -0
- package/dist/src/core/provider-version.js +26 -2
- package/dist/src/core/update-check.js +22 -2
- package/dist/src/index.js +16 -6
- package/dist/src/utils/config.js +17 -0
- package/dist/tests/commands/add.test.js +42 -0
- package/dist/tests/commands/init.test.js +23 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +17 -0
- package/dist/tests/commands/multi-agent-targeting.test.js +9 -4
- package/dist/tests/commands/track/supervisor-wrapper.test.js +21 -0
- package/dist/tests/commands/track/verbs.test.js +50 -3
- package/dist/tests/commands/update.test.js +243 -0
- package/dist/tests/commands/watch/apply.test.js +50 -0
- package/dist/tests/commands/watch/track-join-request.test.js +145 -0
- package/dist/tests/core/provider-version.test.js +41 -0
- package/dist/tests/core/update-check.test.js +5 -1
- package/dist/tests/utils/config.test.js +21 -0
- package/package.json +1 -1
package/dist/src/utils/config.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.getPreferences = getPreferences;
|
|
|
9
9
|
exports.loadPreferences = loadPreferences;
|
|
10
10
|
exports.savePreferences = savePreferences;
|
|
11
11
|
exports.enableAgent = enableAgent;
|
|
12
|
+
exports.resolveScopeOption = resolveScopeOption;
|
|
12
13
|
// src/utils/config.ts
|
|
13
14
|
const fs_1 = __importDefault(require("fs"));
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
@@ -158,3 +159,19 @@ function enableAgent(prefs, agent) {
|
|
|
158
159
|
? prefs
|
|
159
160
|
: { ...prefs, enabledAgents: [...prefs.enabledAgents, agent] };
|
|
160
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve `--scope` the same way across every non-interactive command path: an
|
|
164
|
+
* explicit value wins (validated), otherwise fall back to the caller's default
|
|
165
|
+
* instead of prompting. Pulled out of `remove`'s Commander closure (D-006) after
|
|
166
|
+
* `--yes` was found to still open the scope picker with no `--scope` given —
|
|
167
|
+
* `--yes` means zero prompts, and this is the one call site that had drifted
|
|
168
|
+
* from that rule (the sibling --agent default sits right next to it in index.ts).
|
|
169
|
+
*/
|
|
170
|
+
function resolveScopeOption(explicit, fallback) {
|
|
171
|
+
if (explicit === undefined)
|
|
172
|
+
return { ok: true, scope: fallback };
|
|
173
|
+
if (explicit !== 'local' && explicit !== 'global') {
|
|
174
|
+
return { ok: false, error: `Invalid scope "${explicit}". Use: local or global.` };
|
|
175
|
+
}
|
|
176
|
+
return { ok: true, scope: explicit };
|
|
177
|
+
}
|
|
@@ -94,3 +94,45 @@ it('awm add demo materializes a real Copilot .instructions.md file end-to-end',
|
|
|
94
94
|
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
95
95
|
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
96
96
|
});
|
|
97
|
+
// Regression for WIN-02 (issue #55 Windows playbook): `--method copy` was
|
|
98
|
+
// silently discarded by this exact function — `runAddBundleCore` hardcoded
|
|
99
|
+
// 'symlink' regardless of what the CLI parsed, so a real Windows machine got
|
|
100
|
+
// a Junction back no matter what `--method` asked for.
|
|
101
|
+
describe('runAddBundleCore --method', () => {
|
|
102
|
+
const claudeCodePrefs = {
|
|
103
|
+
defaultAgent: 'claude-code', enabledAgents: ['claude-code'], installMethod: 'symlink', defaultScope: 'local',
|
|
104
|
+
};
|
|
105
|
+
it('honours an explicit --method copy: a real directory, not a symlink', () => {
|
|
106
|
+
const content = makeContentFixture();
|
|
107
|
+
const projectRoot = makeProjectRoot();
|
|
108
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
109
|
+
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'claude-code', method: 'copy', cwd: projectRoot }, claudeCodePrefs, bundles);
|
|
110
|
+
expect(outcome.code).toBe(0);
|
|
111
|
+
const skillPath = path_1.default.join(projectRoot, '.claude/skills/demo-skill');
|
|
112
|
+
expect(fs_1.default.lstatSync(skillPath).isSymbolicLink()).toBe(false);
|
|
113
|
+
expect(fs_1.default.readFileSync(path_1.default.join(skillPath, 'SKILL.md'), 'utf8')).toContain('Follow the demo skill body.');
|
|
114
|
+
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
115
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
it('still defaults to symlink when --method is omitted (unchanged behavior)', () => {
|
|
118
|
+
const content = makeContentFixture();
|
|
119
|
+
const projectRoot = makeProjectRoot();
|
|
120
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
121
|
+
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'claude-code', cwd: projectRoot }, claudeCodePrefs, bundles);
|
|
122
|
+
expect(outcome.code).toBe(0);
|
|
123
|
+
const skillPath = path_1.default.join(projectRoot, '.claude/skills/demo-skill');
|
|
124
|
+
expect(fs_1.default.lstatSync(skillPath).isSymbolicLink()).toBe(true);
|
|
125
|
+
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
126
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
it('rejects an invalid --method value without touching the filesystem', () => {
|
|
129
|
+
const content = makeContentFixture();
|
|
130
|
+
const projectRoot = makeProjectRoot();
|
|
131
|
+
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
132
|
+
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'claude-code', method: 'bogus', cwd: projectRoot }, claudeCodePrefs, bundles);
|
|
133
|
+
expect(outcome.code).toBe(1);
|
|
134
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.claude/skills/demo-skill'))).toBe(false);
|
|
135
|
+
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
136
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -108,6 +108,29 @@ describe('runInit', () => {
|
|
|
108
108
|
const caveatCalls = logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
|
|
109
109
|
expect(caveatCalls).toHaveLength(0);
|
|
110
110
|
});
|
|
111
|
+
// Regression: `awm init --yes --json > init.json` on native Windows used to
|
|
112
|
+
// write the caveat to stdout via console.log AHEAD of the JSON payload,
|
|
113
|
+
// so the documented core-acceptance.md CORE-03 flow produced a file that
|
|
114
|
+
// wasn't valid JSON (banner text, then `{...}`). The caveat must still
|
|
115
|
+
// reach the operator — it now goes to stderr instead of vanishing.
|
|
116
|
+
it('does not pollute --json stdout with the caveat on native Windows', async () => {
|
|
117
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
118
|
+
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
119
|
+
const { runInit } = require('../../src/commands/init');
|
|
120
|
+
await runInit({
|
|
121
|
+
cwd: tmpHome,
|
|
122
|
+
yes: true,
|
|
123
|
+
json: true,
|
|
124
|
+
actions: { syncCache: async () => { }, installHook: () => ({ status: 'installed' }) },
|
|
125
|
+
});
|
|
126
|
+
const written = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
127
|
+
expect(() => JSON.parse(written)).not.toThrow();
|
|
128
|
+
const caveatOnStdout = logSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
|
|
129
|
+
expect(caveatOnStdout).toHaveLength(0);
|
|
130
|
+
const caveatOnStderr = errSpy.mock.calls.filter((c) => /awm watch/i.test(String(c[0])));
|
|
131
|
+
expect(caveatOnStderr).toHaveLength(1);
|
|
132
|
+
errSpy.mockRestore();
|
|
133
|
+
});
|
|
111
134
|
});
|
|
112
135
|
it('returns exit 0 on a bare HOME and never prompts with --yes (cache stubbed)', async () => {
|
|
113
136
|
const { runInit } = require('../../src/commands/init');
|
|
@@ -278,6 +278,23 @@ describe('computeTrackGate — gate local de un track (C6, R3.5)', () => {
|
|
|
278
278
|
expect((0, gate_1.computeTrackGate)(s, false, () => 'same')).toEqual({ pass: true, reasons: [] });
|
|
279
279
|
expect((0, gate_1.computeGate)(s, false, () => 'same').pass).toBe(false);
|
|
280
280
|
});
|
|
281
|
+
// El journal de un track REAL tiene `requiredVerifiers: []`: `initTrackJournal` llama a
|
|
282
|
+
// `initJournal`, no a `initWatch`, así que la detección mecánica de verificadores nunca
|
|
283
|
+
// corre sobre el worktree del track. La fixture de arriba los pobla a mano y por eso
|
|
284
|
+
// tapaba el defecto — R3.6 ("el repo no tiene test/sensors configurado") vivía FUERA del
|
|
285
|
+
// guard `requireGlobalKinds`, así que el gate local de todo track quedaba rojo para
|
|
286
|
+
// siempre, `attemptFreeze` devolvía `continue` en cada tick, y sin freeze no hay merge
|
|
287
|
+
// (C3) ni `COMPLETE`. Observado en la certificación con supervisor vivo.
|
|
288
|
+
test('un journal de track SIN requiredVerifiers (el caso real) certifica igual: R3.6 es del gate global', () => {
|
|
289
|
+
const s = stateWithCompletedTrackTask();
|
|
290
|
+
s.requiredVerifiers = [];
|
|
291
|
+
const g = (0, gate_1.computeTrackGate)(s, false, () => 'same');
|
|
292
|
+
expect(g.reasons.filter((r) => /no se certifica por ausencia/.test(r.detail))).toEqual([]);
|
|
293
|
+
expect(g).toEqual({ pass: true, reasons: [] });
|
|
294
|
+
// El gate GLOBAL sí sigue exigiéndolo: la regla no se debilitó, se puso en su scope.
|
|
295
|
+
const global = (0, gate_1.computeGate)({ ...s, requiredVerifiers: [] }, false, () => 'same');
|
|
296
|
+
expect(global.reasons.some((r) => /no se certifica por ausencia/.test(r.detail))).toBe(true);
|
|
297
|
+
});
|
|
281
298
|
test('corrupcion o journal ausente bloquea con categoria propia (corrupt-state)', () => {
|
|
282
299
|
const g = (0, gate_1.computeTrackGate)(null, true, () => 'same');
|
|
283
300
|
expect(g.pass).toBe(false);
|
|
@@ -18,6 +18,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
const fs_1 = __importDefault(require("fs"));
|
|
19
19
|
const os_1 = __importDefault(require("os"));
|
|
20
20
|
const path_1 = __importDefault(require("path"));
|
|
21
|
+
/** Un registry sincronizado con éxito. `awm update` con CERO registries es una máquina sin
|
|
22
|
+
* `awm init`: falla en vez de anunciar que actualizó (ver `tests/commands/update.test.ts`),
|
|
23
|
+
* así que los tests de acá — que miden targeting de agentes y el caveat de Windows, no
|
|
24
|
+
* registries — necesitan al menos uno para llegar a las etapas que sí les importan. */
|
|
25
|
+
const SYNCED_REGISTRY = { name: 'baseline', action: 'pulled', version: 'v1.0.0' };
|
|
21
26
|
describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
22
27
|
let tmpHome;
|
|
23
28
|
let originalHome;
|
|
@@ -95,7 +100,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
95
100
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
96
101
|
let captured = [];
|
|
97
102
|
const outcome = await runUpdateCore({ agent: explicit }, {
|
|
98
|
-
syncRegistries: async () => [],
|
|
103
|
+
syncRegistries: async () => [SYNCED_REGISTRY],
|
|
99
104
|
verifyMinCliVersions: () => [],
|
|
100
105
|
regenerateGlobalContext: () => [],
|
|
101
106
|
planReconciliation: (o) => { captured = o.targets; return { operations: [], records: [], reports: [] }; },
|
|
@@ -174,7 +179,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
174
179
|
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'hooks'), { recursive: true });
|
|
175
180
|
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2));
|
|
176
181
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
177
|
-
const syncRegistries = jest.fn(async () => []);
|
|
182
|
+
const syncRegistries = jest.fn(async () => [SYNCED_REGISTRY]);
|
|
178
183
|
const planReconciliation = jest.fn((o) => ({ operations: [], records: [], reports: [] }));
|
|
179
184
|
const applyInstallPlan = jest.fn(() => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }));
|
|
180
185
|
const resyncInstalledHooks = jest.fn(() => []);
|
|
@@ -198,7 +203,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
198
203
|
writePrefs(['claude-code'], 'claude-code');
|
|
199
204
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
200
205
|
const outcome = await runUpdateCore({}, {
|
|
201
|
-
syncRegistries: async () => [],
|
|
206
|
+
syncRegistries: async () => [SYNCED_REGISTRY],
|
|
202
207
|
verifyMinCliVersions: () => [],
|
|
203
208
|
regenerateGlobalContext: () => [],
|
|
204
209
|
planReconciliation: () => { throw new Error('reconciliation exploded'); },
|
|
@@ -231,7 +236,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
|
|
|
231
236
|
writePrefs(['claude-code']);
|
|
232
237
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
233
238
|
const deps = {
|
|
234
|
-
syncRegistries: async () => [],
|
|
239
|
+
syncRegistries: async () => [SYNCED_REGISTRY],
|
|
235
240
|
verifyMinCliVersions: () => [],
|
|
236
241
|
regenerateGlobalContext: () => [],
|
|
237
242
|
planReconciliation: () => ({ operations: [], records: [], reports: [] }),
|
|
@@ -93,6 +93,27 @@ describe('runSupervisorWrapper — claim exact-once + espera read-only del ACTIV
|
|
|
93
93
|
expect(second).toBe('already-claimed');
|
|
94
94
|
expect(secondLaunched).toBe(false);
|
|
95
95
|
});
|
|
96
|
+
// El wrapper esperaba la IGUALDAD EXACTA con `ACTIVE`, con un poll de 2 s en producción.
|
|
97
|
+
// Eso es una carrera: el plan puede cruzar `ACTIVE` entre dos lecturas y dejar al wrapper
|
|
98
|
+
// esperando para siempre una fase que ya pasó — sin `awm watch`, nadie consume las
|
|
99
|
+
// requests cross-journal del track (empezando por `track-freeze-request`) y la cohorte se
|
|
100
|
+
// traba en el freeze. Se observó al reparar el join: un track podía ir
|
|
101
|
+
// `ARMED -> ACTIVE -> JOIN_REQUESTED` dentro de un mismo `reconcileTracks`.
|
|
102
|
+
test.each(['ACTIVE', 'JOIN_REQUESTED', 'FROZEN'])('lanza watch con el track ya en %s: la condición es monótona, no la igualdad con ACTIVE', async (phase) => {
|
|
103
|
+
seedTrackRef(phase);
|
|
104
|
+
let launched = null;
|
|
105
|
+
const outcome = await (0, supervisor_wrapper_1.runSupervisorWrapper)({
|
|
106
|
+
worktreePath, trackId: 'cli', nonce: NONCE, readinessNonce: 'nonce-cli'.padEnd(32, '0'), fencingToken: 'fence',
|
|
107
|
+
planRoot, planBranch: 'main', pollMs: 5,
|
|
108
|
+
launchWatch: (wt) => {
|
|
109
|
+
launched = wt;
|
|
110
|
+
fs_1.default.mkdirSync(path_1.default.dirname((0, paths_1.supervisorLockPath)(wt)), { recursive: true });
|
|
111
|
+
fs_1.default.writeFileSync((0, paths_1.supervisorLockPath)(wt), '{}');
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
expect(outcome).toBe('active');
|
|
115
|
+
expect(launched).toBe(worktreePath);
|
|
116
|
+
});
|
|
96
117
|
test('si el plan bloquea el track, el wrapper jamás lanza `awm watch` (C1, R4.9)', async () => {
|
|
97
118
|
seedTrackRef('BLOCKED');
|
|
98
119
|
let launched = false;
|
|
@@ -31,6 +31,11 @@ function git(repo, args) {
|
|
|
31
31
|
function gitInit(repo, branch) {
|
|
32
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
33
|
fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
|
|
34
|
+
// `.awm/` gitignoreado, igual que lo deja `awm watch --init` (watch/init.ts) y que
|
|
35
|
+
// `runPrepareTrack` EXIGE en cada worktree nuevo: sin esto el journal propio hace que
|
|
36
|
+
// `git status` reporte el árbol sucio para siempre, y cualquier test que dependa de un
|
|
37
|
+
// árbol limpio mediría un repo que en producción no existe.
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
|
|
34
39
|
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: repo });
|
|
35
40
|
(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
41
|
}
|
|
@@ -98,6 +103,48 @@ describe('awm track — verbos mutantes son request-only (R6.1)', () => {
|
|
|
98
103
|
expect(git(repo, ['rev-parse', 'HEAD'])).toBe(beforeHead);
|
|
99
104
|
expect(fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8')).toBe(beforeState);
|
|
100
105
|
});
|
|
106
|
+
// `finalize` es PLAN-scoped y sin <trackId>, así que no entra en las matrices de
|
|
107
|
+
// add/join/remove — pero comparte con ellas la propiedad que define a la superficie:
|
|
108
|
+
// emite una request y no toca Git ni state.json.
|
|
109
|
+
test('finalize emite track-finalize-request con el HEAD del plan, y no muta Git/state (R7.2)', async () => {
|
|
110
|
+
const beforeHead = git(repo, ['rev-parse', 'HEAD']);
|
|
111
|
+
const beforeState = fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8');
|
|
112
|
+
const out = await runCli(repo, ['track', 'finalize', '--generation', 'g1']);
|
|
113
|
+
expect(out.exitCode).toBe(0);
|
|
114
|
+
expect(readPendingKinds(repo, branch)).toContain('track-finalize-request');
|
|
115
|
+
const pending = (0, requests_1.listPendingRequests)(repo, branch).find((p) => !p.corrupt && p.envelope.kind === 'track-finalize-request');
|
|
116
|
+
// El HEAD se LEE del repo: es el autoreporte del controller sobre SU propio HEAD,
|
|
117
|
+
// no un valor que el llamador pueda elegir.
|
|
118
|
+
expect((pending?.envelope.payload).qaHeadSha).toBe(beforeHead);
|
|
119
|
+
expect(git(repo, ['rev-parse', 'HEAD'])).toBe(beforeHead);
|
|
120
|
+
expect(fs_1.default.readFileSync((0, paths_1.statePath)(repo, branch), 'utf8')).toBe(beforeState);
|
|
121
|
+
});
|
|
122
|
+
test('finalize exige --generation', async () => {
|
|
123
|
+
const out = await runCli(repo, ['track', 'finalize']);
|
|
124
|
+
expect(out.exitCode).not.toBe(0);
|
|
125
|
+
expect(readPendingKinds(repo, branch)).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
test('finalize con el árbol sucio falla nombrando lo que falta, en vez de emitir un autoreporte que el supervisor va a rechazar en silencio', async () => {
|
|
128
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'pendiente.txt'), 'sin comitear');
|
|
129
|
+
const out = await runCli(repo, ['track', 'finalize', '--generation', 'g1']);
|
|
130
|
+
expect(out.exitCode).not.toBe(0);
|
|
131
|
+
expect(out.err).toMatch(/sin comitear/);
|
|
132
|
+
expect(readPendingKinds(repo, branch)).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
test('re-reportar el MISMO HEAD colapsa por idempotencyKey; un HEAD nuevo es una request nueva', async () => {
|
|
135
|
+
await runCli(repo, ['track', 'finalize', '--generation', 'g1']);
|
|
136
|
+
const first = (0, requests_1.listPendingRequests)(repo, branch).filter((p) => !p.corrupt).map((p) => p.envelope.idempotencyKey);
|
|
137
|
+
await runCli(repo, ['track', 'finalize', '--generation', 'g1']);
|
|
138
|
+
const second = (0, requests_1.listPendingRequests)(repo, branch).filter((p) => !p.corrupt).map((p) => p.envelope.idempotencyKey);
|
|
139
|
+
expect(new Set(second)).toEqual(new Set(first)); // mismo HEAD => misma key
|
|
140
|
+
// La QA encontró algo más, se corrigió y se comiteó: HEAD nuevo, request nueva.
|
|
141
|
+
fs_1.default.writeFileSync(path_1.default.join(repo, 'fix.txt'), 'correccion de QA');
|
|
142
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'add', '.'], { cwd: repo });
|
|
143
|
+
(0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fix'], { cwd: repo });
|
|
144
|
+
await runCli(repo, ['track', 'finalize', '--generation', 'g1']);
|
|
145
|
+
const third = new Set((0, requests_1.listPendingRequests)(repo, branch).filter((p) => !p.corrupt).map((p) => p.envelope.idempotencyKey));
|
|
146
|
+
expect(third.size).toBe(new Set(first).size + 1);
|
|
147
|
+
});
|
|
101
148
|
test.each(['add', 'join', 'remove'])('%s exige --generation (Commander valida antes de emitir)', async (verb) => {
|
|
102
149
|
const out = await runCli(repo, ['track', verb, 'cli']);
|
|
103
150
|
expect(out.exitCode).not.toBe(0);
|
|
@@ -316,10 +363,10 @@ describe('awm track status — sale 1 con gate rojo a nivel CLI (Gap: solo se pr
|
|
|
316
363
|
expect(parsed.tracks.cli.gate.reasons.some((r) => r.category === 'pending-task')).toBe(true);
|
|
317
364
|
});
|
|
318
365
|
});
|
|
319
|
-
describe('awm track --help lista exactamente los
|
|
320
|
-
test('help incluye add, list, status, verify-independence, join, remove', async () => {
|
|
366
|
+
describe('awm track --help lista exactamente los 7 verbos', () => {
|
|
367
|
+
test('help incluye add, list, status, verify-independence, join, remove, finalize', async () => {
|
|
321
368
|
const out = await runCli(process.cwd(), ['track', '--help']);
|
|
322
|
-
for (const verb of ['add', 'list', 'status', 'verify-independence', 'join', 'remove']) {
|
|
369
|
+
for (const verb of ['add', 'list', 'status', 'verify-independence', 'join', 'remove', 'finalize']) {
|
|
323
370
|
expect(out.out).toContain(verb);
|
|
324
371
|
}
|
|
325
372
|
});
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
// `awm update` no debe anunciar trabajo que no hizo, ni colgarse esperando a un humano
|
|
7
|
+
// que no está.
|
|
8
|
+
//
|
|
9
|
+
// Los dos defectos que estos tests dejan cerrados se reprodujeron en un HOME aislado
|
|
10
|
+
// contra el binario publicado (v6.3.0):
|
|
11
|
+
//
|
|
12
|
+
// 1. Sobre un AWM_HOME recién creado, SIN NINGÚN registry configurado, el comando
|
|
13
|
+
// imprimía "✅ Registries, skills and hooks updated." y salía 0. No había registries,
|
|
14
|
+
// no había skills y no había hooks: cada etapa "pasaba" porque no tenía nada que
|
|
15
|
+
// hacer, y `runUpdateCore` devolvía `{ code: 0 }` incondicionalmente al final. El
|
|
16
|
+
// literal del outro en `index.ts` solo podía decir "actualizado".
|
|
17
|
+
// 2. Con un TTY en stdin, el comando quedaba colgado para siempre en el confirm de
|
|
18
|
+
// self-update ("Update awm v6.2.1 → v6.3.0 now?"). Con `< /dev/null` terminaba bien
|
|
19
|
+
// — es decir, el cuelgue aparecía exactamente cuando había un humano, y desaparecía
|
|
20
|
+
// cuando no lo había, que es al revés de lo que hace falta: en CI, cron o una sesión
|
|
21
|
+
// agéntica no hay nadie para contestar.
|
|
22
|
+
//
|
|
23
|
+
// Ambos se testean por el VALOR DE RETORNO y por el texto derivado, no scrapeando consola:
|
|
24
|
+
// el defecto vivía justamente en que el retorno no llevaba la información que el mensaje
|
|
25
|
+
// necesitaba.
|
|
26
|
+
const fs_1 = __importDefault(require("fs"));
|
|
27
|
+
const os_1 = __importDefault(require("os"));
|
|
28
|
+
const path_1 = __importDefault(require("path"));
|
|
29
|
+
// `@clack/prompts` es ESM-only y el transform CommonJS de ts-jest no puede cargarlo; se
|
|
30
|
+
// mockea para poder `require` `core/update-check` desde acá (mismo patrón que
|
|
31
|
+
// `tests/core/update-check.test.ts`). Los tests inyectan su propio `confirmImpl`, así que
|
|
32
|
+
// este doble nunca llega a usarse: solo evita que el import de nivel de módulo explote.
|
|
33
|
+
jest.mock('@clack/prompts', () => ({
|
|
34
|
+
confirm: jest.fn(),
|
|
35
|
+
isCancel: jest.fn(),
|
|
36
|
+
}));
|
|
37
|
+
/** Preferencias mínimas válidas en el AWM_HOME activo (`normalizePreferences` exige los
|
|
38
|
+
* cuatro campos). Se llama despues de fijar HOME/AWM_HOME, nunca antes. */
|
|
39
|
+
function writePrefs() {
|
|
40
|
+
const { savePreferences } = require('../../src/utils/config');
|
|
41
|
+
savePreferences({
|
|
42
|
+
defaultAgent: 'claude-code',
|
|
43
|
+
enabledAgents: ['claude-code'],
|
|
44
|
+
installMethod: 'symlink',
|
|
45
|
+
defaultScope: 'local',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
describe('awm update — honest outcome', () => {
|
|
49
|
+
const realHome = process.env.HOME;
|
|
50
|
+
const realAwmHome = process.env.AWM_HOME;
|
|
51
|
+
let tmpHome;
|
|
52
|
+
let logSpy;
|
|
53
|
+
let warnSpy;
|
|
54
|
+
let errSpy;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
// Ningún test toca el `~/.awm` real (CLAUDE.md): HOME y AWM_HOME apuntan al tmpdir.
|
|
57
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-update-'));
|
|
58
|
+
process.env.HOME = tmpHome;
|
|
59
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
60
|
+
fs_1.default.mkdirSync(process.env.AWM_HOME, { recursive: true });
|
|
61
|
+
writePrefs();
|
|
62
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
63
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
64
|
+
errSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
65
|
+
jest.resetModules();
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
logSpy.mockRestore();
|
|
69
|
+
warnSpy.mockRestore();
|
|
70
|
+
errSpy.mockRestore();
|
|
71
|
+
if (realHome === undefined)
|
|
72
|
+
delete process.env.HOME;
|
|
73
|
+
else
|
|
74
|
+
process.env.HOME = realHome;
|
|
75
|
+
if (realAwmHome === undefined)
|
|
76
|
+
delete process.env.AWM_HOME;
|
|
77
|
+
else
|
|
78
|
+
process.env.AWM_HOME = realAwmHome;
|
|
79
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
function deps(overrides = {}) {
|
|
82
|
+
return {
|
|
83
|
+
syncRegistries: async () => [{ name: 'baseline', action: 'pulled', version: 'v1.15.1' }],
|
|
84
|
+
verifyMinCliVersions: () => [],
|
|
85
|
+
regenerateGlobalContext: () => [],
|
|
86
|
+
planReconciliation: () => ({ operations: [], records: [], reports: [] }),
|
|
87
|
+
applyInstallPlan: () => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }),
|
|
88
|
+
resyncInstalledHooks: () => [],
|
|
89
|
+
offerSelfUpdate: async () => { },
|
|
90
|
+
...overrides,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
it('sin registries configurados NO declara éxito: sale != 0 y manda a `awm init`', async () => {
|
|
94
|
+
const { runUpdateCore, updateOutro } = require('../../src/commands/update');
|
|
95
|
+
const result = await runUpdateCore({}, deps({ syncRegistries: async () => [] }));
|
|
96
|
+
expect(result.code).not.toBe(0);
|
|
97
|
+
expect(result.registries).toEqual({ configured: 0, synced: 0, failed: [] });
|
|
98
|
+
// El cierre nunca puede afirmar que se actualizó algo.
|
|
99
|
+
expect(updateOutro(result)).toBe('Nothing updated — no registries configured on this machine.');
|
|
100
|
+
expect(updateOutro(result)).not.toMatch(/updated\./i);
|
|
101
|
+
expect(errSpy.mock.calls.flat().join('\n')).toMatch(/awm init/);
|
|
102
|
+
});
|
|
103
|
+
it('sin registries no sigue con las etapas posteriores (que "pasarían" por vacías)', async () => {
|
|
104
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
105
|
+
const planReconciliation = jest.fn(() => ({ operations: [], records: [], reports: [] }));
|
|
106
|
+
const resyncInstalledHooks = jest.fn(() => []);
|
|
107
|
+
const offerSelfUpdate = jest.fn(async () => { });
|
|
108
|
+
await runUpdateCore({}, deps({
|
|
109
|
+
syncRegistries: async () => [],
|
|
110
|
+
planReconciliation, resyncInstalledHooks, offerSelfUpdate,
|
|
111
|
+
}));
|
|
112
|
+
expect(planReconciliation).not.toHaveBeenCalled();
|
|
113
|
+
expect(resyncInstalledHooks).not.toHaveBeenCalled();
|
|
114
|
+
expect(offerSelfUpdate).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
it('con registries sincronizados sí declara éxito, y dice cuántos', async () => {
|
|
117
|
+
const { runUpdateCore, updateOutro } = require('../../src/commands/update');
|
|
118
|
+
const result = await runUpdateCore({}, deps({
|
|
119
|
+
syncRegistries: async () => [
|
|
120
|
+
{ name: 'baseline', action: 'pulled', version: 'v1.15.1' },
|
|
121
|
+
{ name: 'docs', action: 'recloned', version: 'v2.0.0' },
|
|
122
|
+
],
|
|
123
|
+
}));
|
|
124
|
+
expect(result.code).toBe(0);
|
|
125
|
+
expect(result.registries).toEqual({ configured: 2, synced: 2, failed: [] });
|
|
126
|
+
expect(updateOutro(result)).toBe('✅ 2 registries, skills and hooks updated.');
|
|
127
|
+
});
|
|
128
|
+
it('un registry que falló y NO dejó contenido en disco falla cerrado', async () => {
|
|
129
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
130
|
+
const result = await runUpdateCore({}, deps({
|
|
131
|
+
syncRegistries: async () => [{ name: 'baseline', action: 'error', error: 'network unreachable' }],
|
|
132
|
+
}));
|
|
133
|
+
expect(result.code).toBe(1);
|
|
134
|
+
expect(result.registries.failed).toEqual(['baseline']);
|
|
135
|
+
});
|
|
136
|
+
it('un registry que falló pero conserva contenido sigue, y el cierre lo nombra en vez de anunciar éxito parejo', async () => {
|
|
137
|
+
// `unusableSyncedRegistries` mira contenido EN DISCO: se siembra el content root
|
|
138
|
+
// y el registries.json para que `docs` cuente como stale, no como roto.
|
|
139
|
+
const registriesDir = path_1.default.join(process.env.AWM_HOME, 'registries');
|
|
140
|
+
fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs'), { recursive: true });
|
|
141
|
+
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'docs', remote: 'https://example.test/docs.git' }]));
|
|
142
|
+
const { runUpdateCore, updateOutro } = require('../../src/commands/update');
|
|
143
|
+
const result = await runUpdateCore({}, deps({
|
|
144
|
+
syncRegistries: async () => [
|
|
145
|
+
{ name: 'baseline', action: 'pulled', version: 'v1.15.1' },
|
|
146
|
+
{ name: 'docs', action: 'error', error: 'network unreachable' },
|
|
147
|
+
],
|
|
148
|
+
}));
|
|
149
|
+
expect(result.code).toBe(0);
|
|
150
|
+
expect(result.registries).toEqual({ configured: 2, synced: 1, failed: ['docs'] });
|
|
151
|
+
const outro = updateOutro(result);
|
|
152
|
+
expect(outro).toMatch(/stale/i);
|
|
153
|
+
expect(outro).toMatch(/docs/);
|
|
154
|
+
expect(outro).not.toMatch(/^✅/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('awm update — nunca se cuelga esperando a un humano que no está', () => {
|
|
158
|
+
const realHome = process.env.HOME;
|
|
159
|
+
const realAwmHome = process.env.AWM_HOME;
|
|
160
|
+
let tmpHome;
|
|
161
|
+
let logSpy;
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-update-tty-'));
|
|
164
|
+
process.env.HOME = tmpHome;
|
|
165
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
166
|
+
fs_1.default.mkdirSync(process.env.AWM_HOME, { recursive: true });
|
|
167
|
+
writePrefs();
|
|
168
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
169
|
+
jest.resetModules();
|
|
170
|
+
});
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
logSpy.mockRestore();
|
|
173
|
+
if (realHome === undefined)
|
|
174
|
+
delete process.env.HOME;
|
|
175
|
+
else
|
|
176
|
+
process.env.HOME = realHome;
|
|
177
|
+
if (realAwmHome === undefined)
|
|
178
|
+
delete process.env.AWM_HOME;
|
|
179
|
+
else
|
|
180
|
+
process.env.AWM_HOME = realAwmHome;
|
|
181
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
182
|
+
});
|
|
183
|
+
function updateDeps(offerSelfUpdate) {
|
|
184
|
+
return {
|
|
185
|
+
syncRegistries: async () => [{ name: 'baseline', action: 'pulled', version: 'v1.15.1' }],
|
|
186
|
+
verifyMinCliVersions: () => [],
|
|
187
|
+
regenerateGlobalContext: () => [],
|
|
188
|
+
planReconciliation: () => ({ operations: [], records: [], reports: [] }),
|
|
189
|
+
applyInstallPlan: () => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }),
|
|
190
|
+
resyncInstalledHooks: () => [],
|
|
191
|
+
offerSelfUpdate,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
it('`--yes` pide assume-yes; sin flag delega la decisión al modo por defecto', async () => {
|
|
195
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
196
|
+
const withFlag = jest.fn(async () => { });
|
|
197
|
+
await runUpdateCore({ yes: true }, updateDeps(withFlag));
|
|
198
|
+
expect(withFlag).toHaveBeenCalledWith('assume-yes');
|
|
199
|
+
const withoutFlag = jest.fn(async () => { });
|
|
200
|
+
await runUpdateCore({}, updateDeps(withoutFlag));
|
|
201
|
+
// `undefined`, no `'prompt'`: quién decide es `defaultSelfUpdateMode()`, según
|
|
202
|
+
// haya o no alguien en stdin. Fijar 'prompt' acá reintroduciría el cuelgue.
|
|
203
|
+
expect(withoutFlag).toHaveBeenCalledWith(undefined);
|
|
204
|
+
});
|
|
205
|
+
describe('defaultSelfUpdateMode / offerSelfUpdate', () => {
|
|
206
|
+
const realIsTTY = process.stdin.isTTY;
|
|
207
|
+
const realNoCheck = process.env.AWM_NO_UPDATE_CHECK;
|
|
208
|
+
beforeEach(() => { delete process.env.AWM_NO_UPDATE_CHECK; });
|
|
209
|
+
afterEach(() => {
|
|
210
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: realIsTTY, configurable: true });
|
|
211
|
+
if (realNoCheck === undefined)
|
|
212
|
+
delete process.env.AWM_NO_UPDATE_CHECK;
|
|
213
|
+
else
|
|
214
|
+
process.env.AWM_NO_UPDATE_CHECK = realNoCheck;
|
|
215
|
+
});
|
|
216
|
+
it('sin TTY el modo por defecto es skip; con TTY es prompt', () => {
|
|
217
|
+
const { defaultSelfUpdateMode } = require('../../src/core/update-check');
|
|
218
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
|
|
219
|
+
expect(defaultSelfUpdateMode()).toBe('skip');
|
|
220
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
221
|
+
expect(defaultSelfUpdateMode()).toBe('prompt');
|
|
222
|
+
});
|
|
223
|
+
it('sin TTY no pregunta ni actualiza: avisa y sigue', async () => {
|
|
224
|
+
const { offerSelfUpdate } = require('../../src/core/update-check');
|
|
225
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
|
|
226
|
+
const confirmImpl = jest.fn(async () => true);
|
|
227
|
+
const runner = jest.fn(() => ({ status: 0 }));
|
|
228
|
+
await offerSelfUpdate({ current: '6.2.1', latest: '6.3.0', confirmImpl, runner });
|
|
229
|
+
expect(confirmImpl).not.toHaveBeenCalled();
|
|
230
|
+
// El silencio no autoriza reemplazar el binario global de la máquina.
|
|
231
|
+
expect(runner).not.toHaveBeenCalled();
|
|
232
|
+
expect(logSpy.mock.calls.flat().join('\n')).toMatch(/npm i -g/);
|
|
233
|
+
});
|
|
234
|
+
it("modo 'assume-yes' actualiza sin preguntar", async () => {
|
|
235
|
+
const { offerSelfUpdate } = require('../../src/core/update-check');
|
|
236
|
+
const confirmImpl = jest.fn(async () => true);
|
|
237
|
+
const runner = jest.fn(() => ({ status: 0 }));
|
|
238
|
+
await offerSelfUpdate({ current: '6.2.1', latest: '6.3.0', mode: 'assume-yes', confirmImpl, runner });
|
|
239
|
+
expect(confirmImpl).not.toHaveBeenCalled();
|
|
240
|
+
expect(runner).toHaveBeenCalledWith('npm', ['i', '-g', expect.stringContaining('@latest')]);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -478,6 +478,56 @@ describe('aplicacion transaccional de requests', () => {
|
|
|
478
478
|
expect(fs_1.default.existsSync(`${r.file}.rejected`)).toBe(true);
|
|
479
479
|
expect((0, store_1.readJournal)(repo, 'rama').state.trackIntegration).toBeUndefined();
|
|
480
480
|
});
|
|
481
|
+
// El modo de falla que estos dos tests cierran: `applyRequestToState` era una cadena de
|
|
482
|
+
// `if (env.kind === …) { …; return; }` que se caía POR EL FINAL ante un kind sin handler.
|
|
483
|
+
// El caller no veía excepción, hacía `applied++` y BORRABA el archivo. `awm track join`
|
|
484
|
+
// vivió así: salía 0 con su requestId, el journal contaba la request como aplicada, y el
|
|
485
|
+
// track se quedaba en `ACTIVE` para siempre — la cohorte no podía congelarse (C3) y por
|
|
486
|
+
// lo tanto nunca llegaba a `COMPLETE`.
|
|
487
|
+
test('track-join-request marca la intención de join sobre un track existente (declarativo, R6.1)', () => {
|
|
488
|
+
const s0 = (0, store_1.readJournal)(repo, 'rama').state;
|
|
489
|
+
const alpha = {
|
|
490
|
+
trackId: 'alpha', worktreePath: path_1.default.join(repo, '.track-alpha'), branch: 'awm-track/alpha',
|
|
491
|
+
ownership: [], sharedResources: [], dependsOn: [],
|
|
492
|
+
fencingToken: 'f'.repeat(32), phase: 'ACTIVE', readinessNonce: 'n'.repeat(32),
|
|
493
|
+
};
|
|
494
|
+
s0.tracks = [alpha];
|
|
495
|
+
(0, store_1.writeJournal)(repo, 'rama', s0);
|
|
496
|
+
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
497
|
+
kind: 'track-join-request', generationToken: 'g1', idempotencyKey: 'join-alpha',
|
|
498
|
+
payload: { trackId: 'alpha' },
|
|
499
|
+
});
|
|
500
|
+
expect((0, apply_1.consumePendingRequests)(repo, 'rama', 'g1').applied).toBe(1);
|
|
501
|
+
const s = (0, store_1.readJournal)(repo, 'rama').state;
|
|
502
|
+
expect(s.tracks.find((t) => t.trackId === 'alpha').joinRequested).toBe(true);
|
|
503
|
+
// La FASE no la mueve este consumo: eso es del reducer puro vía `reconcileTracks`.
|
|
504
|
+
expect(s.tracks.find((t) => t.trackId === 'alpha').phase).toBe('ACTIVE');
|
|
505
|
+
});
|
|
506
|
+
test('un join sobre un track inexistente se RECHAZA en vez de absorberse en silencio', () => {
|
|
507
|
+
const r = (0, requests_1.emitRequest)(repo, 'rama', {
|
|
508
|
+
kind: 'track-join-request', generationToken: 'g1', idempotencyKey: 'join-fantasma',
|
|
509
|
+
payload: { trackId: 'no-existe' },
|
|
510
|
+
});
|
|
511
|
+
const out = (0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
512
|
+
expect(out.applied).toBe(0);
|
|
513
|
+
expect(out.rejectedInvalid).toBe(1);
|
|
514
|
+
expect(fs_1.default.existsSync(`${r.file}.rejected`)).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
test('un kind SIN handler falla cerrado: rechazo visible, no un `applied` que borra el archivo', () => {
|
|
517
|
+
// `track-teardown-request` es un kind real, emitido por `awm track remove`, que hoy
|
|
518
|
+
// no tiene handler en el supervisor. Antes de este cambio se contaba como aplicado y
|
|
519
|
+
// desaparecía sin dejar rastro; ahora queda registrado como problema y el archivo se
|
|
520
|
+
// conserva con sufijo `.rejected`, que es lo que permite darse cuenta.
|
|
521
|
+
const r = (0, requests_1.emitRequest)(repo, 'rama', {
|
|
522
|
+
kind: 'track-teardown-request', generationToken: 'g1', idempotencyKey: 'teardown-1',
|
|
523
|
+
payload: { trackId: 'alpha' },
|
|
524
|
+
});
|
|
525
|
+
const out = (0, apply_1.consumePendingRequests)(repo, 'rama', 'g1');
|
|
526
|
+
expect(out.applied).toBe(0);
|
|
527
|
+
expect(out.rejectedInvalid).toBe(1);
|
|
528
|
+
expect(fs_1.default.existsSync(`${r.file}.rejected`)).toBe(true);
|
|
529
|
+
expect((0, store_1.readJournal)(repo, 'rama').state.requestProblems.some((p) => p.kind === 'rejected')).toBe(true);
|
|
530
|
+
});
|
|
481
531
|
test('track-finalize-request persiste el autoreporte del controller (qaFinalizeRequested) — puramente declarativo (R7.2)', () => {
|
|
482
532
|
(0, requests_1.emitRequest)(repo, 'rama', {
|
|
483
533
|
kind: 'track-finalize-request', generationToken: 'g1', idempotencyKey: 'finalize-1',
|