agentic-workflow-manager 9.0.2 → 9.1.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.
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const body_1 = require("../../../src/core/process/body");
4
+ const body = `
5
+ # Ejemplo
6
+
7
+ ## Objetivo
8
+
9
+ G — Llevar una idea hasta una rama cerrada.
10
+
11
+ ## Cuándo aplica
12
+
13
+ Cuando hay una tarea de desarrollo sin plan previo.
14
+
15
+ ## Estructura
16
+
17
+ - SG-1 — Diseñar
18
+ - OP-1.1 — Elicitar requisitos
19
+ - OP-1.2 — Escribir el design doc
20
+ - SG-2 — Ejecutar
21
+ - OP-2.1 — Implementar por tasks
22
+
23
+ ## Ruteo
24
+
25
+ | Cuándo | Estado requerido | Va a | Termina en |
26
+ |---|---|---|---|
27
+ | No hay design doc | | OP-1.1 | SG-1 |
28
+ | Design doc aprobado | SG-1 | OP-2.1 | SG-2 |
29
+
30
+ ## Terminación
31
+
32
+ finishing-a-development-branch
33
+
34
+ ## Sin verificar
35
+
36
+ - Que el usuario tenga un registry propio instalado.
37
+ `;
38
+ describe('parseProcessBody', () => {
39
+ it('extrae las seis secciones', () => {
40
+ const r = (0, body_1.parseProcessBody)(body, 'p');
41
+ expect(r.diagnostics).toEqual([]);
42
+ expect(r.model.objective).toBe('G — Llevar una idea hasta una rama cerrada.');
43
+ expect(r.model.appliesWhen).toBe('Cuando hay una tarea de desarrollo sin plan previo.');
44
+ expect(r.model.termination).toBe('finishing-a-development-branch');
45
+ expect(r.model.unverified).toEqual(['Que el usuario tenga un registry propio instalado.']);
46
+ });
47
+ it('descompone Estructura en SG-# con sus OP-#', () => {
48
+ expect((0, body_1.parseProcessBody)(body, 'p').model.structure).toEqual([
49
+ { id: 'SG-1', text: 'Diseñar', operations: [
50
+ { id: 'OP-1.1', text: 'Elicitar requisitos' }, { id: 'OP-1.2', text: 'Escribir el design doc' }
51
+ ] },
52
+ { id: 'SG-2', text: 'Ejecutar', operations: [{ id: 'OP-2.1', text: 'Implementar por tasks' }] },
53
+ ]);
54
+ });
55
+ it('lee Ruteo con sus cuatro columnas, incluida la vacía', () => {
56
+ expect((0, body_1.parseProcessBody)(body, 'p').model.routing).toEqual([
57
+ { when: 'No hay design doc', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' },
58
+ { when: 'Design doc aprobado', requiredState: 'SG-1', goesTo: 'OP-2.1', endsAt: 'SG-2' },
59
+ ]);
60
+ });
61
+ it('conserva las filas de Ruteo como datos, sin evaluarlas ni colapsarlas', () => {
62
+ // WCP16 Deferred Choice: la condición se evalúa al llegar a la decisión,
63
+ // leyendo el estado real del proyecto. El parser no puede precomputar cuál
64
+ // fila gana — si lo hiciera, `show --json` emitiría una decisión ya tomada.
65
+ const r = (0, body_1.parseProcessBody)(body, 'p').model;
66
+ expect(r.routing).toHaveLength(2);
67
+ expect(Object.keys(r)).not.toContain('activeRoute');
68
+ expect(JSON.stringify(r.routing)).toContain('No hay design doc');
69
+ });
70
+ it.each(['## Objetivo', '## Ruteo', '## Terminación'])('rechaza si falta %s', (heading) => {
71
+ const r = (0, body_1.parseProcessBody)(body.replace(heading, '## Otra cosa'), 'p');
72
+ expect(r.model).toBeUndefined();
73
+ expect(r.diagnostics.join(' ')).toMatch(/missing/i);
74
+ });
75
+ it('rechaza una tabla de Ruteo con número de columnas equivocado', () => {
76
+ const r = (0, body_1.parseProcessBody)(body.replace('| No hay design doc | | OP-1.1 | SG-1 |', '| No hay design doc | OP-1.1 |'), 'p');
77
+ expect(r.model).toBeUndefined();
78
+ });
79
+ it('rechaza un id de operación que no cuelga de su subobjetivo', () => {
80
+ const r = (0, body_1.parseProcessBody)(body.replace('OP-2.1', 'OP-9.1'), 'p');
81
+ expect(r.model).toBeUndefined();
82
+ expect(r.diagnostics.join(' ')).toMatch(/OP-9\.1/);
83
+ });
84
+ it('nunca lanza ante entrada basura', () => {
85
+ for (const junk of ['', '## Objetivo', '| | | |', '#'.repeat(500)]) {
86
+ expect(() => (0, body_1.parseProcessBody)(junk, 'p')).not.toThrow();
87
+ }
88
+ });
89
+ it('rechaza un heading de nivel 2 repetido en vez de fusionarlo', () => {
90
+ // Un `## Ruteo` duplicado no debe fusionar sus líneas con la primera
91
+ // aparición: si lo hiciera, la fila de cabecera de la segunda tabla
92
+ // ("| Cuándo | ... |") caería en un índice no-cero del array fusionado
93
+ // y `parseRouting` la confundiría con una fila de datos — corrupción
94
+ // silenciosa, sin diagnóstico. Esto es especialmente grave porque este
95
+ // parser procesa contenido no confiable de registries externos.
96
+ const duped = `
97
+ ## Objetivo
98
+
99
+ G — Llevar una idea hasta una rama cerrada.
100
+
101
+ ## Cuándo aplica
102
+
103
+ Cuando hay una tarea de desarrollo sin plan previo.
104
+
105
+ ## Estructura
106
+
107
+ - SG-1 — Diseñar
108
+ - OP-1.1 — Elicitar requisitos
109
+
110
+ ## Ruteo
111
+
112
+ | Cuándo | Estado requerido | Va a | Termina en |
113
+ |---|---|---|---|
114
+ | No hay design doc | | OP-1.1 | SG-1 |
115
+
116
+ ## Ruteo
117
+
118
+ | Cuándo | Estado requerido | Va a | Termina en |
119
+ |---|---|---|---|
120
+ | Otra condición | | OP-1.1 | SG-1 |
121
+
122
+ ## Terminación
123
+
124
+ finishing-a-development-branch
125
+
126
+ ## Sin verificar
127
+
128
+ - Que el usuario tenga un registry propio instalado.
129
+ `;
130
+ const r = (0, body_1.parseProcessBody)(duped, 'p');
131
+ expect(r.model).toBeUndefined();
132
+ expect(r.diagnostics.join(' ')).toMatch(/duplicate/i);
133
+ });
134
+ it('parsea un heading con una racha larga de espacios finales sin colgarse (ReDoS)', () => {
135
+ // Regresión: `splitSections` matcheaba headings con `/^##\s+(.+?)\s*$/`.
136
+ // El grupo perezoso `(.+?)` seguido de `\s*` sobre una clase que lo
137
+ // solapa (`.` ⊃ `\s`) es cuadrático — ver AGENTS.md
138
+ // "regex-cuantificador-adyacente-a-clase-que-lo-solapa". Una sola línea
139
+ // `## `-prefijada con una racha larga de espacios finales (ni siquiera
140
+ // maliciosa, un typo alcanza) colgaba el proceso. El fix usa captura
141
+ // greedy a fin-de-línea (`(.*)$`, sin cuantificador adyacente) y hace
142
+ // `.trim()` en código. Este test prueba que el parseo completo del
143
+ // documento sigue siendo lineal, no solo el regex aislado.
144
+ const longTrailingSpace = '## Objetivo' + ' '.repeat(50000) + 'x';
145
+ const doc = body.replace('## Objetivo', longTrailingSpace);
146
+ const t0 = Date.now();
147
+ const r = (0, body_1.parseProcessBody)(doc, 'p');
148
+ const elapsed = Date.now() - t0;
149
+ expect(elapsed).toBeLessThan(500);
150
+ // El heading matcheado debe llevar el trim aplicado en código, no en el regex.
151
+ expect(r.diagnostics.join(' ')).toMatch(/missing required section "## Objetivo"/);
152
+ });
153
+ it('ignora una línea "## " (solo espacio, sin nombre) como contenido ordinario, sin abrir una sección fantasma', () => {
154
+ // Regresión del fix de ReDoS: la regex vieja `/^##\s+(.+?)\s*$/` exigía
155
+ // al menos un carácter no-blanco capturado, así que una línea "##"
156
+ // seguida solo de espacio NUNCA matcheaba como heading. La regex nueva
157
+ // `/^##\s+(.*)$/` + trim() sí puede capturar "" — sin el guard de
158
+ // finding-1, esa línea abriría una sección "" fantasma y desviaría lo
159
+ // que sigue (aquí, una fila real de la tabla de Ruteo) hacia ella,
160
+ // dejando `parseProcessBody` "exitoso" pero con datos perdidos y CERO
161
+ // diagnósticos. La línea debe tratarse como contenido ordinario de la
162
+ // sección abierta (`## Ruteo`), donde `parseRouting` ya la ignora por
163
+ // no empezar con "|".
164
+ const strayHeading = body.replace('## Ruteo\n\n| Cuándo | Estado requerido | Va a | Termina en |', '## Ruteo\n\n## \n\n| Cuándo | Estado requerido | Va a | Termina en |');
165
+ expect(strayHeading).not.toBe(body); // guard: el replace debe haber encontrado el punto de inserción
166
+ const r = (0, body_1.parseProcessBody)(strayHeading, 'p');
167
+ expect(r.diagnostics).toEqual([]);
168
+ expect(r.model.routing).toEqual([
169
+ { when: 'No hay design doc', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' },
170
+ { when: 'Design doc aprobado', requiredState: 'SG-1', goesTo: 'OP-2.1', endsAt: 'SG-2' },
171
+ ]);
172
+ });
173
+ it('reconoce un separador de tabla con colons de alineación GFM (|:---|:---:|) sin fabricar una fila', () => {
174
+ // finding-2: el detector viejo `/^-{1,}$/.test(cells.join(''))` solo
175
+ // reconoce separadores de dashes puros. `|:---|:---:|:---:|:---:|` es
176
+ // sintaxis GFM estándar de alineación de columnas (muy probable si el
177
+ // autor del registry usó cualquier formateador de tablas markdown) y
178
+ // NO era reconocida — la fila colaba como dato fabricado con celdas
179
+ // literales como ":---". El fix la reconoce por celda con un regex
180
+ // anclado `^:?-+:?$` (lineal, sin riesgo ReDoS).
181
+ const gfmAligned = body.replace('|---|---|---|---|', '|:---|:---:|:---:|:---:|');
182
+ expect(gfmAligned).not.toBe(body);
183
+ const r = (0, body_1.parseProcessBody)(gfmAligned, 'p');
184
+ expect(r.diagnostics).toEqual([]);
185
+ expect(r.model.routing).toEqual([
186
+ { when: 'No hay design doc', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' },
187
+ { when: 'Design doc aprobado', requiredState: 'SG-1', goesTo: 'OP-2.1', endsAt: 'SG-2' },
188
+ ]);
189
+ // Ninguna fila fabricada a partir del separador (p.ej. celdas ":---").
190
+ expect(JSON.stringify(r.model.routing)).not.toContain(':--');
191
+ });
192
+ it.each(['Objetivo', 'Cuándo aplica', 'Terminación'])('rechaza %s presente pero con contenido vacío', (heading) => {
193
+ // A diferencia del test "rechaza si falta %s" (heading ausente por
194
+ // completo), acá el heading `## ${heading}` SÍ está presente pero no
195
+ // tiene contenido no-blanco debajo antes del siguiente `##` — el
196
+ // rechazo lo produce el chequeo `value === ''` sobre `paragraph()`,
197
+ // no `splitSections`, y ese camino no tenía cobertura.
198
+ const emptied = body.replace(new RegExp(`## ${heading}\\n\\n[^#]*`), `## ${heading}\n\n \n\n`);
199
+ expect(emptied).not.toBe(body);
200
+ const r = (0, body_1.parseProcessBody)(emptied, 'p');
201
+ expect(r.model).toBeUndefined();
202
+ expect(r.diagnostics.join(' ')).toMatch(new RegExp(`"${heading}" is empty`));
203
+ });
204
+ it('rechaza una sección de prosa que excede el largo máximo', () => {
205
+ // finding-1 QA: ninguna sección de texto libre tenía cota de largo —
206
+ // un `## Objetivo` de 10MB en un registry no confiable se aceptaba
207
+ // sin rechazo ni truncamiento. `MAX_SECTION_LENGTH` en body.ts cierra
208
+ // ese hueco para las secciones de prosa (Objetivo/Cuándo
209
+ // aplica/Terminación).
210
+ const overlong = body.replace('G — Llevar una idea hasta una rama cerrada.', 'x'.repeat(5001));
211
+ expect(overlong).not.toBe(body);
212
+ const r = (0, body_1.parseProcessBody)(overlong, 'p');
213
+ expect(r.model).toBeUndefined();
214
+ expect(r.diagnostics.join(' ')).toMatch(/"Objetivo" must be at most 5000 characters/);
215
+ });
216
+ it('rechaza un texto de SG-# que excede el largo máximo de ítem', () => {
217
+ // Mismo hueco que la sección de prosa, pero para los ítems
218
+ // estructurales (texto de SG-#/OP-#, celdas de Ruteo, entradas de
219
+ // "Sin verificar"), acotados por `MAX_ITEM_LENGTH` — más corto que
220
+ // `MAX_SECTION_LENGTH` porque son título-cortos por contrato.
221
+ const overlong = body.replace('- SG-1 — Diseñar', `- SG-1 — ${'x'.repeat(501)}`);
222
+ expect(overlong).not.toBe(body);
223
+ const r = (0, body_1.parseProcessBody)(overlong, 'p');
224
+ expect(r.model).toBeUndefined();
225
+ expect(r.diagnostics.join(' ')).toMatch(/"SG-1" text must be at most 500 characters/);
226
+ });
227
+ it('rechaza un SG-# duplicado en Estructura', () => {
228
+ // finding-3: a diferencia de la validación OP-# <-> SG-# (que sí
229
+ // rechaza una pertenencia inconsistente), un SG-# repetido pasaba en
230
+ // silencio y producía un `structure` con dos entradas compartiendo el
231
+ // mismo `id` — inconsistente para cualquier consumidor que indexe por
232
+ // id. Mismo patrón `problems` que el resto del archivo.
233
+ const dupedSg = body.replace('- SG-1 — Diseñar\n - OP-1.1 — Elicitar requisitos\n - OP-1.2 — Escribir el design doc\n- SG-2 — Ejecutar', '- SG-1 — Diseñar\n - OP-1.1 — Elicitar requisitos\n - OP-1.2 — Escribir el design doc\n- SG-1 — Diseñar otra vez\n- SG-2 — Ejecutar');
234
+ expect(dupedSg).not.toBe(body);
235
+ const r = (0, body_1.parseProcessBody)(dupedSg, 'p');
236
+ expect(r.model).toBeUndefined();
237
+ expect(r.diagnostics.join(' ')).toMatch(/duplicate.*SG-1/i);
238
+ });
239
+ });
@@ -0,0 +1,139 @@
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
+ // cli/tests/core/process/discover.test.ts
7
+ const discover_1 = require("../../../src/core/process/discover");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const FIXTURE = `---
12
+ awm: process-model
13
+ schema: 1
14
+ name: NAME
15
+ status: draft
16
+ entry_point: true
17
+ terminates_to: none
18
+ created: 2026-08-23
19
+ updated: 2026-08-23
20
+ ---
21
+
22
+ ## Objetivo
23
+
24
+ G — Objetivo.
25
+
26
+ ## Cuándo aplica
27
+
28
+ Siempre.
29
+
30
+ ## Estructura
31
+
32
+ - SG-1 — Uno
33
+ - OP-1.1 — Hacer
34
+
35
+ ## Ruteo
36
+
37
+ | Cuándo | Estado requerido | Va a | Termina en |
38
+ |---|---|---|---|
39
+ | Al empezar | | OP-1.1 | SG-1 |
40
+
41
+ ## Terminación
42
+
43
+ none
44
+
45
+ ## Sin verificar
46
+
47
+ - Nada.
48
+ `;
49
+ function registry(root, skills) {
50
+ for (const [name, content] of Object.entries(skills)) {
51
+ fs_1.default.mkdirSync(path_1.default.join(root, 'skills', name), { recursive: true });
52
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills', name, 'SKILL.md'), content);
53
+ }
54
+ return root;
55
+ }
56
+ describe('discoverProcessModels', () => {
57
+ let tmp;
58
+ beforeEach(() => { tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-discover-')); });
59
+ afterEach(() => { fs_1.default.rmSync(tmp, { recursive: true, force: true }); });
60
+ it('encuentra solo los SKILL.md con el discriminador', () => {
61
+ const root = registry(path_1.default.join(tmp, 'r1'), {
62
+ 'mi-proceso': FIXTURE.replace('NAME', 'mi-proceso'),
63
+ 'skill-normal': '---\nname: skill-normal\ndescription: otra cosa\n---\n\n# Normal\n',
64
+ });
65
+ const r = (0, discover_1.discoverProcessModels)([root]);
66
+ expect(r.models.map((m) => m.name)).toEqual(['mi-proceso']);
67
+ expect(r.diagnostics).toEqual([]);
68
+ });
69
+ it('un modelo roto no impide descubrir los sanos', () => {
70
+ const root = registry(path_1.default.join(tmp, 'r2'), {
71
+ 'sano': FIXTURE.replace('NAME', 'sano'),
72
+ 'roto': FIXTURE.replace('NAME', 'roto').replace('schema: 1', 'schema: cero'),
73
+ });
74
+ const r = (0, discover_1.discoverProcessModels)([root]);
75
+ expect(r.models.map((m) => m.name)).toEqual(['sano']);
76
+ expect(r.diagnostics).toHaveLength(1);
77
+ });
78
+ it('deduplica por name entre registries y lo reporta', () => {
79
+ const a = registry(path_1.default.join(tmp, 'a'), { 'dup': FIXTURE.replace('NAME', 'dup') });
80
+ const b = registry(path_1.default.join(tmp, 'b'), { 'dup': FIXTURE.replace('NAME', 'dup') });
81
+ const r = (0, discover_1.discoverProcessModels)([a, b]);
82
+ expect(r.models).toHaveLength(1);
83
+ expect(r.diagnostics.join(' ')).toMatch(/duplicates/i);
84
+ });
85
+ it('sin registries devuelve vacío sin diagnósticos', () => {
86
+ expect((0, discover_1.discoverProcessModels)([])).toEqual({ models: [], diagnostics: [] });
87
+ });
88
+ it('nunca lanza con un root inexistente', () => {
89
+ expect(() => (0, discover_1.discoverProcessModels)([path_1.default.join(tmp, 'no-existe')])).not.toThrow();
90
+ });
91
+ it('un `skills` roto (archivo en vez de directorio) no impide seguir, y queda diagnosticado', () => {
92
+ const root = path_1.default.join(tmp, 'r3');
93
+ fs_1.default.mkdirSync(root, { recursive: true });
94
+ fs_1.default.writeFileSync(path_1.default.join(root, 'skills'), 'no soy un directorio');
95
+ let r;
96
+ expect(() => { r = (0, discover_1.discoverProcessModels)([root]); }).not.toThrow();
97
+ expect(r.models).toEqual([]);
98
+ expect(r.diagnostics).toHaveLength(1);
99
+ expect(r.diagnostics[0]).toContain(root);
100
+ });
101
+ it('un SKILL.md roto (directorio en vez de archivo) no impide seguir, y queda diagnosticado', () => {
102
+ const root = path_1.default.join(tmp, 'r4');
103
+ const skillMd = path_1.default.join(root, 'skills', 'roto', 'SKILL.md');
104
+ fs_1.default.mkdirSync(skillMd, { recursive: true });
105
+ let r;
106
+ expect(() => { r = (0, discover_1.discoverProcessModels)([root]); }).not.toThrow();
107
+ expect(r.models).toEqual([]);
108
+ expect(r.diagnostics).toHaveLength(1);
109
+ expect(r.diagnostics[0]).toContain(skillMd);
110
+ expect(r.diagnostics[0]).toMatch(/cannot read/);
111
+ });
112
+ it('un SKILL.md simbolico apuntando fuera del registry no se lee: ni modelo ni contenido filtrado en diagnósticos', () => {
113
+ const root = path_1.default.join(tmp, 'r5');
114
+ const skillDir = path_1.default.join(root, 'skills', 'malicioso');
115
+ fs_1.default.mkdirSync(skillDir, { recursive: true });
116
+ // Archivo "sensible" FUERA de la raíz del registry — simula ~/.ssh/id_rsa u otro
117
+ // archivo local legible por el usuario que ejecuta `awm`.
118
+ const secretDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-outside-registry-'));
119
+ const secretFile = path_1.default.join(secretDir, 'id_rsa');
120
+ const SECRET_MARKER = 'SUPER-SECRET-PRIVATE-KEY-CONTENT-DO-NOT-LEAK';
121
+ fs_1.default.writeFileSync(secretFile, `-----BEGIN OPENSSH PRIVATE KEY-----\n${SECRET_MARKER}\n-----END OPENSSH PRIVATE KEY-----\n`);
122
+ const skillMd = path_1.default.join(skillDir, 'SKILL.md');
123
+ fs_1.default.symlinkSync(secretFile, skillMd);
124
+ let r;
125
+ expect(() => { r = (0, discover_1.discoverProcessModels)([root]); }).not.toThrow();
126
+ // Ningún modelo se construye a partir del symlink.
127
+ expect(r.models).toEqual([]);
128
+ // Se reporta el problema, pero SOLO nombrando el path — nunca el contenido del
129
+ // archivo apuntado. El nombre del archivo secreto en sí (id_rsa) sirve como proxy
130
+ // de "esto no debería aparecer en diagnósticos" tanto como el contenido.
131
+ expect(r.diagnostics).toHaveLength(1);
132
+ expect(r.diagnostics[0]).toContain(skillMd);
133
+ expect(r.diagnostics[0]).toMatch(/symbolic link/i);
134
+ const allDiagnosticsText = r.diagnostics.join('\n');
135
+ expect(allDiagnosticsText).not.toContain(SECRET_MARKER);
136
+ expect(allDiagnosticsText).not.toContain('BEGIN OPENSSH PRIVATE KEY');
137
+ fs_1.default.rmSync(secretDir, { recursive: true, force: true });
138
+ });
139
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const model_1 = require("../../../src/core/process/model");
4
+ const valid = `---
5
+ awm: process-model
6
+ schema: 1
7
+ name: ejemplo-proceso
8
+ status: draft
9
+ entry_point: true
10
+ terminates_to: none
11
+ created: 2026-08-23
12
+ updated: 2026-08-23
13
+ ---
14
+
15
+ # Ejemplo
16
+ `;
17
+ describe('parseProcessFrontmatter', () => {
18
+ it('acepta el modelo canónico', () => {
19
+ const r = (0, model_1.parseProcessFrontmatter)(valid, 'skills/x/SKILL.md');
20
+ expect(r.diagnostics).toEqual([]);
21
+ expect(r.model).toEqual(expect.objectContaining({
22
+ schema: 1, name: 'ejemplo-proceso', status: 'draft', entryPoint: true, terminatesTo: 'none',
23
+ }));
24
+ });
25
+ it('no infiere: sin el discriminador no es un modelo', () => {
26
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('awm: process-model\n', ''), 'skills/x/SKILL.md');
27
+ expect(r.model).toBeUndefined();
28
+ expect(r.diagnostics).toEqual([]); // no es un error: es "no es un modelo"
29
+ });
30
+ it('se detiene ante un schema más nuevo en vez de leerlo como el anterior', () => {
31
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('schema: 1', 'schema: 2'), 'skills/x/SKILL.md');
32
+ expect(r.model).toBeUndefined();
33
+ expect(r.diagnostics.join(' ')).toMatch(/newer/i);
34
+ });
35
+ it.each(['0', '-1', '1.5', 'uno'])('rechaza schema no-entero-positivo %s', (bad) => {
36
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('schema: 1', `schema: ${bad}`), 'skills/x/SKILL.md');
37
+ expect(r.model).toBeUndefined();
38
+ expect(r.diagnostics).not.toEqual([]);
39
+ });
40
+ it.each(['active', 'draft'])('admite status %s', (s) => {
41
+ expect((0, model_1.parseProcessFrontmatter)(valid.replace('status: draft', `status: ${s}`), 'p').model?.status).toBe(s);
42
+ });
43
+ it('rechaza un status inventado', () => {
44
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('status: draft', 'status: published'), 'p');
45
+ expect(r.model).toBeUndefined();
46
+ expect(r.diagnostics.join(' ')).toMatch(/status/);
47
+ });
48
+ it('rechaza un campo desconocido en vez de ignorarlo', () => {
49
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('created:', 'api_token: ghp_x\ncreated:'), 'p');
50
+ expect(r.model).toBeUndefined();
51
+ expect(r.diagnostics.join(' ')).toMatch(/unknown field/i);
52
+ });
53
+ it('rechaza un name que no es slug', () => {
54
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('name: ejemplo-proceso', 'name: ../../etc/passwd'), 'p');
55
+ expect(r.model).toBeUndefined();
56
+ });
57
+ it('nunca lanza ante entrada basura', () => {
58
+ for (const junk of ['', '---\n', '---\n\x00\n---\n', 'no frontmatter']) {
59
+ expect(() => (0, model_1.parseProcessFrontmatter)(junk, 'p')).not.toThrow();
60
+ }
61
+ });
62
+ it.each(['si', 'True', '1', 'yes'])('rechaza un entry_point que no es true/false: %s', (bad) => {
63
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('entry_point: true', `entry_point: ${bad}`), 'p');
64
+ expect(r.model).toBeUndefined();
65
+ expect(r.diagnostics.join(' ')).toMatch(/"entry_point" must be true or false/);
66
+ });
67
+ it.each(['../../etc/passwd', 'None', 'Foo-Bar', 'has spaces'])('rechaza un terminates_to que no es slug ni "none": %s', (bad) => {
68
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace('terminates_to: none', `terminates_to: ${bad}`), 'p');
69
+ expect(r.model).toBeUndefined();
70
+ expect(r.diagnostics.join(' ')).toMatch(/"terminates_to" must be a lowercase slug or "none"/);
71
+ });
72
+ it.each(['created', 'updated'])('rechaza un %s con formato de fecha inválido', (field) => {
73
+ const r = (0, model_1.parseProcessFrontmatter)(valid.replace(`${field}: 2026-08-23`, `${field}: 23-08-2026`), 'p');
74
+ expect(r.model).toBeUndefined();
75
+ expect(r.diagnostics.join(' ')).toMatch(new RegExp(`${field}.*YYYY-MM-DD`));
76
+ });
77
+ it.each(['name', 'status', 'entry_point', 'terminates_to', 'created', 'updated'])('rechaza si falta el campo requerido %s', (field) => {
78
+ const withoutField = valid.split('\n').filter((line) => !line.startsWith(`${field}:`)).join('\n');
79
+ const r = (0, model_1.parseProcessFrontmatter)(withoutField, 'p');
80
+ expect(r.model).toBeUndefined();
81
+ expect(r.diagnostics.join(' ')).toMatch(new RegExp(`"${field}" is required`));
82
+ });
83
+ });
@@ -0,0 +1,57 @@
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
+ // cli/tests/core/process/no-regression.test.ts
7
+ const collect_1 = require("../../../src/core/dashboard/collect");
8
+ const discover_1 = require("../../../src/core/process/discover");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ describe('R7 — ausencia de modelos y aislamiento', () => {
13
+ let root;
14
+ let home;
15
+ const realHome = process.env.HOME;
16
+ const realAwmHome = process.env.AWM_HOME;
17
+ beforeEach(() => {
18
+ root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-noreg-'));
19
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-home-'));
20
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
21
+ // R7.4: ningún test toca el ~/.awm real.
22
+ process.env.HOME = home;
23
+ process.env.AWM_HOME = path_1.default.join(home, '.awm');
24
+ });
25
+ afterEach(() => {
26
+ if (realHome === undefined)
27
+ delete process.env.HOME;
28
+ else
29
+ process.env.HOME = realHome;
30
+ if (realAwmHome === undefined)
31
+ delete process.env.AWM_HOME;
32
+ else
33
+ process.env.AWM_HOME = realAwmHome;
34
+ fs_1.default.rmSync(root, { recursive: true, force: true });
35
+ fs_1.default.rmSync(home, { recursive: true, force: true });
36
+ });
37
+ it('sin modelos, el snapshot conserva las nueve secciones en orden', () => {
38
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: '2026-08-23T00:00:00.000Z' });
39
+ expect(snapshot.sections.map((s) => s.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes']);
40
+ expect(snapshot.schema).toBe(2);
41
+ expect(snapshot.sections.find((s) => s.id === 'processes')).toEqual(expect.objectContaining({ availability: 'not_applicable', items: [] }));
42
+ });
43
+ it('el descubridor no toca el ~/.awm real', () => {
44
+ // contentRoots() (usado por default por discoverProcessModels) lee
45
+ // AWM_HOME dinámicamente en cada llamada — con AWM_HOME apuntando al
46
+ // tmpdir de este test, sin modelos de proceso instalados ahí, esto
47
+ // no debe lanzar. (Se descartó una aserción sobre `.awm/process-cache`:
48
+ // ningún código de este repo escribe ese path, así que nunca podía
49
+ // fallar y no aportaba evidencia real de aislamiento.)
50
+ expect(() => (0, discover_1.discoverProcessModels)()).not.toThrow();
51
+ });
52
+ it('usa path.join y no separadores hardcodeados', () => {
53
+ const src = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../src/core/process/discover.ts'), 'utf-8');
54
+ expect(src).not.toMatch(/['"`][^'"`]*\/skills\//);
55
+ expect(src).toContain('path.join');
56
+ });
57
+ });