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
|
@@ -21,9 +21,10 @@ describe('collectDashboardSnapshot', () => {
|
|
|
21
21
|
const project = jest.fn();
|
|
22
22
|
const plans = jest.fn();
|
|
23
23
|
const execution = jest.fn();
|
|
24
|
+
const processes = jest.fn();
|
|
24
25
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
25
26
|
cwd: '/definitely-not-a-project', now: fixedNow,
|
|
26
|
-
adapters: { machine: () => ({ findings: [] }), project, plans, execution },
|
|
27
|
+
adapters: { machine: () => ({ findings: [] }), project, plans, execution, processes },
|
|
27
28
|
});
|
|
28
29
|
expect(snapshot.project).toEqual({ detected: false, label: 'No project detected' });
|
|
29
30
|
expect(snapshot.sections.map((section) => section.id)).toEqual(['machine']);
|
|
@@ -31,14 +32,15 @@ describe('collectDashboardSnapshot', () => {
|
|
|
31
32
|
expect(project).not.toHaveBeenCalled();
|
|
32
33
|
expect(plans).not.toHaveBeenCalled();
|
|
33
34
|
expect(execution).not.toHaveBeenCalled();
|
|
35
|
+
expect(processes).not.toHaveBeenCalled();
|
|
34
36
|
});
|
|
35
37
|
it('fails loudly for malformed central machine findings', () => {
|
|
36
|
-
expect(() => (0, collect_1.collectDashboardSnapshot)({ cwd: '/definitely-not-a-project', now: fixedNow, adapters: { machine: () => ({ findings: [{ id: '', label: 'Preferences', state: 'ok' }] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() } })).toThrow(/finding/i);
|
|
38
|
+
expect(() => (0, collect_1.collectDashboardSnapshot)({ cwd: '/definitely-not-a-project', now: fixedNow, adapters: { machine: () => ({ findings: [{ id: '', label: 'Preferences', state: 'ok' }] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn(), processes: jest.fn() } })).toThrow(/finding/i);
|
|
37
39
|
});
|
|
38
40
|
it.each([undefined, null, {}, 'healthy'])('rejects a non-array central machine findings value: %p', (findings) => {
|
|
39
41
|
expect(() => (0, collect_1.collectDashboardSnapshot)({
|
|
40
42
|
cwd: '/definitely-not-a-project', now: fixedNow,
|
|
41
|
-
adapters: { machine: () => ({ findings }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() },
|
|
43
|
+
adapters: { machine: () => ({ findings }), project: jest.fn(), plans: jest.fn(), execution: jest.fn(), processes: jest.fn() },
|
|
42
44
|
})).toThrow('Dashboard findings must be an array');
|
|
43
45
|
});
|
|
44
46
|
it('uses exact verified remediation commands and stable ordered sections', () => {
|
|
@@ -49,6 +51,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
49
51
|
project: () => ({ label: 'Demo', findings: [{ id: 'project.profile.missing', label: 'Profile', state: 'missing' }] }),
|
|
50
52
|
plans: () => Array.from({ length: 2000 }, (_, index) => ({ id: `plan.${index}`, label: `Plan ${index}`, state: 'ok' })),
|
|
51
53
|
execution: () => ({}),
|
|
54
|
+
processes: () => [],
|
|
52
55
|
},
|
|
53
56
|
});
|
|
54
57
|
expect(snapshot.sections.map((section) => section.id)).toEqual(// verifies R6.3
|
|
@@ -59,11 +62,11 @@ describe('collectDashboardSnapshot', () => {
|
|
|
59
62
|
expect(JSON.stringify(snapshot)).not.toMatch(/score|ranking/i);
|
|
60
63
|
});
|
|
61
64
|
it('el snapshot declara schema 2', () => {
|
|
62
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
65
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
63
66
|
expect(snapshot.schema).toBe(2);
|
|
64
67
|
});
|
|
65
|
-
it('processes queda
|
|
66
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
68
|
+
it('processes queda no aplicable cuando el adapter no declara modelos', () => {
|
|
69
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
67
70
|
const processes = snapshot.sections.find((section) => section.id === 'processes');
|
|
68
71
|
expect(processes).toEqual({ id: 'processes', availability: 'not_applicable', items: [] });
|
|
69
72
|
});
|
|
@@ -73,7 +76,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
73
76
|
adapters: {
|
|
74
77
|
machine: () => ({ findings: [{ id: 'unknown', label: 'Unknown', state: 'missing' }] }),
|
|
75
78
|
project: () => { throw new Error('corrupt project source'); },
|
|
76
|
-
plans: () => [], execution: () => undefined,
|
|
79
|
+
plans: () => [], execution: () => undefined, processes: () => [],
|
|
77
80
|
},
|
|
78
81
|
});
|
|
79
82
|
expect(snapshot.sections.find((section) => section.id === 'machine')?.items).toEqual([]);
|
|
@@ -83,7 +86,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
83
86
|
it('marks execution-derived sections unavailable when no read-only execution source exists', () => {
|
|
84
87
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
85
88
|
cwd: process.cwd(), now: fixedNow,
|
|
86
|
-
adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined },
|
|
89
|
+
adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] },
|
|
87
90
|
});
|
|
88
91
|
for (const id of ['execution', 'qa', 'docs', 'retro']) {
|
|
89
92
|
expect(snapshot.sections.find((section) => section.id === id)?.availability).toBe('unavailable');
|
|
@@ -92,29 +95,29 @@ describe('collectDashboardSnapshot', () => {
|
|
|
92
95
|
it('isolates malformed optional source data to its owning section', () => {
|
|
93
96
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
94
97
|
cwd: process.cwd(), now: fixedNow,
|
|
95
|
-
adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [{ id: 'bad', label: 'Profile', state: 'invented' }] }), plans: () => [], execution: () => undefined },
|
|
98
|
+
adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [{ id: 'bad', label: 'Profile', state: 'invented' }] }), plans: () => [], execution: () => undefined, processes: () => [] },
|
|
96
99
|
});
|
|
97
100
|
expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
|
|
98
101
|
expect(snapshot.sections.find((section) => section.id === 'machine')?.availability).toBe('available');
|
|
99
102
|
});
|
|
100
103
|
it('isolates malformed post-sanitization plan findings', () => {
|
|
101
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: '', label: 'Profile', state: 'ok' }], execution: () => undefined } });
|
|
104
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: '', label: 'Profile', state: 'ok' }], execution: () => undefined, processes: () => [] } });
|
|
102
105
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('unavailable');
|
|
103
106
|
});
|
|
104
107
|
it('isolates malformed project findings without dropping other sections', () => {
|
|
105
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [{ id: '', label: 'Profile', state: 'ok' }] }), plans: () => [], execution: () => undefined } });
|
|
108
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [{ id: '', label: 'Profile', state: 'ok' }] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
106
109
|
expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
|
|
107
110
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('available');
|
|
108
111
|
});
|
|
109
112
|
it.each(['execution', 'qa', 'docs', 'retro'])('isolates malformed %s findings', (key) => {
|
|
110
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }) } });
|
|
113
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }), processes: () => [] } });
|
|
111
114
|
expect(snapshot.sections.find((section) => section.id === key)?.availability).toBe('unavailable');
|
|
112
115
|
});
|
|
113
116
|
it('renders exact remediation only for a canonical optional source failure', () => {
|
|
114
117
|
const knownFailure = Object.assign(new Error('sensors unavailable'), { findingId: 'project.sensors.unavailable', remediationVerified: true });
|
|
115
118
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
116
119
|
cwd: process.cwd(), now: fixedNow,
|
|
117
|
-
adapters: { machine: () => ({ findings: [] }), project: () => { throw knownFailure; }, plans: () => [], execution: () => undefined },
|
|
120
|
+
adapters: { machine: () => ({ findings: [] }), project: () => { throw knownFailure; }, plans: () => [], execution: () => undefined, processes: () => [] },
|
|
118
121
|
});
|
|
119
122
|
expect(snapshot.sections.find((section) => section.id === 'project')?.items).toEqual([
|
|
120
123
|
expect.objectContaining({ id: 'project.sensors.unavailable', state: 'unavailable', remediation: 'awm sensors status' }),
|
|
@@ -133,6 +136,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
133
136
|
throw failure; return []; },
|
|
134
137
|
execution: () => { if (adapter === 'execution')
|
|
135
138
|
throw failure; return undefined; },
|
|
139
|
+
processes: () => [],
|
|
136
140
|
},
|
|
137
141
|
});
|
|
138
142
|
expect(snapshot.sections.find((section) => section.id === sectionId)?.items[0]).toEqual(expect.objectContaining({ id: findingId, remediation }));
|
|
@@ -143,7 +147,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
143
147
|
adapters: { machine: () => ({ findings: [
|
|
144
148
|
{ id: 'machine.registries.stale', label: 'Registries', state: 'attention' },
|
|
145
149
|
{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' },
|
|
146
|
-
] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() },
|
|
150
|
+
] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn(), processes: jest.fn() },
|
|
147
151
|
});
|
|
148
152
|
expect(snapshot.sections[0].items.map((item) => item.id)).toEqual(['machine.preferences.missing', 'machine.registries.stale']);
|
|
149
153
|
});
|
|
@@ -154,13 +158,13 @@ describe('collectDashboardSnapshot', () => {
|
|
|
154
158
|
: expected === 'retro_pending' ? { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } }
|
|
155
159
|
: expected === 'qa_pending' ? { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 1 } }
|
|
156
160
|
: { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
|
|
157
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: 'plan.lifecycle', label: 'Profile', state: 'ok', lifecycle }], execution: () => undefined } });
|
|
161
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: 'plan.lifecycle', label: 'Profile', state: 'ok', lifecycle }], execution: () => undefined, processes: () => [] } });
|
|
158
162
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.items[0].detail).toBe(expected);
|
|
159
163
|
});
|
|
160
164
|
it('degrades a machine-only dashboard for actionable machine findings', () => {
|
|
161
165
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
162
166
|
cwd: '/definitely-not-a-project', now: fixedNow,
|
|
163
|
-
adapters: { machine: () => ({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' }] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() },
|
|
167
|
+
adapters: { machine: () => ({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' }] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn(), processes: jest.fn() },
|
|
164
168
|
});
|
|
165
169
|
expect(snapshot.sections.map((section) => section.id)).toEqual(['machine']);
|
|
166
170
|
expect(snapshot.overall).toBe('degraded');
|
|
@@ -169,7 +173,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
169
173
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
170
174
|
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
171
175
|
(0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
|
|
172
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
176
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
173
177
|
const history = snapshot.sections.find((section) => section.id === 'history');
|
|
174
178
|
expect(snapshot.confidence).toBe('provisional');
|
|
175
179
|
expect(history?.availability).toBe('available');
|
|
@@ -180,7 +184,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
180
184
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
181
185
|
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
182
186
|
(0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleState: 'blocked', plan: { ref: 'docs/plans/current.md', state: 'blocked' } });
|
|
183
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
187
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
184
188
|
expect(snapshot.sections.find((section) => section.id === 'history')?.items).toEqual([
|
|
185
189
|
expect.objectContaining({ state: 'attention', remediation: 'awm preflight' }),
|
|
186
190
|
]);
|
|
@@ -203,7 +207,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
203
207
|
fs_1.default.mkdirSync(path_1.default.join(root, '.awm', 'evidence'), { recursive: true });
|
|
204
208
|
fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence', 'cycles'));
|
|
205
209
|
}
|
|
206
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
210
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
207
211
|
const history = snapshot.sections.find((section) => section.id === 'history');
|
|
208
212
|
expect(history).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
|
|
209
213
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
@@ -219,7 +223,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
219
223
|
const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
|
|
220
224
|
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
221
225
|
fs_1.default.symlinkSync(externalFile, path_1.default.join(directory, `${cycleId}.json`));
|
|
222
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
226
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
223
227
|
expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
|
|
224
228
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
225
229
|
fs_1.default.rmSync(external, { recursive: true, force: true });
|
|
@@ -232,7 +236,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
232
236
|
const record = (0, evidence_fixtures_1.cycleEvidenceFixture)();
|
|
233
237
|
const filename = caseName === 'duplicate' ? `${'b'.repeat(64)}.json` : `${'b'.repeat(64)}.json`;
|
|
234
238
|
fs_1.default.writeFileSync(path_1.default.join(directory, filename), JSON.stringify(caseName === 'duplicate' ? record : { ...record, cycleId: 'c'.repeat(64) }));
|
|
235
|
-
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
239
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes: () => [] } });
|
|
236
240
|
expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
|
|
237
241
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
238
242
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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 collect_1 = require("../../../src/core/dashboard/collect");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
function adapters(processes) {
|
|
11
|
+
return { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined, processes };
|
|
12
|
+
}
|
|
13
|
+
describe('sección processes del Dashboard', () => {
|
|
14
|
+
let root;
|
|
15
|
+
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-processes-section-')); fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}'); });
|
|
16
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
17
|
+
function snapshot(a) {
|
|
18
|
+
return (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: '2026-08-23T00:00:00.000Z', adapters: a });
|
|
19
|
+
}
|
|
20
|
+
function section(a) {
|
|
21
|
+
return snapshot(a).sections.find((s) => s.id === 'processes');
|
|
22
|
+
}
|
|
23
|
+
it('puebla la sección desde el adapter', () => {
|
|
24
|
+
const s = section(adapters(() => [{ name: 'mi-proceso', status: 'active' }]));
|
|
25
|
+
expect(s.availability).toBe('available');
|
|
26
|
+
expect(s.items).toEqual([expect.objectContaining({ id: 'process.mi-proceso', label: 'Process', state: 'ok', detail: 'active' })]);
|
|
27
|
+
});
|
|
28
|
+
it('un draft se reporta como attention, no como ok', () => {
|
|
29
|
+
expect(section(adapters(() => [{ name: 'x', status: 'draft' }])).items[0]).toEqual(expect.objectContaining({ state: 'attention', detail: 'draft' }));
|
|
30
|
+
});
|
|
31
|
+
it('un draft trae remediation "awm process list" y degrada el overall del snapshot', () => {
|
|
32
|
+
const snap = snapshot(adapters(() => [{ name: 'x', status: 'draft' }]));
|
|
33
|
+
const s = snap.sections.find((sec) => sec.id === 'processes');
|
|
34
|
+
expect(s.items[0]).toEqual(expect.objectContaining({ state: 'attention', detail: 'draft', remediation: 'awm process list' }));
|
|
35
|
+
expect(snap.overall).toBe('degraded');
|
|
36
|
+
});
|
|
37
|
+
it('sin procesos la sección queda not_applicable, como antes de R1a', () => {
|
|
38
|
+
expect(section(adapters(() => []))).toEqual(expect.objectContaining({ availability: 'not_applicable', items: [] }));
|
|
39
|
+
});
|
|
40
|
+
it('un adapter que lanza degrada a unavailable sin tumbar el snapshot', () => {
|
|
41
|
+
const s = section(adapters(() => { throw new Error('/home/u/secreto boom'); }));
|
|
42
|
+
expect(s.availability).toBe('unavailable');
|
|
43
|
+
expect(JSON.stringify(s)).not.toContain('secreto');
|
|
44
|
+
});
|
|
45
|
+
it('un registry externo no puede inyectar markup ni rutas por el nombre', () => {
|
|
46
|
+
// El nombre ya fue validado como slug por el contrato (Task 1). Este test
|
|
47
|
+
// prueba la SEGUNDA barrera: aunque un adapter mal escrito dejara pasar
|
|
48
|
+
// algo hostil, el sanitizador lo neutraliza antes del render.
|
|
49
|
+
const s = section(adapters(() => [{ name: '<script>/etc/passwd', status: 'active' }]));
|
|
50
|
+
const serialized = JSON.stringify(s);
|
|
51
|
+
expect(serialized).not.toContain('<script>');
|
|
52
|
+
expect(serialized).not.toContain('/etc/passwd');
|
|
53
|
+
});
|
|
54
|
+
it('un nombre con forma de slug inválida no produce un id process.<bad-name>', () => {
|
|
55
|
+
// 'mi proceso' (con espacio) no matchea DANGEROUS (no es un path, token,
|
|
56
|
+
// secreto ni markup), así que sanitizeDashboardSource lo deja pasar tal
|
|
57
|
+
// cual sobre el source crudo. La SEGUNDA barrera tiene que actuar sobre
|
|
58
|
+
// el id ya construido (`process.mi proceso`), no sobre el nombre crudo:
|
|
59
|
+
// PROCESS_FINDING_ID no matchea ese id, así que debe caer al fallback
|
|
60
|
+
// hasheado `item-<hash>` — igual que cualquier otro id malformado — y
|
|
61
|
+
// nunca renderizarse como `process.mi proceso`.
|
|
62
|
+
const s = section(adapters(() => [{ name: 'mi proceso', status: 'active' }]));
|
|
63
|
+
expect(s.items).toHaveLength(1);
|
|
64
|
+
const item = s.items[0];
|
|
65
|
+
expect(item.id).toMatch(/^item-[0-9a-f]{16}$/);
|
|
66
|
+
expect(item.id).not.toContain('process.mi proceso');
|
|
67
|
+
expect(item.label).toBe('Process');
|
|
68
|
+
expect(item.state).toBe('ok');
|
|
69
|
+
expect(item.detail).toBe('active');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -12,6 +12,44 @@ const child_process_1 = require("child_process");
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const os_1 = __importDefault(require("os"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const PROCESS_MODEL_FIXTURE = `---
|
|
16
|
+
awm: process-model
|
|
17
|
+
schema: 1
|
|
18
|
+
name: NAME
|
|
19
|
+
status: STATUS
|
|
20
|
+
entry_point: true
|
|
21
|
+
terminates_to: none
|
|
22
|
+
created: 2026-08-23
|
|
23
|
+
updated: 2026-08-23
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Objetivo
|
|
27
|
+
|
|
28
|
+
G — Objetivo.
|
|
29
|
+
|
|
30
|
+
## Cuándo aplica
|
|
31
|
+
|
|
32
|
+
Siempre.
|
|
33
|
+
|
|
34
|
+
## Estructura
|
|
35
|
+
|
|
36
|
+
- SG-1 — Uno
|
|
37
|
+
- OP-1.1 — Hacer
|
|
38
|
+
|
|
39
|
+
## Ruteo
|
|
40
|
+
|
|
41
|
+
| Cuándo | Estado requerido | Va a | Termina en |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| Al empezar | | OP-1.1 | SG-1 |
|
|
44
|
+
|
|
45
|
+
## Terminación
|
|
46
|
+
|
|
47
|
+
none
|
|
48
|
+
|
|
49
|
+
## Sin verificar
|
|
50
|
+
|
|
51
|
+
- Nada.
|
|
52
|
+
`;
|
|
15
53
|
function context() {
|
|
16
54
|
return {
|
|
17
55
|
machine: {},
|
|
@@ -174,4 +212,24 @@ describe('productionDashboardAdapters', () => {
|
|
|
174
212
|
expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-journal-only|dashboard@example|Dashboard Test/i);
|
|
175
213
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
176
214
|
});
|
|
215
|
+
it('processes delegates to discoverProcessModels() and maps its models to {name, status}', () => {
|
|
216
|
+
const awmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-processes-home-'));
|
|
217
|
+
const originalAwmHome = process.env.AWM_HOME;
|
|
218
|
+
process.env.AWM_HOME = awmHome;
|
|
219
|
+
try {
|
|
220
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
221
|
+
const skillDir = path_1.default.join(awmHome, 'registries', 'baseline', 'skills', 'mi-proceso');
|
|
222
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
223
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), PROCESS_MODEL_FIXTURE.replace('NAME', 'mi-proceso').replace('STATUS', 'active'));
|
|
224
|
+
const adapters = (0, collect_1.productionDashboardAdapters)(context());
|
|
225
|
+
expect(adapters.processes({ root: '/private/project' })).toEqual([{ name: 'mi-proceso', status: 'active' }]);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (originalAwmHome === undefined)
|
|
229
|
+
delete process.env.AWM_HOME;
|
|
230
|
+
else
|
|
231
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
232
|
+
fs_1.default.rmSync(awmHome, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
177
235
|
});
|
|
@@ -217,6 +217,44 @@ describe('collectDeclaredOrchestrators', () => {
|
|
|
217
217
|
expect(diagnostics[0]).toMatch(/shared/);
|
|
218
218
|
expect(diagnostics[0]).toMatch(/duplicate|shadow/i);
|
|
219
219
|
});
|
|
220
|
+
it('sanea el nombre antes de interpolarlo en el diagnostico de duplicado', () => {
|
|
221
|
+
(0, registries_1.writeRegistriesConfig)([
|
|
222
|
+
{ name: 'first', remote: 'unused' },
|
|
223
|
+
{ name: 'second', remote: 'unused' },
|
|
224
|
+
]);
|
|
225
|
+
const nombreHostil = 'shared\x1b[31m';
|
|
226
|
+
writeManifest('first', {
|
|
227
|
+
orchestrator: { name: nombreHostil, appliesWhen: 'primero', terminatesTo: 'a' },
|
|
228
|
+
});
|
|
229
|
+
writeManifest('second', {
|
|
230
|
+
orchestrator: { name: nombreHostil, appliesWhen: 'segundo', terminatesTo: 'b' },
|
|
231
|
+
});
|
|
232
|
+
const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
233
|
+
expect(diagnostics).toHaveLength(1);
|
|
234
|
+
// eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
|
|
235
|
+
expect(diagnostics[0]).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
236
|
+
expect(diagnostics[0]).toContain('shared[31m');
|
|
237
|
+
});
|
|
238
|
+
it('emite un diagnostico de colision post-saneo entre nombres crudos distintos, sin descartar ninguno', () => {
|
|
239
|
+
(0, registries_1.writeRegistriesConfig)([
|
|
240
|
+
{ name: 'first', remote: 'unused' },
|
|
241
|
+
{ name: 'second', remote: 'unused' },
|
|
242
|
+
]);
|
|
243
|
+
writeManifest('first', {
|
|
244
|
+
orchestrator: { name: 'foo_bar', appliesWhen: 'x', terminatesTo: 'a' },
|
|
245
|
+
});
|
|
246
|
+
writeManifest('second', {
|
|
247
|
+
orchestrator: { name: 'foo*bar', appliesWhen: 'y', terminatesTo: 'b' },
|
|
248
|
+
});
|
|
249
|
+
const { declared, diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
|
|
250
|
+
// Ambas declaraciones son genuinamente distintas (nombre crudo distinto) — ninguna se descarta.
|
|
251
|
+
expect(declared).toHaveLength(2);
|
|
252
|
+
expect(declared.map((d) => d.name).sort()).toEqual(['foo*bar', 'foo_bar']);
|
|
253
|
+
// Pero se advierte de la colision post-saneo (ambas renderizan como "foobar").
|
|
254
|
+
expect(diagnostics).toHaveLength(1);
|
|
255
|
+
expect(diagnostics[0]).toMatch(/foobar/);
|
|
256
|
+
expect(diagnostics[0]).toMatch(/collis|same composed name/i);
|
|
257
|
+
});
|
|
220
258
|
it('no dedupea orquestadores con nombres distintos, ambos se conservan', () => {
|
|
221
259
|
(0, registries_1.writeRegistriesConfig)([
|
|
222
260
|
{ name: 'first', remote: 'unused' },
|
|
@@ -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
|
+
});
|