agentic-workflow-manager 9.0.2 → 9.2.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/dist/src/commands/context/index.js +47 -0
- package/dist/src/commands/ledger/index.js +13 -0
- package/dist/src/commands/process/index.js +58 -0
- package/dist/src/core/command-result.js +17 -0
- package/dist/src/core/context/provider.js +27 -18
- package/dist/src/core/dashboard/collect.js +30 -5
- package/dist/src/core/dashboard/sanitize.js +24 -2
- package/dist/src/core/orchestrators.js +24 -3
- package/dist/src/core/process/body.js +181 -0
- package/dist/src/core/process/discover.js +94 -0
- package/dist/src/core/process/model.js +109 -0
- package/dist/src/core/process/types.js +5 -0
- package/dist/src/core/text.js +58 -0
- package/dist/src/index.js +4 -0
- package/dist/tests/commands/context.test.js +64 -0
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/process.test.js +88 -0
- package/dist/tests/core/context/provider.test.js +49 -0
- package/dist/tests/core/dashboard/collect.test.js +25 -21
- package/dist/tests/core/dashboard/processes-section.test.js +71 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +58 -0
- package/dist/tests/core/orchestrators.test.js +38 -0
- package/dist/tests/core/process/body.test.js +239 -0
- package/dist/tests/core/process/discover.test.js +139 -0
- package/dist/tests/core/process/model.test.js +83 -0
- package/dist/tests/core/process/no-regression.test.js +57 -0
- package/dist/tests/core/text.test.js +29 -0
- package/dist/tests/integration/context-orchestrators-e2e.test.js +144 -0
- package/dist/tests/integration/process-model-e2e.test.js +161 -0
- package/dist/tests/structural/process-model-single-parser.test.js +63 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const text_1 = require("../../src/core/text");
|
|
4
|
+
describe('stripControlChars', () => {
|
|
5
|
+
it('elimina ESC y demas bytes de control C0', () => {
|
|
6
|
+
expect((0, text_1.stripControlChars)('a\x1b[31mb\x00c\x07d')).toBe('a[31mbcd');
|
|
7
|
+
});
|
|
8
|
+
it('preserva \\n y \\t, que son whitespace legitimo', () => {
|
|
9
|
+
expect((0, text_1.stripControlChars)('a\nb\tc')).toBe('a\nb\tc');
|
|
10
|
+
});
|
|
11
|
+
it('elimina DEL (0x7F)', () => {
|
|
12
|
+
expect((0, text_1.stripControlChars)('a\x7Fb')).toBe('ab');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('sanitizeDeclaredField', () => {
|
|
16
|
+
it('quita marcado markdown Y bytes de control en una sola pasada', () => {
|
|
17
|
+
expect((0, text_1.sanitizeDeclaredField)('a\x1bx')).toBe('ax');
|
|
18
|
+
expect((0, text_1.sanitizeDeclaredField)('a\n## Forjado *b* `c` <d>')).toBe('a Forjado b c d');
|
|
19
|
+
});
|
|
20
|
+
it('no deja ESC ni otros C0 sobrevivir aunque el saneo markdown no los toque', () => {
|
|
21
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
22
|
+
expect((0, text_1.sanitizeDeclaredField)('name\x1b[31mred\x07')).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
23
|
+
});
|
|
24
|
+
it('es idempotente sobre texto ya limpio', () => {
|
|
25
|
+
const clean = 'a plain name';
|
|
26
|
+
expect((0, text_1.sanitizeDeclaredField)(clean)).toBe(clean);
|
|
27
|
+
expect((0, text_1.sanitizeDeclaredField)((0, text_1.sanitizeDeclaredField)(clean))).toBe(clean);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
11
|
+
const DIST = path_1.default.join(CLI_ROOT, 'dist', 'src', 'index.js');
|
|
12
|
+
/** Registry de prueba con un orquestador declarado. Nunca toca el ~/.awm real (R7.4). */
|
|
13
|
+
function seedRegistry() {
|
|
14
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-')));
|
|
15
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
16
|
+
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'using-awm'), { recursive: true });
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
19
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
20
|
+
minCliVersion: '8.5.0',
|
|
21
|
+
orchestrator: { name: 'ejemplo-proceso', appliesWhen: 'cuando hay una tarea sin plan', terminatesTo: 'development-process' },
|
|
22
|
+
}));
|
|
23
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
24
|
+
return home;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Registry con un nombre de orquestador REALISTA: `task_capture_process`, con guion bajo —
|
|
28
|
+
* la misma clase de caracter (`_`) que disparaba el falso-negativo de `--verify` fijado en
|
|
29
|
+
* 3e8ce65 (arg crudo vs. nombre saneado). `seedRegistry()` usa un nombre puramente
|
|
30
|
+
* alfanumerico-con-guiones que nunca ejercito esa clase de caracter a nivel del binario
|
|
31
|
+
* real — este fixture cierra ese hueco (Finding 1 confirmado del QA post-implementacion) y
|
|
32
|
+
* ademas es representativo de lo que la generacion de `process-lifecycle` (Task 5)
|
|
33
|
+
* plausiblemente produce a partir de un `name` tipo slug.
|
|
34
|
+
*/
|
|
35
|
+
function seedRealisticRegistry() {
|
|
36
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-real-')));
|
|
37
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
38
|
+
const root = path_1.default.join(awmHome, 'registries', 'test-registry');
|
|
39
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills', 'using-awm'), { recursive: true });
|
|
40
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'test-registry', remote: 'https://example.invalid/test.git' }]));
|
|
41
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify({
|
|
42
|
+
minCliVersion: '8.5.0',
|
|
43
|
+
orchestrator: {
|
|
44
|
+
name: 'task_capture_process',
|
|
45
|
+
appliesWhen: 'cuando llega una tarea sin capturar en el backlog',
|
|
46
|
+
terminatesTo: 'development-process',
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
50
|
+
return home;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* DOS registries en el mismo `AWM_HOME`: uno con declaracion valida ('sana') y otro con una
|
|
54
|
+
* declaracion rota (le falta `terminatesTo`) que `readDeclaredOrchestrators`
|
|
55
|
+
* (core/orchestrators.ts) diagnostica sin lanzar (R1.2) — rechaza SOLO esa declaracion,
|
|
56
|
+
* sin invalidar el registry que la contiene ni a los demas. Ejercita R7.1 a nivel del
|
|
57
|
+
* binario real: diagnosticos por stderr sin dejar de listar lo sano por stdout
|
|
58
|
+
* (Finding 2 confirmado del QA post-implementacion, hasta ahora solo unit-testeado en
|
|
59
|
+
* tests/commands/context.test.ts).
|
|
60
|
+
*/
|
|
61
|
+
function seedMixedRegistries() {
|
|
62
|
+
const home = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ctx-e2e-mixed-')));
|
|
63
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
64
|
+
const sanoRoot = path_1.default.join(awmHome, 'registries', 'sano-registry');
|
|
65
|
+
const rotoRoot = path_1.default.join(awmHome, 'registries', 'roto-registry');
|
|
66
|
+
fs_1.default.mkdirSync(path_1.default.join(sanoRoot, 'skills', 'using-awm'), { recursive: true });
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.join(rotoRoot, 'skills', 'using-awm'), { recursive: true });
|
|
68
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([
|
|
69
|
+
{ name: 'sano-registry', remote: 'https://example.invalid/sano.git' },
|
|
70
|
+
{ name: 'roto-registry', remote: 'https://example.invalid/roto.git' },
|
|
71
|
+
]));
|
|
72
|
+
fs_1.default.writeFileSync(path_1.default.join(sanoRoot, 'awm-registry.json'), JSON.stringify({
|
|
73
|
+
minCliVersion: '8.5.0',
|
|
74
|
+
orchestrator: { name: 'proceso-sano', appliesWhen: 'cuando hay una tarea sin plan', terminatesTo: 'development-process' },
|
|
75
|
+
}));
|
|
76
|
+
// Declaracion rota: falta "terminatesTo" -- readDeclaredOrchestrators la rechaza y
|
|
77
|
+
// reporta un diagnostico en vez de lanzar o componerla silenciosamente (R1.2).
|
|
78
|
+
fs_1.default.writeFileSync(path_1.default.join(rotoRoot, 'awm-registry.json'), JSON.stringify({
|
|
79
|
+
minCliVersion: '8.5.0',
|
|
80
|
+
orchestrator: { name: 'proceso-roto', appliesWhen: 'cuando algo pasa' },
|
|
81
|
+
}));
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(sanoRoot, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
83
|
+
fs_1.default.writeFileSync(path_1.default.join(rotoRoot, 'skills', 'using-awm', 'SKILL.md'), '---\nname: using-awm\nversion: "1.0.0"\n---\n\n# using-awm\n');
|
|
84
|
+
return home;
|
|
85
|
+
}
|
|
86
|
+
function run(home, ...args) {
|
|
87
|
+
return (0, child_process_1.spawnSync)(process.execPath, [DIST, ...args], {
|
|
88
|
+
cwd: CLI_ROOT, encoding: 'utf8',
|
|
89
|
+
env: { ...process.env, HOME: home, AWM_HOME: path_1.default.join(home, '.awm'), AWM_NO_UPDATE_CHECK: '1' },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
describe('awm context orchestrators (binario real)', () => {
|
|
93
|
+
let home;
|
|
94
|
+
beforeAll(() => { home = seedRegistry(); });
|
|
95
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
96
|
+
it('reporta el orquestador declarado por el registry sembrado', () => {
|
|
97
|
+
const r = run(home, 'context', 'orchestrators');
|
|
98
|
+
expect(r.status).toBe(0);
|
|
99
|
+
expect(r.stdout).toContain('ejemplo-proceso');
|
|
100
|
+
});
|
|
101
|
+
it('--json emite la lista parseable', () => {
|
|
102
|
+
const r = run(home, 'context', 'orchestrators', '--json');
|
|
103
|
+
expect(r.status).toBe(0);
|
|
104
|
+
expect(JSON.parse(r.stdout).orchestrators[0].name).toBe('ejemplo-proceso');
|
|
105
|
+
});
|
|
106
|
+
it('--verify cierra el ciclo de verificacion con exit code', () => {
|
|
107
|
+
expect(run(home, 'context', 'orchestrators', '--verify', 'ejemplo-proceso').status).toBe(0);
|
|
108
|
+
const ausente = run(home, 'context', 'orchestrators', '--verify', 'no-existe');
|
|
109
|
+
expect(ausente.status).toBe(2);
|
|
110
|
+
expect(ausente.stderr).toContain('ejemplo-proceso');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('awm context orchestrators -- nombre realista con guion bajo (binario real)', () => {
|
|
114
|
+
let home;
|
|
115
|
+
beforeAll(() => { home = seedRealisticRegistry(); });
|
|
116
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
117
|
+
it('--verify con el nombre RAW declarado (con guion bajo) compone -- regression del fix 3e8ce65', () => {
|
|
118
|
+
const r = run(home, 'context', 'orchestrators', '--verify', 'task_capture_process');
|
|
119
|
+
expect(r.status).toBe(0);
|
|
120
|
+
expect(r.stdout).toMatch(/composed/i);
|
|
121
|
+
});
|
|
122
|
+
it('la lista en texto plano y --json muestran el nombre saneado (guion bajo removido)', () => {
|
|
123
|
+
const plano = run(home, 'context', 'orchestrators');
|
|
124
|
+
expect(plano.status).toBe(0);
|
|
125
|
+
expect(plano.stdout).toContain('taskcaptureprocess');
|
|
126
|
+
expect(plano.stdout).not.toContain('task_capture_process');
|
|
127
|
+
const json = run(home, 'context', 'orchestrators', '--json');
|
|
128
|
+
expect(json.status).toBe(0);
|
|
129
|
+
expect(JSON.parse(json.stdout).orchestrators[0].name).toBe('taskcaptureprocess');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('awm context orchestrators -- diagnosticos + sanos mezclados (binario real)', () => {
|
|
133
|
+
let home;
|
|
134
|
+
beforeAll(() => { home = seedMixedRegistries(); });
|
|
135
|
+
afterAll(() => fs_1.default.rmSync(home, { recursive: true, force: true }));
|
|
136
|
+
it('lista el orquestador sano por stdout y diagnostica el roto por stderr sin fallar', () => {
|
|
137
|
+
const r = run(home, 'context', 'orchestrators');
|
|
138
|
+
expect(r.status).toBe(0);
|
|
139
|
+
expect(r.stdout).toContain('proceso-sano');
|
|
140
|
+
expect(r.stderr).toContain('warning:');
|
|
141
|
+
expect(r.stderr).toContain('roto-registry');
|
|
142
|
+
expect(r.stderr).toContain('terminatesTo');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
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/integration/process-model-e2e.test.ts
|
|
7
|
+
//
|
|
8
|
+
// Ledger signature: ca1-2-1-3-no-e2e-regression-test.
|
|
9
|
+
//
|
|
10
|
+
// CA-1.2 y CA-1.3 del plan R1a (docs/plans/2026-08-23-process-model-r1a-plan.md)
|
|
11
|
+
// fueron verificados MANUALMENTE varias veces durante esa sesión, pero ningún
|
|
12
|
+
// test permanente los sostenía. Este test cierra ese hueco ejecutando el
|
|
13
|
+
// binario compilado real (dist/src/index.js) contra un AWM_HOME/registry
|
|
14
|
+
// fixture aislado — no llama funciones internas directamente. El plan es
|
|
15
|
+
// explícito: "No vale el test unitario: el criterio dice 'reporta', y eso se
|
|
16
|
+
// prueba ejecutando" (CA-1.2). Sigue el patrón de invocación de
|
|
17
|
+
// tests/structural/r3-cli-major-version.test.ts (spawnSync del dist compilado)
|
|
18
|
+
// y el patrón de fixture de tests/core/process/discover.test.ts +
|
|
19
|
+
// tests/core/process/no-regression.test.ts (registries.json + skills/<name>/SKILL.md
|
|
20
|
+
// bajo un AWM_HOME de tmpdir, nunca el ~/.awm real).
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const os_1 = __importDefault(require("os"));
|
|
23
|
+
const path_1 = __importDefault(require("path"));
|
|
24
|
+
const child_process_1 = require("child_process");
|
|
25
|
+
const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
26
|
+
const DIST_ENTRYPOINT = path_1.default.join(CLI_ROOT, 'dist', 'src', 'index.js');
|
|
27
|
+
// Fixture canónico del plan R1a, reproducido byte a byte (bloque "Canonical fixture").
|
|
28
|
+
const EJEMPLO_PROCESO_SKILL_MD = `---
|
|
29
|
+
awm: process-model
|
|
30
|
+
schema: 1
|
|
31
|
+
name: ejemplo-proceso
|
|
32
|
+
status: draft
|
|
33
|
+
entry_point: true
|
|
34
|
+
terminates_to: none
|
|
35
|
+
created: 2026-08-23
|
|
36
|
+
updated: 2026-08-23
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Ejemplo
|
|
40
|
+
|
|
41
|
+
## Objetivo
|
|
42
|
+
|
|
43
|
+
G — Llevar una idea hasta una rama cerrada.
|
|
44
|
+
|
|
45
|
+
## Cuándo aplica
|
|
46
|
+
|
|
47
|
+
Cuando hay una tarea de desarrollo sin plan previo.
|
|
48
|
+
|
|
49
|
+
## Estructura
|
|
50
|
+
|
|
51
|
+
- SG-1 — Diseñar
|
|
52
|
+
- OP-1.1 — Elicitar requisitos
|
|
53
|
+
- OP-1.2 — Escribir el design doc
|
|
54
|
+
- SG-2 — Ejecutar
|
|
55
|
+
- OP-2.1 — Implementar por tasks
|
|
56
|
+
|
|
57
|
+
## Ruteo
|
|
58
|
+
|
|
59
|
+
| Cuándo | Estado requerido | Va a | Termina en |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| No hay design doc | | OP-1.1 | SG-1 |
|
|
62
|
+
| Design doc aprobado | SG-1 | OP-2.1 | SG-2 |
|
|
63
|
+
|
|
64
|
+
## Terminación
|
|
65
|
+
|
|
66
|
+
finishing-a-development-branch
|
|
67
|
+
|
|
68
|
+
## Sin verificar
|
|
69
|
+
|
|
70
|
+
Nada.
|
|
71
|
+
`;
|
|
72
|
+
/** Construye AWM_HOME/registries.json + AWM_HOME/registries/<name>/skills/<skill>/SKILL.md
|
|
73
|
+
* — el mismo layout que contentRoots() (src/core/registries.ts) resuelve para
|
|
74
|
+
* descubrimiento real, sin clonar git: contentRoots() solo necesita que el
|
|
75
|
+
* directorio exista y pase inspectRegistrySafety (ningún symlink, ≥1 dir
|
|
76
|
+
* administrado). El campo "remote" no se usa en este test — nada corre `awm update`. */
|
|
77
|
+
function fixture() {
|
|
78
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-process-e2e-'));
|
|
79
|
+
const project = path_1.default.join(root, 'project');
|
|
80
|
+
const home = path_1.default.join(root, 'home');
|
|
81
|
+
const awmHome = path_1.default.join(home, '.awm');
|
|
82
|
+
const registryRoot = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
83
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
84
|
+
fs_1.default.mkdirSync(path_1.default.join(registryRoot, 'skills', 'ejemplo-proceso'), { recursive: true });
|
|
85
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ name: 'process-model-e2e-fixture', private: true }));
|
|
86
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.invalid/baseline.git' }]));
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'skills', 'ejemplo-proceso', 'SKILL.md'), EJEMPLO_PROCESO_SKILL_MD);
|
|
88
|
+
return { root, project, home, awmHome };
|
|
89
|
+
}
|
|
90
|
+
function runCompiledCli(f, ...args) {
|
|
91
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [DIST_ENTRYPOINT, ...args], {
|
|
92
|
+
cwd: f.project,
|
|
93
|
+
encoding: 'utf8',
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
HOME: f.home,
|
|
97
|
+
USERPROFILE: f.home,
|
|
98
|
+
AWM_HOME: f.awmHome,
|
|
99
|
+
AWM_NO_UPDATE_CHECK: '1',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
expect(result.error).toBeUndefined();
|
|
103
|
+
return { code: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
104
|
+
}
|
|
105
|
+
describe('process model E2E against the real built binary (CA-1.2, CA-1.3)', () => {
|
|
106
|
+
jest.setTimeout(60_000);
|
|
107
|
+
beforeAll(() => {
|
|
108
|
+
if (!fs_1.default.existsSync(DIST_ENTRYPOINT)) {
|
|
109
|
+
throw new Error(`${DIST_ENTRYPOINT} no existe — este test ejecuta el binario compilado real. ` +
|
|
110
|
+
'Correr `npm run build` antes de `jest` (mismo pre-requisito que tests/structural/r3-cli-major-version.test.ts).');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
let f;
|
|
114
|
+
beforeEach(() => { f = fixture(); });
|
|
115
|
+
afterEach(() => { fs_1.default.rmSync(f.root, { recursive: true, force: true }); });
|
|
116
|
+
test('CA-1.2 — `process list` reporta ejemplo-proceso con su status', () => {
|
|
117
|
+
const result = runCompiledCli(f, 'process', 'list');
|
|
118
|
+
expect(result.code).toBe(0);
|
|
119
|
+
expect(result.stdout).toContain('ejemplo-proceso');
|
|
120
|
+
expect(result.stdout).toContain('draft');
|
|
121
|
+
});
|
|
122
|
+
test('CA-1.2 — `process show ejemplo-proceso --json` emite el modelo parseado completo', () => {
|
|
123
|
+
const result = runCompiledCli(f, 'process', 'show', 'ejemplo-proceso', '--json');
|
|
124
|
+
expect(result.code).toBe(0);
|
|
125
|
+
const model = JSON.parse(result.stdout);
|
|
126
|
+
expect(model).toEqual(expect.objectContaining({
|
|
127
|
+
name: 'ejemplo-proceso',
|
|
128
|
+
schema: 1,
|
|
129
|
+
status: 'draft',
|
|
130
|
+
entryPoint: true,
|
|
131
|
+
terminatesTo: 'none',
|
|
132
|
+
}));
|
|
133
|
+
expect(model.body.structure).toEqual([
|
|
134
|
+
{ id: 'SG-1', text: 'Diseñar', operations: [
|
|
135
|
+
{ id: 'OP-1.1', text: 'Elicitar requisitos' },
|
|
136
|
+
{ id: 'OP-1.2', text: 'Escribir el design doc' },
|
|
137
|
+
] },
|
|
138
|
+
{ id: 'SG-2', text: 'Ejecutar', operations: [
|
|
139
|
+
{ id: 'OP-2.1', text: 'Implementar por tasks' },
|
|
140
|
+
] },
|
|
141
|
+
]);
|
|
142
|
+
expect(model.body.routing).toEqual([
|
|
143
|
+
{ when: 'No hay design doc', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' },
|
|
144
|
+
{ when: 'Design doc aprobado', requiredState: 'SG-1', goesTo: 'OP-2.1', endsAt: 'SG-2' },
|
|
145
|
+
]);
|
|
146
|
+
expect(model.body.termination).toBe('finishing-a-development-branch');
|
|
147
|
+
// No source path leaks into the published/consumable view (see publicView
|
|
148
|
+
// in src/commands/process/index.ts) — assert the field is entirely absent.
|
|
149
|
+
expect(model).not.toHaveProperty('source');
|
|
150
|
+
});
|
|
151
|
+
test('CA-1.3 — `doctor --full` renderiza la seccion Processes desde el adapter, sin re-parseo ad-hoc', () => {
|
|
152
|
+
const result = runCompiledCli(f, 'doctor', '--full');
|
|
153
|
+
expect([0, 1]).toContain(result.code);
|
|
154
|
+
expect(result.stdout).toContain('Processes');
|
|
155
|
+
// El adapter de producción (productionDashboardAdapters.processes, src/core/dashboard/collect.ts)
|
|
156
|
+
// construye el id de item como `process.<name>` vía sanitizeDashboardId — esto es lo que
|
|
157
|
+
// demuestra que la sección viene del adapter real (discoverProcessModels() -> Dashboard),
|
|
158
|
+
// no de un parser propio del Dashboard.
|
|
159
|
+
expect(result.stdout).toContain('process.ejemplo-proceso');
|
|
160
|
+
});
|
|
161
|
+
});
|