agentic-workflow-manager 8.3.0 → 8.5.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.
Files changed (36) hide show
  1. package/dist/src/commands/doctor.js +40 -1
  2. package/dist/src/commands/evidence/index.js +95 -0
  3. package/dist/src/core/dashboard/collect.js +223 -0
  4. package/dist/src/core/dashboard/plan-state.js +44 -0
  5. package/dist/src/core/dashboard/render-html.js +88 -0
  6. package/dist/src/core/dashboard/render-terminal.js +45 -0
  7. package/dist/src/core/dashboard/sanitize.js +62 -0
  8. package/dist/src/core/dashboard/styles.js +51 -0
  9. package/dist/src/core/dashboard/types.js +21 -0
  10. package/dist/src/core/dashboard/validate.js +106 -0
  11. package/dist/src/core/dashboard/write-html.js +88 -0
  12. package/dist/src/core/diagnostics/context.js +1 -1
  13. package/dist/src/core/evidence/capture.js +46 -0
  14. package/dist/src/core/evidence/history.js +50 -0
  15. package/dist/src/core/evidence/store.js +32 -0
  16. package/dist/src/core/evidence/types.js +97 -0
  17. package/dist/src/index.js +2 -0
  18. package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
  19. package/dist/tests/commands/doctor.test.js +160 -0
  20. package/dist/tests/core/dashboard/collect.test.js +242 -0
  21. package/dist/tests/core/dashboard/contracts.test.js +92 -0
  22. package/dist/tests/core/dashboard/plan-state.test.js +32 -0
  23. package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
  24. package/dist/tests/core/dashboard/render-html.test.js +191 -0
  25. package/dist/tests/core/dashboard/render-terminal.test.js +72 -0
  26. package/dist/tests/core/dashboard/write-html.test.js +112 -0
  27. package/dist/tests/core/evidence/capture.test.js +48 -0
  28. package/dist/tests/core/evidence/command.test.js +37 -0
  29. package/dist/tests/core/evidence/history.test.js +41 -0
  30. package/dist/tests/core/evidence/store.test.js +32 -0
  31. package/dist/tests/core/evidence/types.test.js +30 -0
  32. package/dist/tests/helpers/dashboard-fixtures.js +66 -0
  33. package/dist/tests/helpers/evidence-fixtures.js +17 -0
  34. package/dist/tests/integration/doctor-dashboard.e2e.test.js +177 -0
  35. package/dist/tests/integration/published-doctor-evidence.e2e.test.js +180 -0
  36. package/package.json +1 -1
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  const fs_1 = __importDefault(require("fs"));
22
22
  const os_1 = __importDefault(require("os"));
23
23
  const path_1 = __importDefault(require("path"));
