agentic-workflow-manager 9.6.0 → 9.7.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 +2 -0
- package/dist/src/commands/context/index.js +3 -1
- package/dist/src/commands/track/index.js +3 -7
- package/dist/src/commands/watch/apply.js +15 -0
- package/dist/src/commands/watch/tracks.js +56 -7
- package/dist/src/core/discovery.js +79 -4
- package/dist/src/core/journal/types.js +1 -0
- package/dist/src/core/orchestrators.js +34 -10
- package/dist/src/core/text.js +7 -0
- package/dist/src/core/tracks/git.js +15 -3
- package/dist/src/core/tracks/protocol.js +26 -6
- package/dist/tests/commands/context.test.js +9 -1
- package/dist/tests/commands/hooks/install.test.js +4 -0
- package/dist/tests/commands/hooks/resync.test.js +2 -0
- package/dist/tests/commands/track/verbs.test.js +6 -0
- package/dist/tests/commands/watch/apply.test.js +46 -9
- package/dist/tests/commands/watch/track-remove-request.test.js +165 -0
- package/dist/tests/commands/watch/track-teardown-crash.test.js +27 -0
- package/dist/tests/core/context/orchestrator.test.js +2 -0
- package/dist/tests/core/discovery-multiroot.test.js +5 -0
- package/dist/tests/core/discovery-parent-race.test.js +95 -0
- package/dist/tests/core/discovery-safe-read.test.js +115 -0
- package/dist/tests/core/discovery.test.js +28 -1
- package/dist/tests/core/journal/types.test.js +11 -0
- package/dist/tests/core/orchestrators.test.js +171 -14
- package/dist/tests/core/text.test.js +10 -0
- package/dist/tests/core/tracks/git.test.js +7 -0
- package/dist/tests/core/tracks/protocol.test.js +103 -0
- package/dist/tests/integration/context-orchestrators-e2e.test.js +57 -7
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const fs_1 = __importDefault(require("fs"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const command_result_1 = require("../../src/core/command-result");
|
|
9
10
|
const orchestrators_1 = require("../../src/core/orchestrators");
|
|
10
11
|
const registries_1 = require("../../src/core/registries");
|
|
11
12
|
function registryWith(manifest) {
|
|
@@ -16,6 +17,7 @@ function registryWith(manifest) {
|
|
|
16
17
|
describe('readDeclaredOrchestrators', () => {
|
|
17
18
|
const created = [];
|
|
18
19
|
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
19
21
|
for (const r of created.splice(0))
|
|
20
22
|
fs_1.default.rmSync(r, { recursive: true, force: true });
|
|
21
23
|
});
|
|
@@ -176,6 +178,62 @@ describe('readDeclaredOrchestrators', () => {
|
|
|
176
178
|
expect(diagnostics[0].split('\n')).toHaveLength(1);
|
|
177
179
|
expect(diagnostics[0]).toContain(JSON.stringify('evil\nfake-warning: injected line'));
|
|
178
180
|
});
|
|
181
|
+
describe('single-line diagnostic boundary', () => {
|
|
182
|
+
const hostile = 'before\u2028\u2029\u0085\r\n\t'
|
|
183
|
+
+ String.fromCharCode(...Array.from({ length: 32 }, (_, i) => i))
|
|
184
|
+
+ String.fromCharCode(...Array.from({ length: 33 }, (_, i) => 127 + i)) + 'after';
|
|
185
|
+
it('sanitizes unknown metadata keys without accepting the declaration', () => {
|
|
186
|
+
const root = registryWith({
|
|
187
|
+
orchestrator: { name: 'x', appliesWhen: 'y', terminatesTo: 'none', [hostile]: true },
|
|
188
|
+
});
|
|
189
|
+
created.push(root);
|
|
190
|
+
const result = (0, orchestrators_1.readDeclaredOrchestrators)(root);
|
|
191
|
+
expect(result.orchestrators).toEqual([]);
|
|
192
|
+
expect(result.diagnostics).toHaveLength(1);
|
|
193
|
+
expect(result.diagnostics[0]).toContain('unknown field');
|
|
194
|
+
expect(result.diagnostics[0]).toContain('before');
|
|
195
|
+
expect(result.diagnostics[0]).toContain('after');
|
|
196
|
+
const stderrBoundary = jest.fn(command_result_1.diagnosticsToStderr);
|
|
197
|
+
stderrBoundary(result.diagnostics);
|
|
198
|
+
expect(stderrBoundary.mock.calls[0][0].join('')).not.toMatch(/[\u2028\u0085]/);
|
|
199
|
+
expect(stderrBoundary.mock.results[0].value).not.toMatch(/[\u2028\u0085]/);
|
|
200
|
+
// eslint-disable-next-line no-control-regex -- reject all terminal controls and line separators
|
|
201
|
+
expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
202
|
+
});
|
|
203
|
+
it.each(['inspection', 'read', 'parse', 'shape', 'fields'])('sanitizes %s diagnostics, including paths and error text', (branch) => {
|
|
204
|
+
// Mock hostile paths instead of creating filenames that are invalid on Windows.
|
|
205
|
+
const root = path_1.default.join(os_1.default.tmpdir(), hostile);
|
|
206
|
+
const error = new Error(hostile);
|
|
207
|
+
jest.spyOn(fs_1.default, 'lstatSync').mockImplementation(() => {
|
|
208
|
+
if (branch === 'inspection')
|
|
209
|
+
throw error;
|
|
210
|
+
return { isFile: () => true, isSymbolicLink: () => false };
|
|
211
|
+
});
|
|
212
|
+
jest.spyOn(fs_1.default, 'readFileSync').mockImplementation(() => {
|
|
213
|
+
if (branch === 'read')
|
|
214
|
+
throw error;
|
|
215
|
+
if (branch === 'parse')
|
|
216
|
+
return `{ ${hostile}`;
|
|
217
|
+
return JSON.stringify({ orchestrator: branch === 'shape' ? [] : {} });
|
|
218
|
+
});
|
|
219
|
+
if (branch === 'parse')
|
|
220
|
+
jest.spyOn(JSON, 'parse').mockImplementationOnce(() => { throw new SyntaxError(hostile); });
|
|
221
|
+
const result = (0, orchestrators_1.readDeclaredOrchestrators)(root);
|
|
222
|
+
expect(result.orchestrators).toEqual([]);
|
|
223
|
+
expect(result.diagnostics).toHaveLength(1);
|
|
224
|
+
expect(result.diagnostics[0]).toContain('awm-registry.json');
|
|
225
|
+
expect(result.diagnostics[0]).toContain('before');
|
|
226
|
+
expect(result.diagnostics[0]).toContain('after');
|
|
227
|
+
// eslint-disable-next-line no-control-regex -- reject all terminal controls and line separators
|
|
228
|
+
expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
229
|
+
});
|
|
230
|
+
it('preserves valid raw fields and structural parsing without skill discovery', () => {
|
|
231
|
+
const declaration = { name: hostile, appliesWhen: hostile, terminatesTo: hostile };
|
|
232
|
+
const root = registryWith({ orchestrator: declaration });
|
|
233
|
+
created.push(root);
|
|
234
|
+
expect((0, orchestrators_1.readDeclaredOrchestrators)(root)).toEqual({ orchestrators: [declaration], diagnostics: [] });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
179
237
|
});
|
|
180
238
|
describe('collectDeclaredOrchestrators', () => {
|
|
181
239
|
// Isolates AWM_HOME per CLAUDE.md's testing rule ("ningun test puede tocar el ~/.awm
|
|
@@ -199,6 +257,11 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
199
257
|
fs_1.default.mkdirSync(root, { recursive: true });
|
|
200
258
|
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify(manifest));
|
|
201
259
|
}
|
|
260
|
+
function writeSkill(registryName, skillName) {
|
|
261
|
+
const skillDir = path_1.default.join((0, registries_1.registryContentRoot)(registryName), 'skills', skillName);
|
|
262
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
263
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), `---\nname: ${skillName}\n---\n\n# ${skillName}\n`);
|
|
264
|
+
}
|
|
202
265
|
it('dedupea por nombre entre dos registries, conservando el primero por orden de registries.json', () => {
|
|
203
266
|
(0, registries_1.writeRegistriesConfig)([
|
|
204
267
|
{ name: 'first', remote: 'unused' },
|
|
@@ -210,6 +273,7 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
210
273
|
writeManifest('second', {
|
|
211
274
|
orchestrator: { name: 'shared', appliesWhen: 'segundo', terminatesTo: 'b' },
|
|
212
275
|
});
|
|
276
|
+
writeSkill('first', 'shared');
|
|
213
277
|
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
214
278
|
expect(declared).toHaveLength(1);
|
|
215
279
|
expect(declared[0]).toEqual({ name: 'shared', appliesWhen: 'primero', terminatesTo: 'a' });
|
|
@@ -217,23 +281,60 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
217
281
|
expect(diagnostics[0]).toMatch(/shared/);
|
|
218
282
|
expect(diagnostics[0]).toMatch(/duplicate|shadow/i);
|
|
219
283
|
});
|
|
220
|
-
it('
|
|
221
|
-
(0, registries_1.writeRegistriesConfig)([
|
|
222
|
-
|
|
223
|
-
{ name: 'second', remote: 'unused' },
|
|
224
|
-
]);
|
|
225
|
-
const nombreHostil = 'shared\x1b[31m';
|
|
284
|
+
it('keeps missing-skill diagnostics single-line with hostile metadata and portable paths', () => {
|
|
285
|
+
(0, registries_1.writeRegistriesConfig)([{ name: 'first', remote: 'unused' }]);
|
|
286
|
+
const nombreHostil = 'shared\r\nforged\t\x1b[31m\x00\x7f\x85\x9b\u2028\u2029';
|
|
226
287
|
writeManifest('first', {
|
|
227
288
|
orchestrator: { name: nombreHostil, appliesWhen: 'primero', terminatesTo: 'a' },
|
|
228
289
|
});
|
|
229
|
-
|
|
230
|
-
orchestrator: { name: nombreHostil, appliesWhen: 'segundo', terminatesTo: 'b' },
|
|
231
|
-
});
|
|
290
|
+
writeSkill('first', 'real-skill');
|
|
232
291
|
const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
233
292
|
expect(diagnostics).toHaveLength(1);
|
|
234
|
-
// eslint-disable-next-line no-control-regex --
|
|
235
|
-
expect(diagnostics[0]).not.toMatch(/[\x00-\
|
|
236
|
-
expect(diagnostics[0]).toContain('shared
|
|
293
|
+
// eslint-disable-next-line no-control-regex -- diagnostic must contain no terminal controls or line separators
|
|
294
|
+
expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
295
|
+
expect(diagnostics[0]).toContain('shared');
|
|
296
|
+
expect(diagnostics[0]).toContain('forged');
|
|
297
|
+
expect(diagnostics[0]).toContain('declaration dropped');
|
|
298
|
+
});
|
|
299
|
+
it('keeps discovery diagnostics single-line for hostile override metadata', () => {
|
|
300
|
+
(0, registries_1.writeRegistriesConfig)([{ name: 'hostile', remote: 'unused' }]);
|
|
301
|
+
writeManifest('hostile', { overrides: ['../forged\r\nwarning:\t\x1b[31m\x85\u2028'] });
|
|
302
|
+
writeSkill('hostile', 'real-skill');
|
|
303
|
+
const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
304
|
+
expect(diagnostics).toHaveLength(1);
|
|
305
|
+
expect(diagnostics[0]).toContain('discovery unavailable');
|
|
306
|
+
expect(diagnostics[0]).toContain('forged');
|
|
307
|
+
// eslint-disable-next-line no-control-regex -- diagnostic must contain no terminal controls or line separators
|
|
308
|
+
expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
|
|
309
|
+
});
|
|
310
|
+
it('excludes a later colliding root without losing earlier or subsequent healthy roots', () => {
|
|
311
|
+
(0, registries_1.writeRegistriesConfig)(['first', 'collision', 'last'].map((name) => ({ name, remote: 'unused' })));
|
|
312
|
+
for (const name of ['first', 'collision', 'last']) {
|
|
313
|
+
writeManifest(name, { orchestrator: { name: `${name}-process`, appliesWhen: 'x', terminatesTo: 'none' } });
|
|
314
|
+
writeSkill(name, `${name}-process`);
|
|
315
|
+
}
|
|
316
|
+
writeSkill('first', 'shared');
|
|
317
|
+
writeSkill('collision', 'shared');
|
|
318
|
+
// This name belongs only to the excluded root and must not poison later admission.
|
|
319
|
+
writeSkill('collision', 'last-process');
|
|
320
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
321
|
+
expect(declared.map((d) => d.name)).toEqual(['first-process', 'last-process']);
|
|
322
|
+
expect(diagnostics).toHaveLength(2);
|
|
323
|
+
expect(diagnostics[0]).toMatch(/collision.*discovery unavailable.*Artifact name collision/);
|
|
324
|
+
expect(diagnostics[1]).toMatch(/collision-process.*not discoverable/);
|
|
325
|
+
});
|
|
326
|
+
it('admits a later root with a declared override', () => {
|
|
327
|
+
(0, registries_1.writeRegistriesConfig)(['first', 'override'].map((name) => ({ name, remote: 'unused' })));
|
|
328
|
+
writeSkill('first', 'shared');
|
|
329
|
+
writeSkill('override', 'shared');
|
|
330
|
+
writeSkill('override', 'override-process');
|
|
331
|
+
writeManifest('override', {
|
|
332
|
+
overrides: ['shared'],
|
|
333
|
+
orchestrator: { name: 'override-process', appliesWhen: 'x', terminatesTo: 'none' },
|
|
334
|
+
});
|
|
335
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
336
|
+
expect(declared.map((d) => d.name)).toEqual(['override-process']);
|
|
337
|
+
expect(diagnostics).toEqual([]);
|
|
237
338
|
});
|
|
238
339
|
it('emite un diagnostico de colision post-saneo entre nombres crudos distintos, sin descartar ninguno', () => {
|
|
239
340
|
(0, registries_1.writeRegistriesConfig)([
|
|
@@ -244,12 +345,14 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
244
345
|
orchestrator: { name: 'foo_bar', appliesWhen: 'x', terminatesTo: 'a' },
|
|
245
346
|
});
|
|
246
347
|
writeManifest('second', {
|
|
247
|
-
orchestrator: { name: '
|
|
348
|
+
orchestrator: { name: 'foobar', appliesWhen: 'y', terminatesTo: 'b' },
|
|
248
349
|
});
|
|
350
|
+
writeSkill('first', 'foo_bar');
|
|
351
|
+
writeSkill('second', 'foobar');
|
|
249
352
|
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
250
353
|
// Ambas declaraciones son genuinamente distintas (nombre crudo distinto) — ninguna se descarta.
|
|
251
354
|
expect(declared).toHaveLength(2);
|
|
252
|
-
expect(declared.map((d) => d.name).sort()).toEqual(['
|
|
355
|
+
expect(declared.map((d) => d.name).sort()).toEqual(['foo_bar', 'foobar']);
|
|
253
356
|
// Pero se advierte de la colision post-saneo (ambas renderizan como "foobar").
|
|
254
357
|
expect(diagnostics).toHaveLength(1);
|
|
255
358
|
expect(diagnostics[0]).toMatch(/foobar/);
|
|
@@ -266,9 +369,63 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
266
369
|
writeManifest('second', {
|
|
267
370
|
orchestrator: { name: 'dos', appliesWhen: 'y', terminatesTo: 'b' },
|
|
268
371
|
});
|
|
372
|
+
writeSkill('first', 'uno');
|
|
373
|
+
writeSkill('second', 'dos');
|
|
269
374
|
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
270
375
|
expect(declared).toHaveLength(2);
|
|
271
376
|
expect(declared.map((d) => d.name).sort()).toEqual(['dos', 'uno']);
|
|
272
377
|
expect(diagnostics).toEqual([]);
|
|
273
378
|
});
|
|
379
|
+
it('omite una declaracion cuyo skill no existe y la diagnostica', () => {
|
|
380
|
+
(0, registries_1.writeRegistriesConfig)([{ name: 'phantom', remote: 'unused' }]);
|
|
381
|
+
writeManifest('phantom', {
|
|
382
|
+
orchestrator: { name: 'phantom-process', appliesWhen: 'x', terminatesTo: 'none' },
|
|
383
|
+
});
|
|
384
|
+
writeSkill('phantom', 'real-skill');
|
|
385
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
386
|
+
expect(declared).toEqual([]);
|
|
387
|
+
expect(diagnostics).toHaveLength(1);
|
|
388
|
+
expect(diagnostics[0]).toMatch(/phantom-process/);
|
|
389
|
+
expect(diagnostics[0]).toMatch(/declaration dropped|skill/i);
|
|
390
|
+
});
|
|
391
|
+
it('resuelve una declaracion contra un skill de otro registry configurado', () => {
|
|
392
|
+
(0, registries_1.writeRegistriesConfig)([
|
|
393
|
+
{ name: 'declarations', remote: 'unused' },
|
|
394
|
+
{ name: 'skills', remote: 'unused' },
|
|
395
|
+
]);
|
|
396
|
+
writeManifest('declarations', {
|
|
397
|
+
orchestrator: { name: 'cross-registry', appliesWhen: 'x', terminatesTo: 'none' },
|
|
398
|
+
});
|
|
399
|
+
writeSkill('skills', 'cross-registry');
|
|
400
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
401
|
+
expect(declared).toEqual([{ name: 'cross-registry', appliesWhen: 'x', terminatesTo: 'none' }]);
|
|
402
|
+
expect(diagnostics).toEqual([]);
|
|
403
|
+
});
|
|
404
|
+
it('diagnostica un root cuyo discovery falla y conserva una declaracion sana', () => {
|
|
405
|
+
(0, registries_1.writeRegistriesConfig)([
|
|
406
|
+
{ name: 'broken', remote: 'unused' },
|
|
407
|
+
{ name: 'healthy', remote: 'unused' },
|
|
408
|
+
]);
|
|
409
|
+
writeManifest('broken', {
|
|
410
|
+
orchestrator: { name: 'broken-process', appliesWhen: 'x', terminatesTo: 'none' },
|
|
411
|
+
});
|
|
412
|
+
fs_1.default.writeFileSync(path_1.default.join((0, registries_1.registryContentRoot)('broken'), 'skills'), 'not a directory');
|
|
413
|
+
writeManifest('healthy', {
|
|
414
|
+
orchestrator: { name: 'healthy-process', appliesWhen: 'x', terminatesTo: 'none' },
|
|
415
|
+
});
|
|
416
|
+
writeSkill('healthy', 'healthy-process');
|
|
417
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
418
|
+
expect(declared).toEqual([{ name: 'healthy-process', appliesWhen: 'x', terminatesTo: 'none' }]);
|
|
419
|
+
expect(diagnostics.join('\n')).toMatch(/broken.*discovery unavailable/i);
|
|
420
|
+
});
|
|
421
|
+
it('conserva terminatesTo aunque no corresponda a un skill descubierto', () => {
|
|
422
|
+
(0, registries_1.writeRegistriesConfig)([{ name: 'registry', remote: 'unused' }]);
|
|
423
|
+
writeManifest('registry', {
|
|
424
|
+
orchestrator: { name: 'entry-process', appliesWhen: 'x', terminatesTo: 'missing-successor' },
|
|
425
|
+
});
|
|
426
|
+
writeSkill('registry', 'entry-process');
|
|
427
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
428
|
+
expect(declared).toEqual([{ name: 'entry-process', appliesWhen: 'x', terminatesTo: 'missing-successor' }]);
|
|
429
|
+
expect(diagnostics).toEqual([]);
|
|
430
|
+
});
|
|
274
431
|
});
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const text_1 = require("../../src/core/text");
|
|
4
|
+
describe('sanitizeDiagnosticText', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['a\r\nb\rc\nd\te', 'a b c d e'],
|
|
7
|
+
['a\x00\x07\x1b[31m\x7f\x85\x9bz', 'a[31mz'],
|
|
8
|
+
['a\u2028b\u2029c', 'a b c'],
|
|
9
|
+
['plain *markdown* — café', 'plain *markdown* — café'],
|
|
10
|
+
])('sanitizes a single diagnostic field: %j', (input, expected) => {
|
|
11
|
+
expect((0, text_1.sanitizeDiagnosticText)(input)).toBe(expected);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
4
14
|
describe('stripControlChars', () => {
|
|
5
15
|
it('elimina ESC y demas bytes de control C0', () => {
|
|
6
16
|
expect((0, text_1.stripControlChars)('a\x1b[31mb\x00c\x07d')).toBe('a[31mbcd');
|
|
@@ -128,5 +128,12 @@ describe('git adapter', () => {
|
|
|
128
128
|
(0, child_process_1.execFileSync)('git', ['branch', 'awm-track/x'], { cwd: repo });
|
|
129
129
|
expect((0, git_1.branchExists)(repo, 'awm-track/x')).toBe(true);
|
|
130
130
|
});
|
|
131
|
+
test('branchExists falla cerrado cuando git no puede probar la ausencia', () => {
|
|
132
|
+
repo = (0, git_fixture_1.initRepo)();
|
|
133
|
+
(0, git_fixture_1.commitFile)(repo, 'a.txt', 'x');
|
|
134
|
+
const gitDir = path_1.default.join(repo, '.git');
|
|
135
|
+
fs_1.default.renameSync(gitDir, `${gitDir}.unavailable`);
|
|
136
|
+
expect(() => (0, git_1.branchExists)(repo, 'awm-track/nope')).toThrow(/indemostrable/);
|
|
137
|
+
});
|
|
131
138
|
});
|
|
132
139
|
});
|
|
@@ -34,6 +34,109 @@ describe('parallel-track protocol model', () => {
|
|
|
34
34
|
expect(() => (0, protocol_1.observeProtocolEffect)(s, { kind: 'enter-serial', reason: 'x' }, { kind: 'effect-applied', effect: { kind: 'enter-serial', reason: 'x' } }))
|
|
35
35
|
.toThrow(/SERIAL prohíbe recursos paralelos activos/);
|
|
36
36
|
});
|
|
37
|
+
test.each([
|
|
38
|
+
{
|
|
39
|
+
name: 'observación de ownership ajeno',
|
|
40
|
+
observation: { kind: 'teardown-observation', trackId: 'cli', foreignWorktree: true },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'fallo de begin-teardown',
|
|
44
|
+
observation: {
|
|
45
|
+
kind: 'effect-failed', trackId: 'cli', effect: 'begin-teardown', detail: 'ownership indemostrable',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
])('teardown indemostrable bloquea track y cohorte: $name (TR-REQ-05)', ({ observation }) => {
|
|
49
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
50
|
+
s.cohortPhase = 'FALLBACK_PENDING';
|
|
51
|
+
s.fallbackReason = 'controller-requested:cli';
|
|
52
|
+
s.tracks.cli.phase = 'TEARDOWN_REQUESTED';
|
|
53
|
+
s.tracks.docs.phase = 'REMOVED';
|
|
54
|
+
const out = (0, protocol_1.reconcileProtocol)(s, observation);
|
|
55
|
+
expect(out.tracks.cli.phase).toBe('BLOCKED');
|
|
56
|
+
expect(out.cohortPhase).toBe('BLOCKED');
|
|
57
|
+
expect((0, protocol_1.nextProtocolEffect)(out)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
test('fallo de begin-teardown para track inexistente falla cerrado (TR-REQ-05)', () => {
|
|
60
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
61
|
+
s.cohortPhase = 'FALLBACK_PENDING';
|
|
62
|
+
expect(() => (0, protocol_1.reconcileProtocol)(s, {
|
|
63
|
+
kind: 'effect-failed', trackId: 'missing', effect: 'begin-teardown', detail: 'ownership indemostrable',
|
|
64
|
+
})).toThrow('track desconocido: missing');
|
|
65
|
+
});
|
|
66
|
+
test('remove cancela toda la cohorte activa y conserva DECLARED/BLOCKED (TR-REQ-03, TR-REQ-05)', () => {
|
|
67
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ['active', 'declared', 'blocked']);
|
|
68
|
+
s.cohortPhase = 'ACTIVE';
|
|
69
|
+
s.tracks.active.phase = 'ACTIVE';
|
|
70
|
+
s.tracks.declared.phase = 'DECLARED';
|
|
71
|
+
s.tracks.blocked.phase = 'BLOCKED';
|
|
72
|
+
const out = (0, protocol_1.reconcileProtocol)(s, { kind: 'teardown-requested', trackId: 'active' });
|
|
73
|
+
expect(out.cohortPhase).toBe('FALLBACK_PENDING');
|
|
74
|
+
expect(out.fallbackReason).toBe('controller-requested:active');
|
|
75
|
+
expect(out.tracks.active.phase).toBe('TEARDOWN_REQUESTED');
|
|
76
|
+
expect(out.tracks.declared.phase).toBe('DECLARED');
|
|
77
|
+
expect(out.tracks.blocked.phase).toBe('BLOCKED');
|
|
78
|
+
expect((0, protocol_1.nextProtocolEffect)(out)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
test('remove durante FINAL_INTERLOCK invalida evidencia final antes de pedir teardown (issue #93)', () => {
|
|
81
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
82
|
+
s.cohortPhase = 'FINAL_INTERLOCK';
|
|
83
|
+
s.globalQaHeadSha = 'qa-head';
|
|
84
|
+
s.finalIntegrationJobId = 'integration-job';
|
|
85
|
+
for (const track of Object.values(s.tracks)) {
|
|
86
|
+
track.phase = 'MERGED_UNVERIFIED';
|
|
87
|
+
track.frozenHeadSha = `${track.trackId}-head`;
|
|
88
|
+
}
|
|
89
|
+
const out = (0, protocol_1.reconcileProtocol)(s, { kind: 'teardown-requested', trackId: 'cli' });
|
|
90
|
+
expect(out.cohortPhase).toBe('FALLBACK_PENDING');
|
|
91
|
+
expect(out.globalQaHeadSha).toBeUndefined();
|
|
92
|
+
expect(out.finalIntegrationJobId).toBeUndefined();
|
|
93
|
+
expect(Object.values(out.tracks).every((track) => track.phase === 'TEARDOWN_REQUESTED')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
test('remove repetido no rebobina teardown ni reemplaza la causa (TR-REQ-08)', () => {
|
|
96
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
97
|
+
s.cohortPhase = 'FALLBACK_PENDING';
|
|
98
|
+
s.fallbackReason = 'controller-requested:cli';
|
|
99
|
+
s.tracks.cli.phase = 'SUPERVISOR_STOPPED';
|
|
100
|
+
s.tracks.docs.phase = 'TEARDOWN_REQUESTED';
|
|
101
|
+
const out = (0, protocol_1.reconcileProtocol)(s, { kind: 'teardown-requested', trackId: 'docs' });
|
|
102
|
+
expect(out.fallbackReason).toBe('controller-requested:cli');
|
|
103
|
+
expect(out.tracks.cli.phase).toBe('SUPERVISOR_STOPPED');
|
|
104
|
+
expect(out.tracks.docs.phase).toBe('TEARDOWN_REQUESTED');
|
|
105
|
+
});
|
|
106
|
+
test.each(['SERIAL', 'COMPLETE'])('remove es no-op protocolar en cohorte terminal %s (TR-REQ-09)', (cohortPhase) => {
|
|
107
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
108
|
+
s.cohortPhase = cohortPhase;
|
|
109
|
+
for (const track of Object.values(s.tracks)) {
|
|
110
|
+
track.phase = cohortPhase === 'SERIAL' ? 'REMOVED' : 'JOINED';
|
|
111
|
+
if (cohortPhase === 'COMPLETE')
|
|
112
|
+
track.frozenHeadSha = `${track.trackId}-head`;
|
|
113
|
+
}
|
|
114
|
+
expect((0, protocol_1.reconcileProtocol)(s, { kind: 'teardown-requested', trackId: 'cli' })).toEqual(s);
|
|
115
|
+
});
|
|
116
|
+
test('remove es no-op protocolar en cohorte BLOCKED y no reanuda teardown (TR-REQ-10)', () => {
|
|
117
|
+
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
118
|
+
s.cohortPhase = 'BLOCKED';
|
|
119
|
+
s.tracks.cli.phase = 'BLOCKED';
|
|
120
|
+
s.tracks.cli.blockedReason = 'ownership indemostrable';
|
|
121
|
+
s.tracks.docs.phase = 'REMOVED';
|
|
122
|
+
const out = (0, protocol_1.reconcileProtocol)(s, { kind: 'teardown-requested', trackId: 'docs' });
|
|
123
|
+
expect(out).toEqual(s);
|
|
124
|
+
expect(out.cohortPhase).toBe('BLOCKED');
|
|
125
|
+
expect((0, protocol_1.nextProtocolEffect)(out)).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
test('remove converge a serial solo después de desmontar la cohorte (TR-REQ-06)', () => {
|
|
128
|
+
let s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
129
|
+
for (const track of Object.values(s.tracks))
|
|
130
|
+
track.phase = 'ACTIVE';
|
|
131
|
+
s.cohortPhase = 'ACTIVE';
|
|
132
|
+
s = (0, protocol_1.reconcileProtocol)(s, { kind: 'teardown-requested', trackId: 'cli' });
|
|
133
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({ kind: 'begin-teardown', trackId: 'cli' });
|
|
134
|
+
s.tracks.cli.phase = 'REMOVED';
|
|
135
|
+
s.tracks.docs.phase = 'REMOVED';
|
|
136
|
+
expect((0, protocol_1.nextProtocolEffect)(s)).toEqual({
|
|
137
|
+
kind: 'enter-serial', reason: 'controller-requested:cli',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
37
140
|
test('ningún merge empieza antes de que todos los tracks estén FROZEN (C3)', () => {
|
|
38
141
|
const s = (0, protocol_1.initialCohort)('journal-1', ids);
|
|
39
142
|
s.cohortPhase = 'JOINING';
|
|
@@ -14,13 +14,13 @@ function seedRegistry() {
|
|
|
14
14
|
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-')));
|
|
15
15
|
const awmHome = path_1.default.join(home, '.awm');
|
|
16
16
|
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
17
|
-
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', '
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'ejemplo-proceso'), { recursive: true });
|
|
18
18
|
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
19
19
|
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
20
20
|
minCliVersion: '8.5.0',
|
|
21
21
|
orchestrator: { name: 'ejemplo-proceso', appliesWhen: 'cuando hay una tarea sin plan', terminatesTo: 'development-process' },
|
|
22
22
|
}));
|
|
23
|
-
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', '
|
|
23
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'ejemplo-proceso', 'SKILL.md'), '---\nname: ejemplo-proceso\nversion: "1.0.0"\n---\n\n# ejemplo-proceso\n');
|
|
24
24
|
return home;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
@@ -36,7 +36,7 @@ function seedRealisticRegistry() {
|
|
|
36
36
|
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-real-')));
|
|
37
37
|
const awmHome = path_1.default.join(home, '.awm');
|
|
38
38
|
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
39
|
-
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', '
|
|
39
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'task_capture_process'), { recursive: true });
|
|
40
40
|
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
41
41
|
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
42
42
|
minCliVersion: '8.5.0',
|
|
@@ -46,7 +46,7 @@ function seedRealisticRegistry() {
|
|
|
46
46
|
terminatesTo: 'development-process',
|
|
47
47
|
},
|
|
48
48
|
}));
|
|
49
|
-
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', '
|
|
49
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'task_capture_process', 'SKILL.md'), '---\nname: task_capture_process\nversion: "1.0.0"\n---\n\n# task_capture_process\n');
|
|
50
50
|
return home;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
@@ -63,7 +63,7 @@ function seedMixedRegistries() {
|
|
|
63
63
|
const awmHome = path_1.default.join(home, '.awm');
|
|
64
64
|
const sanoRoot = path_1.default.join(awmHome, 'registries', 'sano-registry');
|
|
65
65
|
const rotoRoot = path_1.default.join(awmHome, 'registries', 'roto-registry');
|
|
66
|
-
fs_1.default.mkdirSync(path_1.default.join(sanoRoot, 'skills', '
|
|
66
|
+
fs_1.default.mkdirSync(path_1.default.join(sanoRoot, 'skills', 'proceso-sano'), { recursive: true });
|
|
67
67
|
fs_1.default.mkdirSync(path_1.default.join(rotoRoot, 'skills', 'using-awm'), { recursive: true });
|
|
68
68
|
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([
|
|
69
69
|
{ name: 'sano-registry', remote: 'https://example.invalid/sano.git' },
|
|
@@ -79,8 +79,20 @@ function seedMixedRegistries() {
|
|
|
79
79
|
minCliVersion: '8.5.0',
|
|
80
80
|
orchestrator: { name: 'proceso-roto', appliesWhen: 'cuando algo pasa' },
|
|
81
81
|
}));
|
|
82
|
-
fs_1.default.writeFileSync(path_1.default.join(sanoRoot, 'skills', '
|
|
83
|
-
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(sanoRoot, 'skills', 'proceso-sano', 'SKILL.md'), '---\nname: proceso-sano\nversion: "1.0.0"\n---\n\n# proceso-sano\n');
|
|
83
|
+
return home;
|
|
84
|
+
}
|
|
85
|
+
function seedPhantomRegistry() {
|
|
86
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-phantom-')));
|
|
87
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
88
|
+
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
89
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'real-skill'), { recursive: true });
|
|
90
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
91
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
92
|
+
minCliVersion: '8.5.0',
|
|
93
|
+
orchestrator: { name: 'phantom-process', appliesWhen: 'cuando hay una tarea sin plan', terminatesTo: 'development-process' },
|
|
94
|
+
}));
|
|
95
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'real-skill', 'SKILL.md'), '---\nname: real-skill\nversion: "1.0.0"\n---\n\n# real-skill\n');
|
|
84
96
|
return home;
|
|
85
97
|
}
|
|
86
98
|
function run(home, ...args) {
|
|
@@ -142,3 +154,41 @@ describe('awm context orchestrators -- diagnosticos + sanos mezclados (binario r
|
|
|
142
154
|
expect(r.stderr).toContain('terminatesTo');
|
|
143
155
|
});
|
|
144
156
|
});
|
|
157
|
+
describe('awm context orchestrators -- declaracion phantom (binario real)', () => {
|
|
158
|
+
let home;
|
|
159
|
+
beforeAll(() => { home = seedPhantomRegistry(); });
|
|
160
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
161
|
+
it('--verify devuelve 2 y diagnostica una declaracion sin skill resoluble', () => {
|
|
162
|
+
const r = run(home, 'context', 'orchestrators', '--verify', 'phantom-process');
|
|
163
|
+
expect(r.status).toBe(2);
|
|
164
|
+
expect(r.stderr).toMatch(/phantom-process/);
|
|
165
|
+
expect(r.stderr).toMatch(/declaration dropped|skill/i);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('awm context orchestrators -- dropped sanitized collision (real binary)', () => {
|
|
169
|
+
let home;
|
|
170
|
+
beforeAll(() => {
|
|
171
|
+
home = seedPhantomRegistry();
|
|
172
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
173
|
+
const phantomRoot = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
174
|
+
const retainedRoot = path_1.default.join(awmHome, 'registries', 'retained');
|
|
175
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([
|
|
176
|
+
{ name: 'test-registry', remote: 'unused' }, { name: 'retained', remote: 'unused' },
|
|
177
|
+
]));
|
|
178
|
+
fs_1.default.writeFileSync(path_1.default.join(phantomRoot, 'awm-registry.json'), JSON.stringify({
|
|
179
|
+
orchestrator: { name: 'foo_bar', appliesWhen: 'x', terminatesTo: 'none' },
|
|
180
|
+
}));
|
|
181
|
+
fs_1.default.mkdirSync(path_1.default.join(retainedRoot, 'skills', 'foobar'), { recursive: true });
|
|
182
|
+
fs_1.default.writeFileSync(path_1.default.join(retainedRoot, 'skills', 'foobar', 'SKILL.md'), '# foobar\n');
|
|
183
|
+
fs_1.default.writeFileSync(path_1.default.join(retainedRoot, 'awm-registry.json'), JSON.stringify({
|
|
184
|
+
orchestrator: { name: 'foobar', appliesWhen: 'x', terminatesTo: 'none' },
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
188
|
+
it('verifies the retained raw name and rejects the dropped raw name', () => {
|
|
189
|
+
expect(run(home, 'context', 'orchestrators', '--verify', 'foobar').status).toBe(0);
|
|
190
|
+
const dropped = run(home, 'context', 'orchestrators', '--verify', 'foo_bar');
|
|
191
|
+
expect(dropped.status).toBe(2);
|
|
192
|
+
expect(dropped.stderr).toContain('declaration dropped');
|
|
193
|
+
});
|
|
194
|
+
});
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|