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.
@@ -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 declarada no aplicable hasta R1', () => {
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
  });