24
+ const child_process_1 = require("child_process");
24
25
  describe('awm doctor no escribe nada', () => {
25
26
  let home;
26
27
  let projectRoot;
@@ -32,7 +33,8 @@ describe('awm doctor no escribe nada', () => {
32
33
  process.env.HOME = home;
33
34
  process.env.AWM_HOME = path_1.default.join(home, '.awm');
34
35
  projectRoot = path_1.default.join(home, 'proj');
35
- fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.git'), { recursive: true });
36
+ fs_1.default.mkdirSync(projectRoot, { recursive: true });
37
+ (0, child_process_1.execFileSync)('git', ['init', '-q', projectRoot]);
36
38
  jest.resetModules();
37
39
  writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
38
40
  });
@@ -62,6 +64,20 @@ describe('awm doctor no escribe nada', () => {
62
64
  walk('.');
63
65
  return out.sort();
64
66
  };
67
+ const bytesOf = (dir) => {
68
+ const out = [];
69
+ const walk = (sub) => {
70
+ for (const entry of fs_1.default.readdirSync(path_1.default.join(dir, sub), { withFileTypes: true })) {
71
+ const rel = path_1.default.join(sub, entry.name);
72
+ if (entry.isDirectory())
73
+ walk(rel);
74
+ else
75
+ out.push([rel, fs_1.default.readFileSync(path_1.default.join(dir, rel)).toString('base64')]);
76
+ }
77
+ };
78
+ walk('.');
79
+ return out.sort(([left], [right]) => left.localeCompare(right));
80
+ };
65
81
  it('no crea AWM_HOME ni preferences.json en una máquina limpia', () => {
66
82
  const { runDoctor } = require('../../src/commands/doctor');
67
83
  const before = treeOf(home);
@@ -94,4 +110,41 @@ describe('awm doctor no escribe nada', () => {
94
110
  const emitted = writeSpy.mock.calls.map((c) => String(c[0])).join('');
95
111
  expect(JSON.parse(emitted).providers.map((p) => p.id)).toEqual(['claude-code']);
96
112
  });
113
+ it('collecting the dashboard leaves project, preferences, journal, ledger, and git bytes unchanged', () => {
114
+ fs_1.default.mkdirSync(path_1.default.join(home, '.awm', 'ledger'), { recursive: true });
115
+ fs_1.default.writeFileSync(path_1.default.join(home, '.awm', 'preferences.json'), '{"defaultAgent":"claude-code","enabledAgents":["claude-code"],"installMethod":"symlink","defaultScope":"local"}\n');
116
+ fs_1.default.writeFileSync(path_1.default.join(home, '.awm', 'ledger', 'events.jsonl'), '{"event":"before"}\n');
117
+ fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm', 'journal'), { recursive: true });
118
+ fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), '{"extensions":[]}\n');
119
+ fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'journal', 'state.json'), '{"state":"active"}\n');
120
+ const before = { home: bytesOf(home), project: bytesOf(projectRoot), git: (() => { try {
121
+ return (0, child_process_1.execFileSync)('git', ['status', '--porcelain=v1'], { cwd: projectRoot, encoding: 'utf8' });
122
+ }
123
+ catch (error) {
124
+ return String(error);
125
+ } })() };
126
+ const { collectDashboardSnapshot } = require('../../src/core/dashboard/collect');
127
+ collectDashboardSnapshot({ cwd: projectRoot, now: '2026-08-22T00:00:00.000Z' });
128
+ const after = { home: bytesOf(home), project: bytesOf(projectRoot), git: (() => { try {
129
+ return (0, child_process_1.execFileSync)('git', ['status', '--porcelain=v1'], { cwd: projectRoot, encoding: 'utf8' });
130
+ }
131
+ catch (error) {
132
+ return String(error);
133
+ } })() };
134
+ expect(after).toEqual(before);
135
+ });
136
+ it('--html replaces only its explicitly requested target', () => {
137
+ const target = path_1.default.join(projectRoot, 'dashboard.html');
138
+ fs_1.default.writeFileSync(target, 'old dashboard');
139
+ const before = { home: bytesOf(home), project: bytesOf(projectRoot) };
140
+ const { runDoctor } = require('../../src/commands/doctor');
141
+ const code = runDoctor({ cwd: projectRoot, html: target, force: true });
142
+ const after = { home: bytesOf(home), project: bytesOf(projectRoot) };
143
+ expect([0, 1]).toContain(code);
144
+ const targetFromHome = path_1.default.join('proj', 'dashboard.html');
145
+ expect(after.home.filter(([name]) => name !== targetFromHome)).toEqual(before.home.filter(([name]) => name !== targetFromHome));
146
+ expect(after.project.filter(([name]) => name !== 'dashboard.html')).toEqual(before.project.filter(([name]) => name !== 'dashboard.html'));
147
+ expect(fs_1.default.readFileSync(target, 'utf8')).not.toBe('old dashboard');
148
+ expect(after.project.map(([name]) => name).filter((name) => name.includes('.tmp'))).toEqual([]);
149
+ });
97
150
  });
@@ -4,9 +4,169 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const doctor_1 = require("../../src/commands/doctor");
7
+ const commander_1 = require("commander");
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const os_1 = __importDefault(require("os"));
9
10
  const path_1 = __importDefault(require("path"));
