agentic-workflow-manager 3.13.3 → 3.13.5
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/core/diagnostics/context.js +33 -9
- package/dist/src/core/discovery.js +13 -21
- package/dist/src/core/export/transform.js +30 -25
- package/dist/src/core/frontmatter.js +223 -0
- package/dist/src/core/journal/process.js +22 -5
- package/dist/src/core/renderers/canonical-agent.js +33 -2
- package/dist/src/core/renderers/skill-source.js +11 -19
- package/dist/src/ui/text.js +10 -2
- package/dist/tests/core/diagnostics/context.test.js +44 -0
- package/dist/tests/core/discovery.test.js +10 -1
- package/dist/tests/core/export/transform.test.js +110 -21
- package/dist/tests/core/frontmatter-description-vs-yaml.test.js +118 -0
- package/dist/tests/core/init/steps.test.js +55 -0
- package/dist/tests/core/journal/process.test.js +54 -1
- package/dist/tests/core/renderers/canonical-agent.test.js +36 -0
- package/dist/tests/core/renderers/cursor-mdc.test.js +37 -7
- package/dist/tests/core/renderers/skill-source-block-scalar.test.js +117 -0
- package/dist/tests/core/sync-gates.test.js +28 -3
- package/dist/tests/integration/copilot-init-isolated.test.js +203 -0
- package/dist/tests/ui/text.test.js +9 -0
- package/package.json +3 -1
|
@@ -105,8 +105,17 @@ describe('Artifact Discovery', () => {
|
|
|
105
105
|
fs_1.default.readFileSync.mockImplementation(() => { throw new Error('ENOENT'); });
|
|
106
106
|
expect((0, discovery_1.readArtifactDescription)('/missing/SKILL.md')).toBe('');
|
|
107
107
|
});
|
|
108
|
-
it('
|
|
108
|
+
it('resuelve un block scalar leyendo sus lineas indentadas (antes descartaba el texto y devolvia vacio)', () => {
|
|
109
|
+
// Este test afirmaba `''` sobre un fixture que contiene literalmente
|
|
110
|
+
// "actual text" — codificaba la degradacion silenciosa, no un
|
|
111
|
+
// contrato. Un `description: >-` es YAML valido y su texto vive en
|
|
112
|
+
// las lineas indentadas siguientes; descartarlo dejaba el skill sin
|
|
113
|
+
// descripcion en todo el discovery del CLI.
|
|
109
114
|
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\n actual text\n---\n');
|
|
115
|
+
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('actual text');
|
|
116
|
+
});
|
|
117
|
+
it('sigue devolviendo vacio si el indicador de block scalar no tiene lineas indentadas (genuinamente sin descripcion)', () => {
|
|
118
|
+
fs_1.default.readFileSync.mockReturnValue('---\ndescription: >-\nname: otra-clave\n---\n');
|
|
110
119
|
expect((0, discovery_1.readArtifactDescription)('/any/SKILL.md')).toBe('');
|
|
111
120
|
});
|
|
112
121
|
});
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
3
7
|
const transform_1 = require("../../../src/core/export/transform");
|
|
4
8
|
const FM = (lines) => `---\n${lines.join('\n')}\n---\nBody line.\n`;
|
|
9
|
+
/** Reparsea el frontmatter EXPORTADO con un YAML real y devuelve su
|
|
10
|
+
* `description`. Lanza si la salida no es YAML valido — que es justamente lo
|
|
11
|
+
* que queremos que falle fuerte, en vez de asertar sobre el texto crudo y no
|
|
12
|
+
* enterarnos de que emitimos algo que ningun parser puede leer. */
|
|
13
|
+
function descriptionOf(exported) {
|
|
14
|
+
const fm = exported.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
15
|
+
if (!fm)
|
|
16
|
+
throw new Error('la salida no tiene bloque de frontmatter');
|
|
17
|
+
const parsed = js_yaml_1.default.load(fm[1]);
|
|
18
|
+
if (typeof parsed.description !== 'string')
|
|
19
|
+
throw new Error('description ausente o no-string en la salida');
|
|
20
|
+
return parsed.description;
|
|
21
|
+
}
|
|
5
22
|
describe('claudeAiTransform', () => {
|
|
6
23
|
it('strips version and portable, keeps other keys and body intact', () => {
|
|
7
24
|
const input = FM(['name: mermaid-diagrams', 'version: "1.0.0"', 'portable: true', 'description: "Guide."']);
|
|
@@ -16,18 +33,25 @@ describe('claudeAiTransform', () => {
|
|
|
16
33
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
17
34
|
expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
|
|
18
35
|
});
|
|
36
|
+
// NOTA sobre el estilo de la salida: el transform ya no conserva el estilo de
|
|
37
|
+
// comillas del fuente — emite SIEMPRE un escalar double-quoted via
|
|
38
|
+
// JSON.stringify, sea cual sea la forma de entrada (plano, entrecomillado,
|
|
39
|
+
// block scalar, multilinea). Antes habia una rama por forma, y cada vez que
|
|
40
|
+
// el lector aprendia una forma nueva el escritor quedaba atras: asi se
|
|
41
|
+
// colaron una deference line enterrada en medio de una descripcion
|
|
42
|
+
// multilinea y otra tragada entera por un `#` de comentario. Un solo camino
|
|
43
|
+
// hace imposible esa clase de divergencia. El contrato que estos tests
|
|
44
|
+
// protegen — YAML valido que conserva descripcion + deference line — se
|
|
45
|
+
// cumple igual, y se verifica reparseando la salida con js-yaml mas abajo.
|
|
19
46
|
it('appends the deference line to an unquoted description', () => {
|
|
20
47
|
const input = FM(['name: x', 'portable: true', 'description: Does things.']);
|
|
21
48
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
22
|
-
expect(out).toContain(`description: Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
49
|
+
expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
23
50
|
});
|
|
24
51
|
it('appends the deference line inside a single-quoted description', () => {
|
|
25
52
|
const input = FM(['name: x', 'portable: true', "description: 'Does things.'"]);
|
|
26
53
|
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
27
|
-
|
|
28
|
-
// even a fixture with no apostrophe of its own must see it doubled ('')
|
|
29
|
-
// per YAML single-quote escaping once spliced into a single-quoted scalar.
|
|
30
|
-
expect(out).toContain(`description: 'Does things. ${(0, transform_1.DEFERENCE_LINE)('x').replace(/'/g, "''")}'`);
|
|
54
|
+
expect(out).toContain(`description: ${JSON.stringify(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
31
55
|
});
|
|
32
56
|
it('appends the deference line inside a double-quoted description with trailing whitespace', () => {
|
|
33
57
|
const input = FM(['name: x', 'portable: true', 'description: "Does things." ']);
|
|
@@ -50,26 +74,91 @@ describe('claudeAiTransform', () => {
|
|
|
50
74
|
it('throws on frontmatter without description', () => {
|
|
51
75
|
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true']), 'x')).toThrow(/description/);
|
|
52
76
|
});
|
|
53
|
-
it('
|
|
54
|
-
|
|
77
|
+
it('resuelve una descripcion en block scalar y le anexa la deference line (antes abortaba el export del bundle entero)', () => {
|
|
78
|
+
// Regresion real: esto lanzaba, y runExport propaga el throw — asi que UN
|
|
79
|
+
// skill del registry baseline con esta forma valida (extract-design-md)
|
|
80
|
+
// hacia fallar `awm export frontend` COMPLETO, no solo ese skill.
|
|
81
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >', ' folded text', ' segunda linea']), 'x');
|
|
82
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
83
|
+
expect(descLine).toBe(`description: ${JSON.stringify(`folded text segunda linea ${(0, transform_1.DEFERENCE_LINE)('x')}`)}`);
|
|
84
|
+
// Las lineas indentadas del bloque se consumieron: no quedan sueltas.
|
|
85
|
+
expect(out).not.toContain(' folded text');
|
|
86
|
+
expect(out).not.toContain('description: >');
|
|
87
|
+
});
|
|
88
|
+
it('no absorbe las claves siguientes del frontmatter al consumir el bloque', () => {
|
|
89
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' solo esto', 'name: sigue-viva']), 'x');
|
|
90
|
+
expect(out).toContain('name: sigue-viva');
|
|
91
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
92
|
+
expect(descLine).toContain('solo esto');
|
|
93
|
+
expect(descLine).not.toContain('sigue-viva');
|
|
94
|
+
});
|
|
95
|
+
it('sigue lanzando si el block scalar no tiene contenido', () => {
|
|
96
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-']), 'x')).toThrow(/no content/);
|
|
97
|
+
});
|
|
98
|
+
it('lanza si description esta presente pero vacia (sin valor y sin bloque)', () => {
|
|
99
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description:']), 'x')).toThrow(/description is empty/);
|
|
55
100
|
});
|
|
56
|
-
it('
|
|
101
|
+
it('resuelve un block scalar CON comentario final sin perder el contenido (regresion: guarda por prefijo vs match completo)', () => {
|
|
102
|
+
// La guarda de la rama de bloque usaba un prefijo (/^[>|]/) mientras el
|
|
103
|
+
// resolver exigia match COMPLETO del indicador. Con `>- # nota` entraban en
|
|
104
|
+
// desacuerdo: se publicaba el indicador como descripcion y las lineas de
|
|
105
|
+
// contenido REALES se borraban del artefacto exportado, en silencio.
|
|
106
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >- # nota al margen', ' el texto real']), 'x');
|
|
107
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
108
|
+
expect(descLine).toContain('el texto real');
|
|
109
|
+
expect(descLine).not.toContain('# nota al margen');
|
|
110
|
+
expect(out).not.toContain('description: >-');
|
|
111
|
+
});
|
|
112
|
+
it('lanza ante un indicador de bloque malformado en vez de emitir YAML invalido', () => {
|
|
113
|
+
// `>-basura` lo rechaza el propio YAML. Tratarlo como escalar plano emitiria
|
|
114
|
+
// `description: >-basura ...`, invalido porque `>` abre un indicador.
|
|
115
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >-basura', ' texto']), 'x'))
|
|
116
|
+
.toThrow(/malformed block scalar indicator/);
|
|
117
|
+
});
|
|
118
|
+
it('conserva la linea en blanco que separa el bloque de la clave siguiente', () => {
|
|
119
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['description: >-', ' el texto', '', 'name: x']), 'x');
|
|
120
|
+
expect(out).toMatch(/description: .*\n\nname: x/);
|
|
121
|
+
});
|
|
122
|
+
it('no deja que el apostrofe de la deference line rompa el escalar emitido', () => {
|
|
123
|
+
// DEFERENCE_LINE siempre contiene un apostrofe ("registry's"). El fix
|
|
124
|
+
// original lo doblaba ('') porque la salida era single-quoted; hoy la
|
|
125
|
+
// salida es double-quoted, donde el apostrofe es un caracter comun. Lo
|
|
126
|
+
// que el test protege no es la convencion de comillas sino que el
|
|
127
|
+
// resultado sea YAML bien formado con el texto intacto — se verifica
|
|
128
|
+
// reparseando con js-yaml en vez de inspeccionar comillas a ojo.
|
|
57
129
|
const input = FM(['name: mermaid', 'portable: true', "description: 'Diagrams and flowcharts.'"]);
|
|
58
130
|
const out = (0, transform_1.claudeAiTransform)(input, 'mermaid');
|
|
59
|
-
|
|
60
|
-
expect(out).toContain("registry'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// quote or part of a doubled '' pair.
|
|
67
|
-
const body = descLine.slice('description: \''.length, -1);
|
|
68
|
-
expect(body.replace(/''/g, '')).not.toMatch(/'/);
|
|
69
|
-
});
|
|
70
|
-
it('throws when a quoted description has trailing content after its closing quote (e.g. inline comment)', () => {
|
|
131
|
+
expect(descriptionOf(out)).toBe(`Diagrams and flowcharts. ${(0, transform_1.DEFERENCE_LINE)('mermaid')}`);
|
|
132
|
+
expect(descriptionOf(out)).toContain("registry's mermaid skill");
|
|
133
|
+
});
|
|
134
|
+
it('resuelve un escalar entrecomillado con comentario final en vez de lanzar', () => {
|
|
135
|
+
// Antes lanzaba. Pero `description: "x" # nota` es YAML valido y js-yaml lo
|
|
136
|
+
// lee como "x": el comentario no forma parte del valor. Lanzar obligaba al
|
|
137
|
+
// autor a tocar un SKILL.md que no tenia nada malo.
|
|
71
138
|
const input = FM(['name: x', 'portable: true', 'description: "Does things." # a comment']);
|
|
72
|
-
expect((
|
|
139
|
+
expect(descriptionOf((0, transform_1.claudeAiTransform)(input, 'x'))).toBe(`Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
140
|
+
});
|
|
141
|
+
describe('round-trip: la salida se reparsea con un YAML real (par lector+escritor)', () => {
|
|
142
|
+
// Los tests del lector, por si solos, no habrian detectado que el ESCRITOR
|
|
143
|
+
// quedaba atras cuando el lector aprendia una forma nueva. Estos casos
|
|
144
|
+
// pasan cada forma por el transform y REPARSEAN el resultado, que es donde
|
|
145
|
+
// se ve si la deference line sobrevivio y si el YAML sigue siendo valido.
|
|
146
|
+
it.each([
|
|
147
|
+
['plano', ['description: Does things.']],
|
|
148
|
+
['plano multilinea', ['description: primera parte', ' y su continuacion']],
|
|
149
|
+
['plano con comentario final', ['description: hola mundo # nota del autor']],
|
|
150
|
+
['single-quoted', ["description: 'con apostrofe: it''s'"]],
|
|
151
|
+
['double-quoted', ['description: "con dos puntos: si"']],
|
|
152
|
+
['folded', ['description: >-', ' primera', ' segunda']],
|
|
153
|
+
['literal', ['description: |-', ' primera', ' segunda']],
|
|
154
|
+
['folded con comentario en el indicador', ['description: >- # nota', ' el texto']],
|
|
155
|
+
])('%s', (_name, descLines) => {
|
|
156
|
+
const out = (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true', ...descLines]), 'x');
|
|
157
|
+
const desc = descriptionOf(out); // lanza si la salida no es YAML valido
|
|
158
|
+
// La deference line es la razon de existir de este transform: nunca puede
|
|
159
|
+
// perderse en un comentario ni quedar sepultada en el medio del texto.
|
|
160
|
+
expect(desc.endsWith((0, transform_1.DEFERENCE_LINE)('x'))).toBe(true);
|
|
161
|
+
});
|
|
73
162
|
});
|
|
74
163
|
it('cleans intra-registry paths in the body', () => {
|
|
75
164
|
const md = [
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
// Test DIFERENCIAL: readFrontmatterDescription (parser a mano, cero deps de
|
|
7
|
+
// runtime) contra js-yaml (parser YAML real, devDependency solo de test).
|
|
8
|
+
//
|
|
9
|
+
// Por que existe: el repo parsea el frontmatter a mano a proposito (ver
|
|
10
|
+
// core/export/transform.ts — "sin parser YAML a proposito, YAGNI, cero deps").
|
|
11
|
+
// Esa decision es sostenible SOLO si algo verifica que la implementacion a
|
|
12
|
+
// mano coincide con la semantica YAML real; si no, cada forma valida que el
|
|
13
|
+
// parser casero no contempla es un bug esperando a un skill que la use. Ya
|
|
14
|
+
// paso dos veces: (1) un block scalar (`description: >-`) se colapsaba a
|
|
15
|
+
// vacio, rompiendo `awm add -a cursor|copilot` y abortando `awm export`
|
|
16
|
+
// entero contra un skill real del registry; (2) este mismo test diferencial
|
|
17
|
+
// descubrio que un escalar single-quoted no deshacia el escape YAML de `'`
|
|
18
|
+
// (duplicado: `''`), devolviendo "it''s" en vez de "it's".
|
|
19
|
+
//
|
|
20
|
+
// js-yaml es devDependency: NO entra en el bundle que instalan los usuarios.
|
|
21
|
+
// La regla de cero dependencias de runtime queda intacta — de hecho este test
|
|
22
|
+
// es lo que la hace defendible.
|
|
23
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
24
|
+
const discovery_1 = require("../../src/core/discovery");
|
|
25
|
+
/** Cada caso es un bloque de frontmatter cuyo `description` debe resolverse
|
|
26
|
+
* IGUAL que como lo hace un parser YAML real. Sin valores esperados
|
|
27
|
+
* hardcodeados a proposito: la referencia es js-yaml, asi que el test no
|
|
28
|
+
* puede "envejecer" hacia una expectativa equivocada. */
|
|
29
|
+
const CASES = [
|
|
30
|
+
['folded (>)', 'description: >\n a\n b'],
|
|
31
|
+
['folded strip (>-)', 'description: >-\n a\n b'],
|
|
32
|
+
['folded keep (>+)', 'description: >+\n a\n b'],
|
|
33
|
+
['literal (|)', 'description: |\n a\n b'],
|
|
34
|
+
['literal strip (|-)', 'description: |-\n a\n b'],
|
|
35
|
+
['folded con parrafos', 'description: >-\n a\n\n b'],
|
|
36
|
+
['literal con parrafos', 'description: |-\n a\n\n b'],
|
|
37
|
+
['bloque seguido de otra clave', 'description: >-\n solo\nname: x'],
|
|
38
|
+
['bloque con indent 4', 'description: >-\n a\n b'],
|
|
39
|
+
['bloque con indent explicito', 'description: >2\n a\n b'],
|
|
40
|
+
['bloque de una sola linea', 'description: >-\n sola'],
|
|
41
|
+
['bloque con colon adentro', 'description: >-\n clave: valor'],
|
|
42
|
+
['bloque con hash adentro', 'description: >-\n a #no-es-comentario'],
|
|
43
|
+
['plain', 'description: hola mundo'],
|
|
44
|
+
['plain con guion', 'description: usa - guiones'],
|
|
45
|
+
['double-quoted con colon', 'description: "hola: mundo"'],
|
|
46
|
+
['double-quoted con comilla escapada', 'description: "dice \\"hola\\""'],
|
|
47
|
+
['double-quoted con \\n', 'description: "a\\nb"'],
|
|
48
|
+
['double-quoted con backslash', 'description: "c:\\\\\\\\ruta"'],
|
|
49
|
+
['single-quoted con apostrofe escapado', "description: 'it''s aqui'"],
|
|
50
|
+
['single-quoted con doble apostrofe', "description: 'a''''b'"],
|
|
51
|
+
['single-quoted con colon', "description: 'clave: valor'"],
|
|
52
|
+
['double-quoted con em-dash', 'description: "em — dash"'],
|
|
53
|
+
// Casos que una revision adversarial encontro y los tests a mano no cubrian.
|
|
54
|
+
['folded con lineas MAS indentadas (no se pliegan)', 'description: >-\n intro\n - item uno\n - item dos\n outro'],
|
|
55
|
+
['folded con indentacion irregular', 'description: >\n foo\n bar\n baz'],
|
|
56
|
+
['indicador con comentario final', 'description: >- # nota al margen\n el texto real'],
|
|
57
|
+
['literal con comentario final', 'description: |- # nota\n linea'],
|
|
58
|
+
['clave description indentada NO gana sobre la real', 'metadata:\n description: nota interna\ndescription: la verdadera'],
|
|
59
|
+
['description dentro del contenido de otro bloque', 'example: |\n description: falsa\ndescription: la verdadera'],
|
|
60
|
+
['plain con comentario final', 'description: hola mundo # un comentario'],
|
|
61
|
+
['plain con hash pegado (NO es comentario)', 'description: usar C# y F#'],
|
|
62
|
+
['plain multilinea (se pliega como folded)', 'description: primera\n segunda\n tercera'],
|
|
63
|
+
['quoted con hash adentro (literal)', 'description: "tiene # adentro"'],
|
|
64
|
+
// Segunda tanda de revision adversarial: espacios, blancos consecutivos y
|
|
65
|
+
// escalares entrecomillados que cierran en una linea posterior.
|
|
66
|
+
['blancos consecutivos en folded', 'description: >-\n a\n\n\n b'],
|
|
67
|
+
['espacios finales dentro de un folded', 'description: >-\n a \n b'],
|
|
68
|
+
['linea solo-espacios dentro de un folded', 'description: >-\n a\n \n b'],
|
|
69
|
+
['double-quoted multilinea', 'description: "hola\n mundo"'],
|
|
70
|
+
['single-quoted multilinea', "description: 'hola\n mundo'"],
|
|
71
|
+
['double-quoted con comentario final', 'description: "x" # comentario'],
|
|
72
|
+
['single-quoted con comentario final', "description: 'y' # comentario"],
|
|
73
|
+
['single-quoted con apostrofe y comentario', "description: 'it''s aqui' # nota"],
|
|
74
|
+
];
|
|
75
|
+
/** Formas que YAML RECHAZA: no hay valor correcto que devolver, y devolver el
|
|
76
|
+
* indicador crudo como si fuera la descripcion seria peor que no devolver
|
|
77
|
+
* nada (se publicaria en el artefacto). El contrato aca es '' — que hace
|
|
78
|
+
* fallar fuerte a parseSkillSource y claudeAiTransform. */
|
|
79
|
+
const INVALID_CASES = [
|
|
80
|
+
['indicador con indent 0', 'description: >0\n a'],
|
|
81
|
+
['indicador con indent de 2 digitos', 'description: >12\n a'],
|
|
82
|
+
['indicador con basura pegada', 'description: >-basura\n a'],
|
|
83
|
+
['indicador con comentario sin espacio', 'description: >-#c\n a'],
|
|
84
|
+
];
|
|
85
|
+
describe('readFrontmatterDescription coincide con un parser YAML real (js-yaml)', () => {
|
|
86
|
+
it.each(CASES)('%s', (_name, frontmatter) => {
|
|
87
|
+
const parsed = js_yaml_1.default.load(frontmatter);
|
|
88
|
+
// Guard del propio fixture: si js-yaml no lo lee como string, el caso
|
|
89
|
+
// esta mal escrito y el test seria vacuo — que falle fuerte.
|
|
90
|
+
expect(typeof parsed.description).toBe('string');
|
|
91
|
+
const expected = parsed.description.trim();
|
|
92
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe(expected);
|
|
93
|
+
});
|
|
94
|
+
it.each(INVALID_CASES)('%s: YAML lo rechaza, nosotros devolvemos vacio (nunca el indicador crudo)', (_name, frontmatter) => {
|
|
95
|
+
// Guard del fixture: si js-yaml LO ACEPTARA, el caso esta mal clasificado
|
|
96
|
+
// y este test seria vacuo — que falle fuerte.
|
|
97
|
+
expect(() => js_yaml_1.default.load(frontmatter)).toThrow();
|
|
98
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe('');
|
|
99
|
+
});
|
|
100
|
+
it('resuelve el SKILL.md real del registry igual que js-yaml (extract-design-md, el caso que origino el bug)', () => {
|
|
101
|
+
// Forma exacta del skill real de awm-baseline-registry que rompia
|
|
102
|
+
// `awm add -a cursor|copilot` y `awm export frontend`.
|
|
103
|
+
const frontmatter = [
|
|
104
|
+
'name: extract-design-md',
|
|
105
|
+
'version: "1.0.1"',
|
|
106
|
+
'description: >-',
|
|
107
|
+
' Extract a comprehensive design system (DESIGN.md) directly from frontend source',
|
|
108
|
+
' code — React, Vue, Svelte, Angular, plain HTML/CSS, or any web framework.',
|
|
109
|
+
'allowed-tools:',
|
|
110
|
+
' - "Bash"',
|
|
111
|
+
].join('\n');
|
|
112
|
+
const expected = (js_yaml_1.default.load(frontmatter).description).trim();
|
|
113
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).toBe(expected);
|
|
114
|
+
// Y no absorbio la clave siguiente ni sus items indentados.
|
|
115
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).not.toContain('allowed-tools');
|
|
116
|
+
expect((0, discovery_1.readFrontmatterDescription)(frontmatter)).not.toContain('Bash');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const steps_1 = require("../../../src/core/init/steps");
|
|
10
10
|
const providers_1 = require("../../../src/providers");
|
|
11
|
+
const context_1 = require("../../../src/core/diagnostics/context");
|
|
11
12
|
function bundle(name, scope, skills) {
|
|
12
13
|
return {
|
|
13
14
|
name, description: '', version: '1.0.0', scope, visibility: 'public',
|
|
@@ -141,6 +142,60 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
|
|
|
141
142
|
expect((0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a)).action).toBe('applied');
|
|
142
143
|
expect(a.installBundle).toHaveBeenCalled();
|
|
143
144
|
});
|
|
145
|
+
// Regression for the confirmed production bug: `awm init -a copilot` crashed
|
|
146
|
+
// 100% of the time with "machine.devCore: skill global scope is not
|
|
147
|
+
// supported by Copilot...", rolling back the ENTIRE init transaction (even
|
|
148
|
+
// project-local artifacts like AGENTS.md). Root cause: Copilot has no
|
|
149
|
+
// global skill directory (providerFor('copilot').skill.global === null),
|
|
150
|
+
// so gatherMachine's devCorePresent was permanently false for it, and
|
|
151
|
+
// stepDevCore fell through to installBundle at global scope on every run
|
|
152
|
+
// — an install that always throws. Fixed in diagnostics/context.ts:
|
|
153
|
+
// devCore.present is now reported `true` (N/A treated as satisfied) when
|
|
154
|
+
// skill.global is null, so this step's existing skip guard applies
|
|
155
|
+
// naturally.
|
|
156
|
+
//
|
|
157
|
+
// Unlike the rest of this describe block (which hand-builds `machine()`
|
|
158
|
+
// fixtures — a fine choice for exercising stepDevCore's own skip-guard
|
|
159
|
+
// logic in isolation), this test calls the REAL `gatherContext` (the
|
|
160
|
+
// function diagnostics/context.ts's fix actually lives in) with an
|
|
161
|
+
// isolated HOME/AWM_HOME, and feeds ITS real output into stepDevCore.
|
|
162
|
+
// That's deliberate: an earlier version of this test hand-built
|
|
163
|
+
// `devCore: { present: true, brokenLinks: [] }` directly and asserted
|
|
164
|
+
// against it, which only re-verified stepDevCore's pre-existing skip
|
|
165
|
+
// guard — reverting the context.ts fix left that version GREEN because it
|
|
166
|
+
// never called the fixed code at all. This version goes RED on revert:
|
|
167
|
+
// gatherContext would then report `devCore.present: false` for Copilot,
|
|
168
|
+
// stepDevCore would fall through to `installBundle`, and the assertions
|
|
169
|
+
// below (`action === 'skipped'`, `installBundle` not called) would fail.
|
|
170
|
+
it('devCore skips cleanly (never calls installBundle) for an agent with no global skill directory (copilot) — via real gatherContext', () => {
|
|
171
|
+
expect((0, providers_1.providerFor)('copilot').skill.global).toBeNull();
|
|
172
|
+
const tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-steps-copilot-devcore-'));
|
|
173
|
+
const originalHome = process.env.HOME;
|
|
174
|
+
const originalAwmHome = process.env.AWM_HOME;
|
|
175
|
+
try {
|
|
176
|
+
process.env.HOME = tmpHome;
|
|
177
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
178
|
+
const baselineBundle = bundle('dev-core', 'baseline', ['brainstorming']);
|
|
179
|
+
const ctx = (0, context_1.gatherContext)({ cwd: tmpHome, bundles: [baselineBundle], agent: 'copilot' });
|
|
180
|
+
const a = spies();
|
|
181
|
+
const r = (0, steps_1.stepDevCore)(deps({ machine: ctx.machine, project: null }, a, {
|
|
182
|
+
agent: 'copilot', enabledAgents: ['copilot'], bundles: [baselineBundle],
|
|
183
|
+
}));
|
|
184
|
+
expect(r.action).toBe('skipped');
|
|
185
|
+
expect(a.installBundle).not.toHaveBeenCalled();
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
189
|
+
if (originalHome === undefined)
|
|
190
|
+
delete process.env.HOME;
|
|
191
|
+
else
|
|
192
|
+
process.env.HOME = originalHome;
|
|
193
|
+
if (originalAwmHome === undefined)
|
|
194
|
+
delete process.env.AWM_HOME;
|
|
195
|
+
else
|
|
196
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
144
199
|
it('ambient installs only missing wanted', () => {
|
|
145
200
|
const a = spies();
|
|
146
201
|
const m = machine();
|
|
@@ -11,7 +11,31 @@ const process_1 = require("../../../src/core/journal/process");
|
|
|
11
11
|
const paths_1 = require("../../../src/core/paths");
|
|
12
12
|
describe('process identity', () => {
|
|
13
13
|
test('spawnStructured produce ProcessRef con tupla completa (R2.1, R4.7)', async () => {
|
|
14
|
-
|
|
14
|
+
// Reintento de INSTANCIA completa (no solo de la consulta), a proposito:
|
|
15
|
+
// esta misma linea fallo 2 veces reales consecutivas en windows-latest
|
|
16
|
+
// CI, la segunda vez YA con el presupuesto interno de pidExistsNative
|
|
17
|
+
// ampliado a 10 intentos/100ms (~900ms de espera real dentro de
|
|
18
|
+
// refIsAlive) — evidencia de que NO es (solo) latencia de visibilidad
|
|
19
|
+
// de OpenProcess bajo carga: un proceso genuinamente vivo no deberia
|
|
20
|
+
// seguir siendo invisible tras casi un segundo de reintentos. Apunta
|
|
21
|
+
// en cambio a que el hijo mismo puede terminar genuinamente muy
|
|
22
|
+
// temprano en este runner (imagen windows-2025-vs2026, sospecha no
|
|
23
|
+
// confirmable sin Windows real: AV/Defender interviniendo un `node -e`
|
|
24
|
+
// recien lanzado bajo carga pesada de CI). pidExistsNative reintenta
|
|
25
|
+
// la MISMA consulta sobre el MISMO pid — si ese pid ya esta
|
|
26
|
+
// genuinamente muerto, reintentar la consulta jamas ayuda. Este loop
|
|
27
|
+
// reintenta el SPAWN entero: un intento NUEVO puede sobrevivir donde
|
|
28
|
+
// el anterior no lo hizo. Si los 3 intentos mueren temprano, el
|
|
29
|
+
// ultimo `expect` de abajo sigue fallando fuerte — no enmascara una
|
|
30
|
+
// regresion real de refIsAlive.
|
|
31
|
+
let { child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'nonce-abc');
|
|
32
|
+
for (let attempt = 1; attempt < 3 && !(0, process_1.refIsAlive)(ref); attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
child.kill('SIGKILL');
|
|
35
|
+
}
|
|
36
|
+
catch { /* ya ausente */ }
|
|
37
|
+
({ child, ref } = (0, process_1.spawnStructured)(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'nonce-abc'));
|
|
38
|
+
}
|
|
15
39
|
expect(ref.pid).toBe(child.pid);
|
|
16
40
|
expect(ref.spawnNonce).toBe('nonce-abc');
|
|
17
41
|
expect(typeof ref.startTime).toBe('string');
|
|
@@ -253,6 +277,35 @@ describe('process identity (win32, mockeado — sin windows real disponible en e
|
|
|
253
277
|
expect((0, process_1.refIsAlive)(fakeRef)).toBe(true); // NUNCA declara muerte por el ESRCH transitorio de los primeros 2 intentos
|
|
254
278
|
expect(killSpy).toHaveBeenCalledTimes(3);
|
|
255
279
|
});
|
|
280
|
+
test('pidExistsNative absorbe una carrera transitoria mas larga que el presupuesto original — arranque en frio del primer spawn del job (regresion #2: misma falla real, mismo test, tras el fix de 3x50ms ya mergeado)', () => {
|
|
281
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
282
|
+
let calls = 0;
|
|
283
|
+
const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
284
|
+
calls++;
|
|
285
|
+
// El pid tarda 9 intentos en "aparecer" — mas alla del presupuesto
|
|
286
|
+
// anterior (3 intentos) pero dentro del ampliado (10 intentos).
|
|
287
|
+
if (calls < 9) {
|
|
288
|
+
const err = new Error('transient');
|
|
289
|
+
err.code = 'ESRCH';
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
});
|
|
294
|
+
const fakeRef = { pid: 424243, startTime: 'x', spawnNonce: 'n', argvDigest: 'd', processGroup: 424243, psArgsDigest: 'x' };
|
|
295
|
+
expect((0, process_1.refIsAlive)(fakeRef)).toBe(true);
|
|
296
|
+
expect(killSpy).toHaveBeenCalledTimes(9);
|
|
297
|
+
});
|
|
298
|
+
test('pidExistsNative declara muerte solo tras agotar el presupuesto ampliado (10 intentos) — un ESRCH sostenido nunca se lee como vivo', () => {
|
|
299
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
300
|
+
const killSpy = jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
301
|
+
const err = new Error('gone');
|
|
302
|
+
err.code = 'ESRCH';
|
|
303
|
+
throw err;
|
|
304
|
+
});
|
|
305
|
+
const fakeRef = { pid: 424244, startTime: 'x', spawnNonce: 'n', argvDigest: 'd', processGroup: 424244, psArgsDigest: 'x' };
|
|
306
|
+
expect((0, process_1.refIsAlive)(fakeRef)).toBe(false);
|
|
307
|
+
expect(killSpy).toHaveBeenCalledTimes(10);
|
|
308
|
+
});
|
|
256
309
|
test('refIsAlive en win32 NUNCA declara muerte por un error que no sea ESRCH (ej. EPERM: el pid existe pero sin permiso de senializarlo) (R2.1)', () => {
|
|
257
310
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
258
311
|
jest.spyOn(process, 'kill').mockImplementation(() => {
|
|
@@ -46,6 +46,42 @@ it('ignores provider-only mode while retaining canonical instructions', () => {
|
|
|
46
46
|
instructions: expect.stringContaining('You do NOT write code directly.'),
|
|
47
47
|
}); // verifies R8, R9
|
|
48
48
|
});
|
|
49
|
+
it('acepta una description en block scalar y la resuelve (regresion: rompia awm add -a codex)', () => {
|
|
50
|
+
// El parser exigia que TODA linea del frontmatter fuera `clave: valor`, asi
|
|
51
|
+
// que las lineas indentadas de un bloque lanzaban "invalid canonical agent
|
|
52
|
+
// frontmatter line" y abortaban el install de codex. Peor: discovery lee el
|
|
53
|
+
// MISMO archivo y (ya arreglado) mostraba la descripcion bien en el picker,
|
|
54
|
+
// asi que el crash llegaba despues de que la UI dijera que todo estaba OK.
|
|
55
|
+
const source = [
|
|
56
|
+
'---',
|
|
57
|
+
'name: bloque-agente',
|
|
58
|
+
'description: >-',
|
|
59
|
+
' Primera linea de la descripcion',
|
|
60
|
+
' y su continuacion.',
|
|
61
|
+
'---',
|
|
62
|
+
'Cuerpo de instrucciones.',
|
|
63
|
+
].join('\n');
|
|
64
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source)).toEqual({
|
|
65
|
+
name: 'bloque-agente',
|
|
66
|
+
description: 'Primera linea de la descripcion y su continuacion.',
|
|
67
|
+
instructions: 'Cuerpo de instrucciones.',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it('acepta un escalar plano multilinea y lo pliega (js-yaml lee exactamente esto)', () => {
|
|
71
|
+
// Ojo: una version previa de este test exigia un throw aca. Estaba mal —
|
|
72
|
+
// `description: x` seguido de una linea indentada es un escalar plano
|
|
73
|
+
// multilinea, YAML perfectamente valido, y js-yaml lo lee como
|
|
74
|
+
// "x suelta e indentada". Asertar el throw codificaba una divergencia con
|
|
75
|
+
// YAML, no un contrato.
|
|
76
|
+
const source = '---\nname: ok\ndescription: x\n suelta e indentada\n---\nbody';
|
|
77
|
+
expect((0, canonical_agent_1.parseCanonicalAgent)(source).description).toBe('x suelta e indentada');
|
|
78
|
+
});
|
|
79
|
+
it('sigue rechazando una linea que no es ni clave ni continuacion indentada', () => {
|
|
80
|
+
// El fix no afloja la validacion estricta: una linea en columna 0 que no es
|
|
81
|
+
// `clave: valor` sigue siendo frontmatter malformado.
|
|
82
|
+
const source = '---\nname: ok\ndescription: x\nno soy un campo\n---\nbody';
|
|
83
|
+
expect(() => (0, canonical_agent_1.parseCanonicalAgent)(source)).toThrow('invalid canonical agent frontmatter line');
|
|
84
|
+
});
|
|
49
85
|
it.each([
|
|
50
86
|
['---\nname: Bad Name\ndescription: x\n---\nbody', 'invalid agent name'],
|
|
51
87
|
['---\nname: ok\ndescription:\n---\nbody', 'non-empty description'],
|
|
@@ -49,11 +49,18 @@ Body content.
|
|
|
49
49
|
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
50
50
|
expect(rendered).toContain('description: "*starred description"');
|
|
51
51
|
});
|
|
52
|
-
it('
|
|
53
|
-
// Regression
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
52
|
+
it('resuelve un block scalar leyendo sus lineas indentadas, sin emitir el indicador literal', () => {
|
|
53
|
+
// Regression, en dos etapas:
|
|
54
|
+
// 1. La version original tomaba el indicador (`>-`) COMO la descripcion,
|
|
55
|
+
// lo que habria escrito el literal ">-" dentro del .mdc.
|
|
56
|
+
// 2. El remedio siguiente lo detectaba pero lo colapsaba a '' (=> throw),
|
|
57
|
+
// tratando una forma YAML valida como "descripcion ausente" — este
|
|
58
|
+
// test codificaba ESE comportamiento parcial. Rompia `awm add
|
|
59
|
+
// <bundle> -a cursor` contra un skill real del registry baseline
|
|
60
|
+
// (extract-design-md), que usa exactamente esta forma.
|
|
61
|
+
// El contrato correcto, y el que se asserta ahora: leer el texto de las
|
|
62
|
+
// lineas indentadas. La intencion original del test — jamas emitir el
|
|
63
|
+
// indicador crudo — se conserva explicitamente abajo.
|
|
57
64
|
const source = `---
|
|
58
65
|
name: block-skill
|
|
59
66
|
description: >-
|
|
@@ -63,16 +70,27 @@ description: >-
|
|
|
63
70
|
|
|
64
71
|
Body content.
|
|
65
72
|
`;
|
|
66
|
-
|
|
73
|
+
const rendered = (0, cursor_mdc_1.renderCursorMdc)(source);
|
|
74
|
+
expect(rendered).toContain('description: This description spans multiple lines.');
|
|
75
|
+
expect(rendered).not.toContain('>-');
|
|
67
76
|
});
|
|
68
77
|
it('quotes a description containing a mid-string " #" (starts a YAML comment, truncating the rest)', () => {
|
|
69
78
|
// Regression: the original YAML_UNSAFE regex only caught `#` at the START
|
|
70
79
|
// of the string — a `#` preceded by whitespace ANYWHERE in a plain scalar
|
|
71
80
|
// also starts a comment. Unquoted, "Use this #important skill" would
|
|
72
81
|
// render as YAML that silently truncates to "Use this".
|
|
82
|
+
//
|
|
83
|
+
// El fuente esta ENTRECOMILLADO a proposito: ahi el `#` es literal y
|
|
84
|
+
// sobrevive al parseo, que es la unica manera de que un `#` llegue al
|
|
85
|
+
// renderer y haya algo que escapar. Antes el fixture usaba la forma SIN
|
|
86
|
+
// comillas, pero YAML dice que ahi ` #` abre un comentario — el lector lo
|
|
87
|
+
// trataba como texto (divergencia con cualquier parser real) y este test
|
|
88
|
+
// se apoyaba en esa divergencia. El contrato que el test realmente
|
|
89
|
+
// protege — nunca emitir un `#` sin comillas en la SALIDA — queda intacto
|
|
90
|
+
// y ahora se ejercita con una entrada que de verdad lo contiene.
|
|
73
91
|
const source = `---
|
|
74
92
|
name: hash-skill
|
|
75
|
-
description: Use this #important skill
|
|
93
|
+
description: "Use this #important skill"
|
|
76
94
|
---
|
|
77
95
|
|
|
78
96
|
Body content.
|
|
@@ -81,6 +99,18 @@ Body content.
|
|
|
81
99
|
expect(rendered).toContain('description: "Use this #important skill"');
|
|
82
100
|
expect(rendered).not.toContain('description: Use this #important skill');
|
|
83
101
|
});
|
|
102
|
+
it('trata un " #" en una description SIN comillas como comentario YAML, igual que un parser real', () => {
|
|
103
|
+
const source = `---
|
|
104
|
+
name: hash-plano
|
|
105
|
+
description: Use this #important skill
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
Body content.
|
|
109
|
+
`;
|
|
110
|
+
// js-yaml devuelve "Use this" para esta entrada: el comentario no es parte
|
|
111
|
+
// del valor. El renderer debe coincidir con esa lectura, no inventar texto.
|
|
112
|
+
expect((0, cursor_mdc_1.renderCursorMdc)(source)).toContain('description: Use this\n');
|
|
113
|
+
});
|
|
84
114
|
it('quotes a description containing an embedded null byte / control character instead of emitting it raw', () => {
|
|
85
115
|
// Regression: an embedded control/null byte is invalid in a YAML plain
|
|
86
116
|
// scalar regardless of position — the original code's YAML_UNSAFE regex
|