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.
- package/dist/src/commands/ledger/index.js +13 -0
- package/dist/src/commands/process/index.js +81 -0
- package/dist/src/core/dashboard/collect.js +30 -5
- package/dist/src/core/dashboard/sanitize.js +24 -2
- 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/index.js +2 -0
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/process.test.js +88 -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/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/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
|
@@ -51,6 +51,17 @@ describe('awm ledger CLI', () => {
|
|
|
51
51
|
.toThrow(/defect-class.*kebab-case/i);
|
|
52
52
|
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([]);
|
|
53
53
|
});
|
|
54
|
+
test.each(['test', 'Test', 'test-check', 'test_x', 'placeholder', 'foo', 'bar', 'todo', 'tbd'])('add rejects a placeholder --desc %p before writing', (desc) => {
|
|
55
|
+
expect(() => run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
|
|
56
|
+
'--signature', 'sig-1', '--severity', 'blocker', '--desc', desc], cwd))
|
|
57
|
+
.toThrow(/placeholder.*ledger add --help/i);
|
|
58
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
test('add accepts a real description that merely contains the word test', () => {
|
|
61
|
+
run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
|
|
62
|
+
'--signature', 'sig-1', '--severity', 'blocker', '--desc', 'unit test for parseConfig throws on empty input'], cwd);
|
|
63
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toHaveLength(1);
|
|
64
|
+
});
|
|
54
65
|
test('list emits the branch entries as JSON', () => {
|
|
55
66
|
run(['add', '--branch', 'feat-x', '--polarity', 'win', '--class', 'proceso',
|
|
56
67
|
'--signature', 'good', '--severity', 'info', '--desc', 'nice'], cwd);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// cli/tests/commands/process.test.ts
|
|
4
|
+
const process_1 = require("../../src/commands/process");
|
|
5
|
+
function model(over = {}) {
|
|
6
|
+
return {
|
|
7
|
+
schema: 1, name: 'mi-proceso', status: 'draft', entryPoint: true, terminatesTo: 'none',
|
|
8
|
+
created: '2026-08-23', updated: '2026-08-23', source: '/r/skills/mi-proceso/SKILL.md',
|
|
9
|
+
body: {
|
|
10
|
+
objective: 'G — Objetivo.', appliesWhen: 'Siempre.',
|
|
11
|
+
structure: [{ id: 'SG-1', text: 'Uno', operations: [{ id: 'OP-1.1', text: 'Hacer' }] }],
|
|
12
|
+
routing: [{ when: 'Al empezar', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' }],
|
|
13
|
+
termination: 'none', unverified: ['Nada.'],
|
|
14
|
+
},
|
|
15
|
+
...over,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('awm process list', () => {
|
|
19
|
+
it('reporta los procesos descubiertos', () => {
|
|
20
|
+
const r = (0, process_1.runProcessList)({ models: [model()], diagnostics: [] });
|
|
21
|
+
expect(r.code).toBe(0);
|
|
22
|
+
expect(r.stdout).toContain('mi-proceso');
|
|
23
|
+
expect(r.stdout).toContain('draft');
|
|
24
|
+
});
|
|
25
|
+
it('sin modelos sale 0 y lo dice, no falla', () => {
|
|
26
|
+
const r = (0, process_1.runProcessList)({ models: [], diagnostics: [] });
|
|
27
|
+
expect(r.code).toBe(0);
|
|
28
|
+
expect(r.stdout).toMatch(/no process models/i);
|
|
29
|
+
});
|
|
30
|
+
it('emite los diagnósticos sin dejar de listar los sanos', () => {
|
|
31
|
+
const r = (0, process_1.runProcessList)({ models: [model()], diagnostics: ['/r/x: invalid process model — boom'] });
|
|
32
|
+
expect(r.code).toBe(0);
|
|
33
|
+
expect(r.stdout).toContain('mi-proceso');
|
|
34
|
+
expect(r.stderr).toContain('boom');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('awm process show --json', () => {
|
|
38
|
+
it('emite el modelo parseado como JSON', () => {
|
|
39
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'mi-proceso', true);
|
|
40
|
+
expect(r.code).toBe(0);
|
|
41
|
+
const parsed = JSON.parse(r.stdout);
|
|
42
|
+
expect(parsed).toEqual(expect.objectContaining({ name: 'mi-proceso', schema: 1, status: 'draft' }));
|
|
43
|
+
expect(parsed.body.routing).toEqual([{ when: 'Al empezar', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' }]);
|
|
44
|
+
});
|
|
45
|
+
it('el JSON no filtra el path del filesystem del registry', () => {
|
|
46
|
+
const parsed = JSON.parse((0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'mi-proceso', true).stdout);
|
|
47
|
+
expect(JSON.stringify(parsed)).not.toContain('/r/skills');
|
|
48
|
+
});
|
|
49
|
+
it('un nombre inexistente sale 2 y nombra lo disponible', () => {
|
|
50
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'no-existe', true);
|
|
51
|
+
expect(r.code).toBe(2);
|
|
52
|
+
expect(r.stderr).toMatch(/no-existe/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('awm process show (texto)', () => {
|
|
56
|
+
it('renderiza name/status/objective/structure en modo texto', () => {
|
|
57
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'mi-proceso', false);
|
|
58
|
+
expect(r.code).toBe(0);
|
|
59
|
+
expect(r.stdout).toContain('mi-proceso (draft)');
|
|
60
|
+
expect(r.stdout).toContain('G — Objetivo.');
|
|
61
|
+
expect(r.stdout).toContain('SG-1 — Uno');
|
|
62
|
+
expect(r.stdout).toContain('OP-1.1 — Hacer');
|
|
63
|
+
});
|
|
64
|
+
it('un nombre inexistente en modo texto sale 2 y nombra lo disponible', () => {
|
|
65
|
+
const r = (0, process_1.runProcessShow)({ models: [model()], diagnostics: [] }, 'no-existe', false);
|
|
66
|
+
expect(r.code).toBe(2);
|
|
67
|
+
expect(r.stderr).toMatch(/no-existe/);
|
|
68
|
+
expect(r.stdout).toBe('');
|
|
69
|
+
});
|
|
70
|
+
it('sanea bytes de control/ANSI del body antes de escribir a stdout', () => {
|
|
71
|
+
const hostile = model({
|
|
72
|
+
body: {
|
|
73
|
+
objective: 'G — Objetivo\x1b[31mmalicioso\x1b[0m.',
|
|
74
|
+
appliesWhen: 'Siempre.',
|
|
75
|
+
structure: [{ id: 'SG-1', text: 'Uno\x1b]0;pwned\x07', operations: [{ id: 'OP-1.1', text: 'Hacer\x1b[2Jcosas' }] }],
|
|
76
|
+
routing: [{ when: 'Al empezar', requiredState: '', goesTo: 'OP-1.1', endsAt: 'SG-1' }],
|
|
77
|
+
termination: 'none', unverified: ['Nada.'],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const r = (0, process_1.runProcessShow)({ models: [hostile], diagnostics: [] }, 'mi-proceso', false);
|
|
81
|
+
expect(r.code).toBe(0);
|
|
82
|
+
// eslint-disable-next-line no-control-regex -- necesitamos verificar la ausencia deliberada de C0
|
|
83
|
+
expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
|
|
84
|
+
expect(r.stdout).toContain('Objetivo[31mmalicioso[0m.');
|
|
85
|
+
expect(r.stdout).toContain('Uno]0;pwned');
|
|
86
|
+
expect(r.stdout).toContain('Hacer[2Jcosas');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -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
|
});
|