11
+ const dashboard_fixtures_1 = require("../helpers/dashboard-fixtures");
12
+ const types_1 = require("../../src/core/dashboard/types");
13
+ const render_html_1 = require("../../src/core/dashboard/render-html");
14
+ describe('runDoctor legacy JSON fixtures', () => {
15
+ it.each(['bare-home', 'project'])('keeps %s JSON byte-for-byte compatible', (kind) => {
16
+ const captured = (0, dashboard_fixtures_1.captureDoctorJsonFixture)(kind);
17
+ try {
18
+ const expected = fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'fixtures', 'doctor-json', `${kind}.json`), 'utf-8');
19
+ expect(captured.output).toBe(expected);
20
+ expect(captured.code).toBe(1);
21
+ const parsed = JSON.parse(captured.output);
22
+ expect(parsed).toEqual(expect.objectContaining({
23
+ overall: 'degraded',
24
+ providers: expect.any(Array),
25
+ }));
26
+ const provider = parsed.providers[0];
27
+ expect(provider).toEqual(expect.objectContaining({
28
+ id: 'copilot',
29
+ label: 'Copilot',
30
+ tier: 'agents-md-managed',
31
+ checks: expect.any(Array),
32
+ }));
33
+ expect(provider.checks).toEqual(expect.arrayContaining([
34
+ expect.objectContaining({ id: 'context.global', state: kind === 'project' ? 'stale' : 'absent' }),
35
+ ]));
36
+ }
37
+ finally {
38
+ captured.cleanup();
39
+ }
40
+ });
41
+ });
42
+ describe('runDoctor dashboard modes', () => {
43
+ it('CLI rejects --html without an argument before collection or writes', async () => {
44
+ const program = new commander_1.Command();
45
+ const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
46
+ const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
47
+ const previousExitCode = process.exitCode;
48
+ try {
49
+ (0, doctor_1.registerDoctorCommand)(program);
50
+ await program.parseAsync(['node', 'awm', 'doctor', '--html']);
51
+ expect(process.exitCode).toBe(2);
52
+ expect(stderr.mock.calls.map((call) => String(call[0])).join('')).toContain('--html requires a file target');
53
+ expect(stdout).not.toHaveBeenCalled();
54
+ }
55
+ finally {
56
+ process.exitCode = previousExitCode;
57
+ stderr.mockRestore();
58
+ stdout.mockRestore();
59
+ }
60
+ });
61
+ it.each([
62
+ [{ json: true, full: true }, '--json cannot be combined with --full'],
63
+ [{ json: true, html: 'report.html' }, '--json cannot be combined with --html'],
64
+ [{ full: true, html: 'report.html' }, '--full cannot be combined with --html'],
65
+ [{ force: true }, '--force requires --html'],
66
+ [{ html: '' }, '--html requires a file target'],
67
+ ])('rejects incompatible options before collection', (options, message) => {
68
+ const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
69
+ try {
70
+ expect((0, doctor_1.runDoctor)({ ...options, collectSnapshot: () => { throw new Error('collection must not run'); } })).toBe(2);
71
+ expect(stderr.mock.calls.map((call) => String(call[0])).join('')).toContain(message);
72
+ }
73
+ finally {
74
+ stderr.mockRestore();
75
+ }
76
+ });
77
+ it('writes exact dashboard bytes and only prints the final path after successful --html', () => {
78
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-html-success-'));
79
+ const target = path_1.default.join(root, 'dashboard.html');
80
+ const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
81
+ const snapshot = (0, types_1.dashboardSnapshot)();
82
+ try {
83
+ expect((0, doctor_1.runDoctor)({ cwd: root, html: target, collectSnapshot: () => snapshot })).toBe(0);
84
+ expect(fs_1.default.readFileSync(target, 'utf8')).toBe((0, render_html_1.renderDashboardHtml)(snapshot));
85
+ expect(stdout.mock.calls.map((call) => String(call[0]))).toEqual([`${target}\n`]);
86
+ }
87
+ finally {
88
+ stdout.mockRestore();
89
+ fs_1.default.rmSync(root, { recursive: true, force: true });
90
+ }
91
+ });
92
+ it('maps healthy full, invalid, and failing HTML modes to 0, 2, and 2', () => {
93
+ const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
94
+ const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
95
+ try {
96
+ expect((0, doctor_1.runDoctor)({ full: true, cwd: '/definitely-not-a-project', collectSnapshot: () => (0, types_1.dashboardSnapshot)() })).toBe(0);
97
+ expect((0, doctor_1.runDoctor)({ html: '' })).toBe(2);
98
+ expect((0, doctor_1.runDoctor)({ html: '/definitely-missing-parent/report.html' })).toBe(2);
99
+ }
100
+ finally {
101
+ stdout.mockRestore();
102
+ stderr.mockRestore();
103
+ }
104
+ });
105
+ it.each([{ full: true }, { html: 'report.html', force: true }])('returns 1 for a degraded dashboard mode', (options) => {
106
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-degraded-'));
107
+ const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
108
+ try {
109
+ expect((0, doctor_1.runDoctor)({ ...options, cwd: root, collectSnapshot: () => (0, types_1.dashboardSnapshot)({ overall: 'degraded' }) })).toBe(1);
110
+ }
111
+ finally {
112
+ stdout.mockRestore();
113
+ fs_1.default.rmSync(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+ it('renders a real read-only machine finding and its remediation in full mode', () => {
117
+ const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-real-dashboard-'));
118
+ const previousHome = process.env.HOME;
119
+ const previousAwmHome = process.env.AWM_HOME;
120
+ const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
121
+ try {
122
+ process.env.HOME = home;
123
+ process.env.AWM_HOME = path_1.default.join(home, '.awm');
124
+ expect((0, doctor_1.runDoctor)({ full: true, cwd: home })).toBe(1);
125
+ const output = stdout.mock.calls.map((call) => String(call[0])).join('');
126
+ expect(output).toContain('machine.preferences.missing');
127
+ expect(output).toContain('awm init');
128
+ expect(fs_1.default.existsSync(path_1.default.join(home, '.awm', 'preferences.json'))).toBe(false);
129
+ }
130
+ finally {
131
+ stdout.mockRestore();
132
+ fs_1.default.rmSync(home, { recursive: true, force: true });
133
+ if (previousHome === undefined)
134
+ delete process.env.HOME;
135
+ else
136
+ process.env.HOME = previousHome;
137
+ if (previousAwmHome === undefined)
138
+ delete process.env.AWM_HOME;
139
+ else
140
+ process.env.AWM_HOME = previousAwmHome;
141
+ }
142
+ });
143
+ it('wires existing read-only project facts into the dashboard without inventing lifecycle observations', () => {
144
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-project-facts-'));
145
+ const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
146
+ try {
147
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
148
+ expect((0, doctor_1.runDoctor)({ full: true, cwd: root })).toBe(1);
149
+ const output = stdout.mock.calls.map((call) => String(call[0])).join('');
150
+ expect(output).toContain('project.profile.missing');
151
+ expect(output).toContain('project.sensors.unavailable');
152
+ expect(output).toContain('unavailable');
153
+ }
154
+ finally {
155
+ stdout.mockRestore();
156
+ fs_1.default.rmSync(root, { recursive: true, force: true });
157
+ }
158
+ });
159
+ it('applies doctor agent selection validation to full dashboard mode', () => {
160
+ const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
161
+ try {
162
+ expect((0, doctor_1.runDoctor)({ full: true, cwd: '/definitely-not-a-project', agent: 'not-a-real-agent' })).toBe(2);
163
+ expect(stderr.mock.calls.map((call) => String(call[0])).join('')).toContain('Invalid agent');
164
+ }
165
+ finally {
166
+ stderr.mockRestore();
167
+ }
168
+ });
169
+ });
10
170
  function report(partial = {}) {
11
171
  return {
12
172
  results: [
@@ -0,0 +1,242 @@
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 sanitize_1 = require("../../../src/core/dashboard/sanitize");
8
+ const store_1 = require("../../../src/core/evidence/store");
9
+ const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const fixedNow = '2026-08-22T00:00:00.000Z';
14
+ describe('collectDashboardSnapshot', () => {
15
+ it('returns only a healthy machine section outside a project', () => {
16
+ const project = jest.fn();
17
+ const plans = jest.fn();
18
+ const execution = jest.fn();
19
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
20
+ cwd: '/definitely-not-a-project', now: fixedNow,
21
+ adapters: { machine: () => ({ findings: [] }), project, plans, execution },
22
+ });
23
+ expect(snapshot.project).toEqual({ detected: false, label: 'No project detected' });
24
+ expect(snapshot.sections.map((section) => section.id)).toEqual(['machine']);
25
+ expect(snapshot.overall).toBe('healthy');
26
+ expect(project).not.toHaveBeenCalled();
27
+ expect(plans).not.toHaveBeenCalled();
28
+ expect(execution).not.toHaveBeenCalled();
29
+ });
30
+ it('fails loudly for malformed central machine findings', () => {
31
+ 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);
32
+ });
33
+ it.each([undefined, null, {}, 'healthy'])('rejects a non-array central machine findings value: %p', (findings) => {
34
+ expect(() => (0, collect_1.collectDashboardSnapshot)({
35
+ cwd: '/definitely-not-a-project', now: fixedNow,
36
+ adapters: { machine: () => ({ findings }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() },
37
+ })).toThrow('Dashboard findings must be an array');
38
+ });
39
+ it('uses exact verified remediation commands and stable ordered sections', () => {
40
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
41
+ cwd: process.cwd(), now: fixedNow,
42
+ adapters: {
43
+ machine: () => ({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' }] }),
44
+ project: () => ({ label: 'Demo', findings: [{ id: 'project.profile.missing', label: 'Profile', state: 'missing' }] }),
45
+ plans: () => Array.from({ length: 2000 }, (_, index) => ({ id: `plan.${index}`, label: `Plan ${index}`, state: 'ok' })),
46
+ execution: () => ({}),
47
+ },
48
+ });
49
+ expect(snapshot.sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
50
+ expect(snapshot.sections.find((section) => section.id === 'machine')?.items[0].remediation).toBe('awm init');
51
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toHaveLength(2000);
52
+ expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(0);
53
+ expect(JSON.stringify(snapshot)).not.toMatch(/score|ranking/i);
54
+ });
55
+ it('isolates optional adapter failures and omits unverified remediation', () => {
56
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
57
+ cwd: process.cwd(), now: fixedNow,
58
+ adapters: {
59
+ machine: () => ({ findings: [{ id: 'unknown', label: 'Unknown', state: 'missing' }] }),
60
+ project: () => { throw new Error('corrupt project source'); },
61
+ plans: () => [], execution: () => undefined,
62
+ },
63
+ });
64
+ expect(snapshot.sections.find((section) => section.id === 'machine')?.items).toEqual([]);
65
+ expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
66
+ expect(snapshot.sections.find((section) => section.id === 'project')?.items).toEqual([]);
67
+ });
68
+ it('marks execution-derived sections unavailable when no read-only execution source exists', () => {
69
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
70
+ cwd: process.cwd(), now: fixedNow,
71
+ adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined },
72
+ });
73
+ for (const id of ['execution', 'qa', 'retro']) {
74
+ expect(snapshot.sections.find((section) => section.id === id)?.availability).toBe('unavailable');
75
+ }
76
+ });
77
+ it('isolates malformed optional source data to its owning section', () => {
78
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
79
+ cwd: process.cwd(), now: fixedNow,
80
+ adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [{ id: 'bad', label: 'Profile', state: 'invented' }] }), plans: () => [], execution: () => undefined },
81
+ });
82
+ expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
83
+ expect(snapshot.sections.find((section) => section.id === 'machine')?.availability).toBe('available');
84
+ });
85
+ it('isolates malformed post-sanitization plan findings', () => {
86
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: '', label: 'Profile', state: 'ok' }], execution: () => undefined } });
87
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('unavailable');
88
+ });
89
+ it('isolates malformed project findings without dropping other sections', () => {
90
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [{ id: '', label: 'Profile', state: 'ok' }] }), plans: () => [], execution: () => undefined } });
91
+ expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
92
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('available');
93
+ });
94
+ it.each(['execution', 'qa', 'retro'])('isolates malformed %s findings', (key) => {
95
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }) } });
96
+ expect(snapshot.sections.find((section) => section.id === key)?.availability).toBe('unavailable');
97
+ });
98
+ it('renders exact remediation only for a canonical optional source failure', () => {
99
+ const knownFailure = Object.assign(new Error('sensors unavailable'), { findingId: 'project.sensors.unavailable', remediationVerified: true });
100
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
101
+ cwd: process.cwd(), now: fixedNow,
102
+ adapters: { machine: () => ({ findings: [] }), project: () => { throw knownFailure; }, plans: () => [], execution: () => undefined },
103
+ });
104
+ expect(snapshot.sections.find((section) => section.id === 'project')?.items).toEqual([
105
+ expect.objectContaining({ id: 'project.sensors.unavailable', state: 'unavailable', remediation: 'awm sensors status' }),
106
+ ]);
107
+ });
108
+ it.each([
109
+ ['plans', 'planning.source.unavailable', 'planning', 'awm preflight'],
110
+ ['execution', 'execution.source.unavailable', 'execution', 'awm sensors status'],
111
+ ])('renders a known %s failure in its owner section', (adapter, findingId, sectionId, remediation) => {
112
+ const failure = Object.assign(new Error('unavailable'), { findingId, remediationVerified: true });
113
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
114
+ cwd: process.cwd(), now: fixedNow,
115
+ adapters: {
116
+ machine: () => ({ findings: [] }), project: () => ({ findings: [] }),
117
+ plans: () => { if (adapter === 'plans')
118
+ throw failure; return []; },
119
+ execution: () => { if (adapter === 'execution')
120
+ throw failure; return undefined; },
121
+ },
122
+ });
123
+ expect(snapshot.sections.find((section) => section.id === sectionId)?.items[0]).toEqual(expect.objectContaining({ id: findingId, remediation }));
124
+ });
125
+ it('sorts findings by stable canonical id', () => {
126
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
127
+ cwd: '/definitely-not-a-project', now: fixedNow,
128
+ adapters: { machine: () => ({ findings: [
129
+ { id: 'machine.registries.stale', label: 'Registries', state: 'attention' },
130
+ { id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' },
131
+ ] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() },
132
+ });
133
+ expect(snapshot.sections[0].items.map((item) => item.id)).toEqual(['machine.preferences.missing', 'machine.registries.stale']);
134
+ });
135
+ it.each(['blocked', 'active', 'executed', 'retro_pending', 'qa_pending', 'legacy_unverifiable'])('integrates lifecycle state %s into plan detail', (expected) => {
136
+ const lifecycle = expected === 'blocked' ? { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
137
+ : expected === 'active' ? { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
138
+ : expected === 'executed' ? { markers: { qaComplete: true, retroComplete: true }, tasks: { total: 1, completed: 1 } }
139
+ : expected === 'retro_pending' ? { markers: { qaComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } }
140
+ : expected === 'qa_pending' ? { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 1 } }
141
+ : { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
142
+ 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 } });
143
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.items[0].detail).toBe(expected);
144
+ });
145
+ it('degrades a machine-only dashboard for actionable machine findings', () => {
146
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
147
+ cwd: '/definitely-not-a-project', now: fixedNow,
148
+ adapters: { machine: () => ({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' }] }), project: jest.fn(), plans: jest.fn(), execution: jest.fn() },
149
+ });
150
+ expect(snapshot.sections.map((section) => section.id)).toEqual(['machine']);
151
+ expect(snapshot.overall).toBe('degraded');
152
+ });
153
+ it('loads validated local evidence only after a project is detected and renders every cycle fact', () => {
154
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
155
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
156
+ (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
157
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
158
+ const history = snapshot.sections.find((section) => section.id === 'history');
159
+ expect(snapshot.confidence).toBe('provisional');
160
+ expect(history?.availability).toBe('available');
161
+ expect(history?.items[0]?.detail).toContain('retries 1; QA 1/1; first-pass yes; cures awaiting_observation');
162
+ fs_1.default.rmSync(root, { recursive: true, force: true });
163
+ });
164
+ it.each(['.awm', 'evidence', 'cycles'])('does not follow a symlinked %s evidence ancestor', (ancestor) => {
165
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
166
+ const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
167
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
168
+ const externalCycles = ancestor === '.awm' ? path_1.default.join(external, 'evidence', 'cycles') : path_1.default.join(external, 'cycles');
169
+ fs_1.default.mkdirSync(externalCycles, { recursive: true });
170
+ fs_1.default.writeFileSync(path_1.default.join(externalCycles, `${'a'.repeat(64)}.json`), JSON.stringify((0, evidence_fixtures_1.cycleEvidenceFixture)()));
171
+ if (ancestor === '.awm')
172
+ fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm'));
173
+ else if (ancestor === 'evidence') {
174
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
175
+ fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence'));
176
+ }
177
+ else {
178
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm', 'evidence'), { recursive: true });
179
+ fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence', 'cycles'));
180
+ }
181
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
182
+ const history = snapshot.sections.find((section) => section.id === 'history');
183
+ expect(history).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
184
+ fs_1.default.rmSync(root, { recursive: true, force: true });
185
+ fs_1.default.rmSync(external, { recursive: true, force: true });
186
+ });
187
+ it('does not follow a symlinked evidence file', () => {
188
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
189
+ const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
190
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
191
+ const cycleId = 'a'.repeat(64);
192
+ const externalFile = path_1.default.join(external, `${cycleId}.json`);
193
+ fs_1.default.writeFileSync(externalFile, JSON.stringify((0, evidence_fixtures_1.cycleEvidenceFixture)()));
194
+ const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
195
+ fs_1.default.mkdirSync(directory, { recursive: true });
196
+ fs_1.default.symlinkSync(externalFile, path_1.default.join(directory, `${cycleId}.json`));
197
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
198
+ expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
199
+ fs_1.default.rmSync(root, { recursive: true, force: true });
200
+ fs_1.default.rmSync(external, { recursive: true, force: true });
201
+ });
202
+ it.each(['duplicate', 'mismatch'])('does not accept %s evidence filenames', (caseName) => {
203
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
204
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
205
+ (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
206
+ const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
207
+ const record = (0, evidence_fixtures_1.cycleEvidenceFixture)();
208
+ const filename = caseName === 'duplicate' ? `${'b'.repeat(64)}.json` : `${'b'.repeat(64)}.json`;
209
+ fs_1.default.writeFileSync(path_1.default.join(directory, filename), JSON.stringify(caseName === 'duplicate' ? record : { ...record, cycleId: 'c'.repeat(64) }));
210
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
211
+ expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
212
+ fs_1.default.rmSync(root, { recursive: true, force: true });
213
+ });
214
+ });
215
+ describe('sanitizeDashboardSource', () => {
216
+ it('removes hostile paths and secrets before rendering', () => {
217
+ const safe = (0, sanitize_1.sanitizeDashboardSource)({ path: '/var/lib/private', token: 'ghp_secret', username: 'alice', output: '<script>alert(1)</script>', rawOutput: 'sk-live-secret', detail: 'log:(cwd=/tmp/run/output) unc=(\\\\server\\share\\secret)(MODE=production)', label: 'alice workstation' });
218
+ expect(JSON.stringify(safe)).not.toMatch(/alice|ghp_|script|\/var\/|\/tmp\/|sk-live|alert|server|share|MODE=production/i);
219
+ });
220
+ it('replaces dynamic finding identifiers with opaque safe identifiers', () => {
221
+ const safe = (0, sanitize_1.sanitizeDashboardSource)({ findings: [
222
+ { id: 'alice@example.com', label: 'Preferences', state: 'missing' },
223
+ { id: '192.0.2.44/repository-private', label: 'Profile', state: 'missing' },
224
+ { id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' },
225
+ ] });
226
+ const serialized = JSON.stringify(safe);
227
+ expect(serialized).not.toMatch(/alice@example|192\.0\.2\.44|repository-private/i);
228
+ expect(serialized).toContain('machine.preferences.missing');
229
+ expect(serialized).toMatch(/item-[a-f0-9]{16}/);
230
+ });
231
+ it('rejects invalid item states explicitly', () => {
232
+ expect(() => (0, sanitize_1.sanitizeDashboardSource)({ state: 'invented' })).toThrow(/state/i);
233
+ });
234
+ it('omits raw dynamic command and error details', () => {
235
+ const safe = (0, sanitize_1.sanitizeDashboardSource)({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing', detail: 'Error: ghp_secret at /tmp/private; TOKEN=value' }] });
236
+ expect(JSON.stringify(safe)).not.toMatch(/Error|ghp_|\/tmp|TOKEN=/i);
237
+ expect(JSON.stringify(safe)).not.toContain('detail');
238
+ });
239
+ });
240
+ test('exports canonical remediation commands', () => {
241
+ expect(collect_1.REMEDIATION_BY_FINDING_ID['machine.preferences.missing']).toBe('awm init');
242
+ });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const validate_1 = require("../../../src/core/dashboard/validate");
4
+ const types_1 = require("../../../src/core/dashboard/types");
5
+ function snapshot() {
6
+ return {
7
+ schema: 1,
8
+ generatedAt: '2026-08-22T00:00:00.000Z',
9
+ overall: 'healthy',
10
+ project: { detected: true, label: 'Demo project' },
11
+ confidence: 'none',
12
+ sections: [
13
+ { id: 'machine', availability: 'available', items: [{ id: 'cli', label: 'CLI', state: 'ok' }] },
14
+ { id: 'project', availability: 'not_applicable', items: [] },
15
+ { id: 'planning', availability: 'not_applicable', items: [] },
16
+ { id: 'execution', availability: 'not_applicable', items: [] },
17
+ { id: 'qa', availability: 'not_applicable', items: [] },
18
+ { id: 'retro', availability: 'not_applicable', items: [] },
19
+ { id: 'history', availability: 'not_applicable', items: [] },
20
+ ],
21
+ };
22
+ }
23
+ describe('validateDashboardSnapshotV1', () => {
24
+ it('creates the safe deterministic dashboard snapshot default', () => {
25
+ expect((0, types_1.dashboardSnapshot)()).toEqual({
26
+ schema: 1,
27
+ generatedAt: '2026-08-22T00:00:00.000Z',
28
+ overall: 'healthy',
29
+ project: { detected: false, label: 'No project detected' },
30
+ confidence: 'none',
31
+ sections: [{ id: 'machine', availability: 'available', items: [] }],
32
+ });
33
+ });
34
+ it('allows callers to override snapshot defaults', () => {
35
+ expect((0, types_1.dashboardSnapshot)({ overall: 'degraded' }).overall).toBe('degraded');
36
+ });
37
+ it('builds a complete canonical section set when callers select a project', () => {
38
+ const value = (0, types_1.dashboardSnapshot)({ project: { detected: true, label: 'Demo project' }, confidence: 'provisional' });
39
+ expect((0, validate_1.validateDashboardSnapshotV1)(value).sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
40
+ });
41
+ it('returns a valid V1 snapshot', () => {
42
+ expect((0, validate_1.validateDashboardSnapshotV1)(snapshot())).toEqual(snapshot());
43
+ });
44
+ it.each(['attention', 'missing', 'unavailable'])('rejects actionable %s without remediation', (state) => {
45
+ const value = snapshot();
46
+ value.sections[0].items[0].state = state;
47
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
48
+ });
49
+ it('rejects project before machine', () => {
50
+ const value = snapshot();
51
+ value.sections.reverse();
52
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(/section order/i);
53
+ });
54
+ it('rejects an invalid schema version', () => {
55
+ const value = snapshot();
56
+ value.schema = 2;
57
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
58
+ });
59
+ it('rejects invalid public enums', () => {
60
+ const value = snapshot();
61
+ value.overall = 'unknown';
62
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
63
+ });
64
+ it('rejects duplicate item ids', () => {
65
+ const value = snapshot();
66
+ value.sections[0].items.push({ id: 'cli', label: 'Duplicate', state: 'not_applicable' });
67
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
68
+ });
69
+ it('rejects remediation on not applicable items', () => {
70
+ const value = snapshot();
71
+ value.sections[0].items[0] = { id: 'cli', label: 'CLI', state: 'not_applicable', remediation: 'remove' };
72
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
73
+ });
74
+ it('rejects lifecycle sections when no project is detected', () => {
75
+ const value = (0, types_1.dashboardSnapshot)({
76
+ project: { detected: false, label: 'No project detected' },
77
+ sections: [
78
+ { id: 'machine', availability: 'available', items: [] },
79
+ { id: 'planning', availability: 'not_applicable', items: [] },
80
+ ],
81
+ });
82
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(/no project.*machine/i);
83
+ });
84
+ it('rejects a detected project without the complete canonical lifecycle sections', () => {
85
+ const value = (0, types_1.dashboardSnapshot)({
86
+ project: { detected: true, label: 'Demo project' },
87
+ confidence: 'provisional',
88
+ sections: [{ id: 'machine', availability: 'available', items: [] }],
89
+ });
90
+ expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(/detected project.*canonical lifecycle/i);
91
+ });
92
+ });
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const plan_state_1 = require("../../../src/core/dashboard/plan-state");
4
+ const base = { journal: undefined, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 2, completed: 0 } };
5
+ describe('classifyPlanState', () => {
6
+ it('gives blocked journal state highest precedence', () => {
7
+ expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'blocked' }, markers: { qaComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('blocked');
8
+ });
9
+ it('gives active journal state precedence over completed markers', () => {
10
+ expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'active' }, markers: { qaComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('active');
11
+ });
12
+ it.each([
13
+ [{ ...base, markers: { qaComplete: true, retroComplete: true } }, 'executed'],
14
+ [{ ...base, markers: { qaComplete: true, retroComplete: false } }, 'retro_pending'],
15
+ [{ ...base, tasks: { total: 2, completed: 2 } }, 'qa_pending'],
16
+ [base, 'legacy_unverifiable'],
17
+ ])('classifies lifecycle state %s', (input, expected) => {
18
+ expect((0, plan_state_1.classifyPlanState)(input)).toBe(expected);
19
+ });
20
+ it.each([
21
+ [null, /input/i],
22
+ [{ markers: null, tasks: base.tasks }, /markers/i],
23
+ [{ markers: base.markers, tasks: null }, /task/i],
24
+ [{ ...base, journal: null }, /journal/i],
25
+ [{ ...base, journal: { state: 'invented' } }, /journal/i],
26
+ [{ ...base, markers: { qaComplete: 'yes', retroComplete: false } }, /markers/i],
27
+ [{ ...base, tasks: { total: 1, completed: 2 } }, /counts/i],
28
+ [{ ...base, tasks: { total: Number.NaN, completed: 0 } }, /counts/i],
29
+ ])('rejects invalid public input %p', (input, error) => {
30
+ expect(() => (0, plan_state_1.classifyPlanState)(input)).toThrow(error);
31
+ });
32
+ });