agentic-workflow-manager 8.2.1 → 8.4.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/doctor.js +40 -1
- package/dist/src/commands/hooks/claude.js +47 -31
- package/dist/src/commands/hooks/index.js +1 -1
- package/dist/src/commands/hooks/shared.js +1 -1
- package/dist/src/commands/registry/add.js +6 -1
- package/dist/src/commands/registry/index.js +3 -0
- package/dist/src/core/context/orchestrator.js +15 -2
- package/dist/src/core/context/provider.js +29 -1
- package/dist/src/core/context/regenerate.js +29 -0
- package/dist/src/core/dashboard/collect.js +170 -0
- package/dist/src/core/dashboard/plan-state.js +44 -0
- package/dist/src/core/dashboard/render-html.js +82 -0
- package/dist/src/core/dashboard/render-terminal.js +43 -0
- package/dist/src/core/dashboard/sanitize.js +62 -0
- package/dist/src/core/dashboard/styles.js +51 -0
- package/dist/src/core/dashboard/types.js +21 -0
- package/dist/src/core/dashboard/validate.js +106 -0
- package/dist/src/core/dashboard/write-html.js +88 -0
- package/dist/src/core/diagnostics/context.js +1 -1
- package/dist/src/core/orchestrators.js +142 -0
- package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
- package/dist/tests/commands/doctor.test.js +160 -0
- package/dist/tests/commands/hooks/install-symlink-fallback.test.js +13 -4
- package/dist/tests/commands/hooks/install.test.js +121 -3
- package/dist/tests/commands/hooks/resync.test.js +40 -2
- package/dist/tests/commands/registry/add.test.js +104 -0
- package/dist/tests/core/context/orchestrator.test.js +86 -0
- package/dist/tests/core/context/provider.test.js +137 -0
- package/dist/tests/core/context/regenerate.test.js +56 -0
- package/dist/tests/core/dashboard/collect.test.js +173 -0
- package/dist/tests/core/dashboard/contracts.test.js +92 -0
- package/dist/tests/core/dashboard/plan-state.test.js +32 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
- package/dist/tests/core/dashboard/render-html.test.js +175 -0
- package/dist/tests/core/dashboard/render-terminal.test.js +65 -0
- package/dist/tests/core/dashboard/write-html.test.js +112 -0
- package/dist/tests/core/orchestrators.test.js +236 -0
- package/dist/tests/helpers/dashboard-fixtures.js +66 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const collect_1 = require("../../../src/core/dashboard/collect");
|
|
4
|
+
function context() {
|
|
5
|
+
return {
|
|
6
|
+
machine: {},
|
|
7
|
+
project: {
|
|
8
|
+
root: '/private/project',
|
|
9
|
+
profile: { present: true, extensions: ['delivery'], registries: { base: '1.2.3' } },
|
|
10
|
+
activeBundles: { expected: ['delivery'], linked: ['delivery'], broken: [] },
|
|
11
|
+
orphanLinks: { repairable: [], dead: [], usurped: [] },
|
|
12
|
+
sensors: { present: true }, constitution: { present: true }, context: { present: true, file: 'AGENTS.md' },
|
|
13
|
+
},
|
|
14
|
+
providers: [{
|
|
15
|
+
id: 'codex', label: 'Codex', tier: 'hooks-native', checks: [
|
|
16
|
+
{ id: 'binary.version', state: 'supported', target: '0.145.0' },
|
|
17
|
+
{ id: 'hook.trust', state: 'pending-trust', remediationCode: 'awm hooks trust --agent codex' },
|
|
18
|
+
],
|
|
19
|
+
}],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe('productionDashboardAdapters', () => {
|
|
23
|
+
it('maps existing diagnostics into canonical, share-safe machine and project facts', () => {
|
|
24
|
+
const adapters = (0, collect_1.productionDashboardAdapters)(context());
|
|
25
|
+
const machine = adapters.machine({ cwd: '/private/project' });
|
|
26
|
+
const project = adapters.project({ root: '/private/project' });
|
|
27
|
+
expect(machine.findings).toEqual(expect.arrayContaining([
|
|
28
|
+
expect.objectContaining({ id: 'machine.provider.codex.binary.version', state: 'ok' }),
|
|
29
|
+
expect.objectContaining({ id: 'machine.provider.codex.hook.trust', state: 'attention' }),
|
|
30
|
+
]));
|
|
31
|
+
expect(project.findings).toEqual(expect.arrayContaining([
|
|
32
|
+
expect.objectContaining({ id: 'project.profile.present', state: 'ok' }),
|
|
33
|
+
expect.objectContaining({ id: 'project.extensions.configured', state: 'ok' }),
|
|
34
|
+
expect.objectContaining({ id: 'project.registry-pins.present', state: 'ok' }),
|
|
35
|
+
expect.objectContaining({ id: 'project.context.present', state: 'ok' }),
|
|
36
|
+
expect.objectContaining({ id: 'project.constitution.present', state: 'ok' }),
|
|
37
|
+
expect.objectContaining({ id: 'project.sensors.present', state: 'ok' }),
|
|
38
|
+
]));
|
|
39
|
+
expect(JSON.stringify({ machine, project })).not.toMatch(/private|0\.145\.0|hooks trust/i);
|
|
40
|
+
});
|
|
41
|
+
it('reports only source-verified remediation and leaves unavailable lifecycle sources explicit', () => {
|
|
42
|
+
const adapters = (0, collect_1.productionDashboardAdapters)({ ...context(), project: null });
|
|
43
|
+
expect(adapters.machine({ cwd: '/tmp' }).findings).toEqual(expect.arrayContaining([
|
|
44
|
+
expect.objectContaining({ id: 'machine.provider.codex.hook.trust', remediationVerified: false }),
|
|
45
|
+
]));
|
|
46
|
+
expect(adapters.execution({ root: '/tmp' })).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
it('contains no diagnostic prose or raw source details', () => {
|
|
49
|
+
const facts = context();
|
|
50
|
+
facts.providers[0].checks[1].detail = 'token=secret /private/path <script>';
|
|
51
|
+
const serialized = JSON.stringify((0, collect_1.productionDashboardAdapters)(facts).machine({ cwd: '/private/project' }));
|
|
52
|
+
expect(serialized).not.toMatch(/token|secret|private|script/i);
|
|
53
|
+
});
|
|
54
|
+
it('retains stable provider ids after collection and maps a verified bundle remedy', () => {
|
|
55
|
+
const facts = context();
|
|
56
|
+
facts.project.activeBundles.broken = ['delivery'];
|
|
57
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
58
|
+
cwd: process.cwd(), now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(facts),
|
|
59
|
+
});
|
|
60
|
+
const machine = snapshot.sections.find((section) => section.id === 'machine');
|
|
61
|
+
const project = snapshot.sections.find((section) => section.id === 'project');
|
|
62
|
+
expect(machine.items).toEqual(expect.arrayContaining([
|
|
63
|
+
expect.objectContaining({ id: 'machine.provider.codex.binary.version', state: 'ok' }),
|
|
64
|
+
]));
|
|
65
|
+
expect(project.items).toEqual(expect.arrayContaining([
|
|
66
|
+
expect.objectContaining({ id: 'project.bundles.coherent', remediation: 'awm sync' }),
|
|
67
|
+
]));
|
|
68
|
+
expect(JSON.stringify(snapshot)).not.toMatch(/remediationVerified|private|0\.145\.0|token/i);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const render_html_1 = require("../../../src/core/dashboard/render-html");
|
|
4
|
+
const types_1 = require("../../../src/core/dashboard/types");
|
|
5
|
+
const validate_1 = require("../../../src/core/dashboard/validate");
|
|
6
|
+
function snapshot(overrides = {}) {
|
|
7
|
+
return (0, types_1.dashboardSnapshot)({
|
|
8
|
+
project: { detected: true, label: 'doctor-dashboard' },
|
|
9
|
+
confidence: 'observing',
|
|
10
|
+
sections: [
|
|
11
|
+
{ id: 'machine', availability: 'available', items: [{ id: 'machine.cli', label: 'CLI', state: 'ok', detail: 'v8.1.6' }] },
|
|
12
|
+
{ id: 'project', availability: 'available', items: [{ id: 'project.context', label: 'Context', state: 'attention', remediation: 'awm sync' }] },
|
|
13
|
+
{ id: 'planning', availability: 'available', items: [] },
|
|
14
|
+
{ id: 'execution', availability: 'available', items: [] },
|
|
15
|
+
{ id: 'qa', availability: 'available', items: [] },
|
|
16
|
+
{ id: 'retro', availability: 'available', items: [] },
|
|
17
|
+
{ id: 'history', availability: 'available', items: [] },
|
|
18
|
+
],
|
|
19
|
+
...overrides,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
describe('renderDashboardHtml', () => {
|
|
23
|
+
it('emits a self-contained scriptless document with the exact restrictive CSP and escaped dynamic values', () => {
|
|
24
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
|
|
25
|
+
project: { detected: true, label: '<img src=x onerror=alert(1)>' },
|
|
26
|
+
sections: [
|
|
27
|
+
{ id: 'machine', availability: 'available', items: [{ id: 'hostile', label: '<script>alert(1)</script>', state: 'attention', detail: '"quoted"', remediation: 'awm sync && echo <unsafe>' }] },
|
|
28
|
+
{ id: 'project', availability: 'not_applicable', items: [] }, { id: 'planning', availability: 'not_applicable', items: [] },
|
|
29
|
+
{ id: 'execution', availability: 'not_applicable', items: [] }, { id: 'qa', availability: 'not_applicable', items: [] },
|
|
30
|
+
{ id: 'retro', availability: 'not_applicable', items: [] }, { id: 'history', availability: 'not_applicable', items: [] },
|
|
31
|
+
],
|
|
32
|
+
})));
|
|
33
|
+
expect(html).toContain(`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">`);
|
|
34
|
+
expect(html).toContain('<script>alert(1)</script>');
|
|
35
|
+
expect(html).toContain('<img src=x onerror=alert(1)>');
|
|
36
|
+
expect(html).toContain('awm sync && echo <unsafe>');
|
|
37
|
+
expect(html).not.toMatch(/<script|https?:\/\//i);
|
|
38
|
+
});
|
|
39
|
+
it('uses semantic landmarks and ordered sections with text state, focus, print, responsive, and reduced-motion support', () => {
|
|
40
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot()));
|
|
41
|
+
expect(html).toMatch(/<header[\s>]/);
|
|
42
|
+
expect(html).toContain('data-operational-sidebar aria-label="Dashboard sections"');
|
|
43
|
+
expect(html).toMatch(/<main[\s>]/);
|
|
44
|
+
expect(html).toMatch(/<footer[\s>]/);
|
|
45
|
+
for (const heading of ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Retro', 'Final / history'])
|
|
46
|
+
expect(html).toContain(`<h2>${heading}</h2>`);
|
|
47
|
+
expect(html).toContain('<table>');
|
|
48
|
+
expect(html).toContain('<th scope="col">State</th>');
|
|
49
|
+
expect(html).not.toMatch(/<span class="state [^"]+" aria-label=/);
|
|
50
|
+
expect(html).toContain(':focus-visible');
|
|
51
|
+
expect(html).toContain('@media print');
|
|
52
|
+
expect(html).toContain('@media (max-width: 720px)');
|
|
53
|
+
expect(html).toContain('@media (prefers-reduced-motion: reduce)');
|
|
54
|
+
expect(html).not.toMatch(/score|ranking/i);
|
|
55
|
+
});
|
|
56
|
+
it('matches the machine-only diagnostic composition with cards, privacy boundary, and prioritized remedies', () => {
|
|
57
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
|
|
58
|
+
project: { detected: false, label: 'No project detected' },
|
|
59
|
+
sections: [{ id: 'machine', availability: 'available', items: [
|
|
60
|
+
{ id: 'machine.cli', label: 'Installation', state: 'ok', detail: 'v8.1.6' },
|
|
61
|
+
{ id: 'machine.sensors', label: 'Sensors', state: 'attention', remediation: 'awm sensors run' },
|
|
62
|
+
{ id: 'machine.permissions', label: 'Permissions', state: 'missing', remediation: 'awm init' },
|
|
63
|
+
] }],
|
|
64
|
+
})));
|
|
65
|
+
expect(html).toContain('data-machine-bento');
|
|
66
|
+
expect(html.match(/class="bento-card"/g)).toHaveLength(3);
|
|
67
|
+
expect(html).toContain('<h2>Instalación</h2>');
|
|
68
|
+
expect(html).not.toContain('<article class="bento-card"><h3>Instalación</h3>');
|
|
69
|
+
expect(html).toContain('Privacy & security');
|
|
70
|
+
expect(html).toContain('data-static-privacy-toggle');
|
|
71
|
+
expect(html).toContain('type="checkbox" checked disabled');
|
|
72
|
+
expect(html).toContain('Siguiente acción requerida');
|
|
73
|
+
expect(html).toContain('Copy command (static)');
|
|
74
|
+
expect(html).toContain('data-machine-bento');
|
|
75
|
+
expect(html).toContain('data-next-actions');
|
|
76
|
+
expect(html.match(/data-next-action=/g)).toHaveLength(3);
|
|
77
|
+
expect(html).toContain('Inicializar proyecto');
|
|
78
|
+
expect(html).toContain('<code>awm init</code>');
|
|
79
|
+
expect(html.indexOf('awm sensors run')).toBeLessThan(html.indexOf('awm init'));
|
|
80
|
+
});
|
|
81
|
+
it('uses neutral machine configuration semantics when no project is detected', () => {
|
|
82
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
|
|
83
|
+
project: { detected: false, label: 'No project detected' },
|
|
84
|
+
sections: [{ id: 'machine', availability: 'available', items: [] }],
|
|
85
|
+
})));
|
|
86
|
+
expect(html).toContain('<h1>Machine configuration</h1>');
|
|
87
|
+
expect(html).toContain('Machine readiness and safe configuration state outside a project.');
|
|
88
|
+
expect(html).toContain('aria-label="Machine configuration sections"');
|
|
89
|
+
expect(html).not.toContain('Project lifecycle');
|
|
90
|
+
expect(html).not.toContain('Dashboard sections');
|
|
91
|
+
});
|
|
92
|
+
it('matches the project lifecycle composition with machine preparation, timeline, provisional evidence, plans, and history', () => {
|
|
93
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({ confidence: 'provisional' })));
|
|
94
|
+
expect(html).toContain('class="machine-preparation-strip" data-machine-preparation');
|
|
95
|
+
expect(html).toContain('data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"');
|
|
96
|
+
expect(html).toContain('class="lifecycle-timeline" data-lifecycle-timeline');
|
|
97
|
+
expect(html).toContain('data-lifecycle-timeline aria-labelledby="lifecycle-timeline-heading"');
|
|
98
|
+
expect(html).toContain('class="timeline connected-timeline"');
|
|
99
|
+
expect(html).toContain('<span aria-hidden="true" class="timeline-marker"></span>');
|
|
100
|
+
for (const stage of ['Planning', 'Execution', 'QA', 'Retro', 'Evidence'])
|
|
101
|
+
expect(html).toContain(`data-stage="${stage.toLowerCase()}"`);
|
|
102
|
+
expect(html).toContain('Provisional evidence');
|
|
103
|
+
expect(html).toContain('data-project-evidence role="group" aria-labelledby="project-evidence-heading"');
|
|
104
|
+
expect(html).toContain('Planes de trabajo');
|
|
105
|
+
expect(html).toContain('Impacto y trazabilidad');
|
|
106
|
+
expect(html).toContain('data-project-header-actions role="group" aria-label="Project actions"');
|
|
107
|
+
expect(html).toContain('data-project-breadcrumb');
|
|
108
|
+
expect(html).toContain('data-project-nav');
|
|
109
|
+
expect(html).toContain('data-machine-preparation-card="installation"');
|
|
110
|
+
expect(html).toContain('data-machine-preparation-card="sensors"');
|
|
111
|
+
expect(html).toContain('data-machine-preparation-card="persistence"');
|
|
112
|
+
expect(html).toContain('data-plan-card="active"');
|
|
113
|
+
expect(html).toContain('data-plan-card="blocked"');
|
|
114
|
+
expect(html).toContain('Progreso: sin observación');
|
|
115
|
+
expect(html).toContain('Ver plan (estático)');
|
|
116
|
+
expect(html).toContain('<th scope="col">Tipo</th>');
|
|
117
|
+
expect(html).toContain('<th scope="col">Fuente</th>');
|
|
118
|
+
expect(html).toContain('<th scope="col">Estado</th>');
|
|
119
|
+
expect(html).toContain('<th scope="col">Fecha</th>');
|
|
120
|
+
expect(html).toContain('No hay evidencia de impacto disponible en este snapshot.');
|
|
121
|
+
expect(html).toContain('data-closure-actions role="group" aria-labelledby="closure-actions-heading"');
|
|
122
|
+
});
|
|
123
|
+
it('uses the compact approved desktop composition rather than stretched generic cards', () => {
|
|
124
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({ confidence: 'provisional' })));
|
|
125
|
+
expect(html).toContain('<div class="dashboard-content">');
|
|
126
|
+
expect(html).toContain('width:min(100%,72rem)');
|
|
127
|
+
expect(html).toContain('.machine-preparation-strip {');
|
|
128
|
+
expect(html).toContain('.connected-timeline { border-top:1px solid var(--border);');
|
|
129
|
+
expect(html).toContain('.timeline-marker {');
|
|
130
|
+
expect(html).not.toContain('.timeline li::before');
|
|
131
|
+
expect(html).not.toContain('.connected-timeline::before');
|
|
132
|
+
expect(html).toContain('.timeline .state.unavailable { background:#38141c; color:#fff0ed;');
|
|
133
|
+
expect(html).toContain('.evidence-grid.compact-composition');
|
|
134
|
+
expect(html).toContain('font:14px/1.35');
|
|
135
|
+
});
|
|
136
|
+
it('includes the artifact-aligned, noninteractive machine toolbar without weakening static CSP', () => {
|
|
137
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
|
|
138
|
+
project: { detected: false, label: 'No project detected' },
|
|
139
|
+
sections: [{ id: 'machine', availability: 'available', items: [] }],
|
|
140
|
+
})));
|
|
141
|
+
expect(html).toContain('data-dashboard-toolbar');
|
|
142
|
+
expect(html).toContain('data-operational-sidebar');
|
|
143
|
+
for (const label of ['Inicio', 'Estado', 'Configuración', 'Terminal'])
|
|
144
|
+
expect(html).toContain(`>${label}<`);
|
|
145
|
+
expect(html).toContain('data-sidebar-diagnose');
|
|
146
|
+
expect(html).toContain('data-sidebar-operator-controls');
|
|
147
|
+
expect(html).toContain('aria-label="Search dashboard"');
|
|
148
|
+
expect(html).toContain('placeholder="Search resources" disabled');
|
|
149
|
+
for (const label of ['Notifications (static)', 'Help (static)', 'Export dashboard (static)', 'New deployment (static)']) {
|
|
150
|
+
expect(html).toContain(label);
|
|
151
|
+
}
|
|
152
|
+
expect(html).toContain("script-src 'none'");
|
|
153
|
+
});
|
|
154
|
+
it('renders every canonical project section once and in lifecycle order', () => {
|
|
155
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({ confidence: 'provisional' })));
|
|
156
|
+
const ids = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
|
|
157
|
+
const positions = ids.map((id) => html.indexOf(`<section id="${id}"`));
|
|
158
|
+
expect(positions.every((position) => position >= 0)).toBe(true);
|
|
159
|
+
expect([...positions].sort((left, right) => left - right)).toEqual(positions);
|
|
160
|
+
for (const id of ids)
|
|
161
|
+
expect(html.match(new RegExp(`<section id="${id}"`, 'g'))).toHaveLength(1);
|
|
162
|
+
expect(html.match(/<section[\s>]/g)).toHaveLength(ids.length);
|
|
163
|
+
});
|
|
164
|
+
it('is deterministic and preserves every history and task observation without privacy leakage', () => {
|
|
165
|
+
const sections = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'].map((id) => ({
|
|
166
|
+
id: id, availability: 'available',
|
|
167
|
+
items: id === 'history' ? Array.from({ length: 500 }, (_, index) => ({ id: `history.${index}`, label: `Cycle ${index}`, state: 'ok' })) : id === 'execution' ? Array.from({ length: 2000 }, (_, index) => ({ id: `execution.${index}`, label: `Task ${index}`, state: 'ok' })) : [],
|
|
168
|
+
}));
|
|
169
|
+
const value = (0, validate_1.validateDashboardSnapshotV1)(snapshot({ sections }));
|
|
170
|
+
const first = (0, render_html_1.renderDashboardHtml)(value);
|
|
171
|
+
expect((0, render_html_1.renderDashboardHtml)(value)).toBe(first);
|
|
172
|
+
expect(first).toContain('Cycle 499');
|
|
173
|
+
expect(first).toContain('Task 1999');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const render_terminal_1 = require("../../../src/core/dashboard/render-terminal");
|
|
4
|
+
const types_1 = require("../../../src/core/dashboard/types");
|
|
5
|
+
const validate_1 = require("../../../src/core/dashboard/validate");
|
|
6
|
+
function completeSnapshot(overrides = {}) {
|
|
7
|
+
return (0, types_1.dashboardSnapshot)({
|
|
8
|
+
project: { detected: true, label: 'agentic-workflow' },
|
|
9
|
+
confidence: 'provisional',
|
|
10
|
+
overall: 'degraded',
|
|
11
|
+
sections: [
|
|
12
|
+
{ id: 'machine', availability: 'available', items: [{ id: 'machine.cli', label: 'CLI installation', state: 'ok', detail: 'v8.1.6' }] },
|
|
13
|
+
{ id: 'project', availability: 'available', items: [{ id: 'project.sensors', label: 'Sensors', state: 'attention', detail: '2 stale', remediation: 'awm sync' }] },
|
|
14
|
+
{ id: 'planning', availability: 'available', items: [{ id: 'planning.plan', label: 'Active plan', state: 'ok', detail: 'executing' }] },
|
|
15
|
+
{ id: 'execution', availability: 'unavailable', items: [{ id: 'execution.source', label: 'Execution source', state: 'unavailable', remediation: 'awm doctor --full' }] },
|
|
16
|
+
{ id: 'qa', availability: 'available', items: [{ id: 'qa.gate', label: 'Verification gate', state: 'missing', remediation: 'awm sensors' }] },
|
|
17
|
+
{ id: 'retro', availability: 'available', items: [{ id: 'retro.cure', label: 'Cure observations', state: 'not_applicable' }] },
|
|
18
|
+
{ id: 'history', availability: 'available', items: [{ id: 'history.cycle', label: 'Eligible cycle', state: 'ok', detail: '5 minutes' }] },
|
|
19
|
+
],
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
describe('renderFullTerminal', () => {
|
|
24
|
+
it('renders all lifecycle sections in canonical order with status and remediation', () => {
|
|
25
|
+
const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot()));
|
|
26
|
+
const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Retro', 'Final / history'];
|
|
27
|
+
expect(headings.map((heading) => output.indexOf(heading))).toEqual([...headings.map((_, index) => expect.any(Number))].map((_, index) => expect.any(Number)));
|
|
28
|
+
for (let index = 1; index < headings.length; index++)
|
|
29
|
+
expect(output.indexOf(headings[index])).toBeGreaterThan(output.indexOf(headings[index - 1]));
|
|
30
|
+
expect(output).toContain('⚠ Sensors [project.sensors] — 2 stale');
|
|
31
|
+
expect(output).toContain('→ awm sync');
|
|
32
|
+
expect(output).toContain('⊘ Execution source [execution.source] — unavailable');
|
|
33
|
+
expect(output).toContain('✖ Verification gate [qa.gate] — missing');
|
|
34
|
+
});
|
|
35
|
+
it('renders machine-only, unavailable, provisional, and empty states honestly without ANSI color', () => {
|
|
36
|
+
const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)((0, types_1.dashboardSnapshot)({
|
|
37
|
+
overall: 'degraded',
|
|
38
|
+
confidence: 'provisional',
|
|
39
|
+
sections: [{ id: 'machine', availability: 'unavailable', items: [] }],
|
|
40
|
+
})));
|
|
41
|
+
expect(output).toContain('Project: No project detected');
|
|
42
|
+
expect(output).toContain('Confidence: provisional');
|
|
43
|
+
expect(output).toContain('source unavailable');
|
|
44
|
+
expect(output).toContain('No observations reported.');
|
|
45
|
+
expect(output).not.toMatch(/\u001b\[/);
|
|
46
|
+
});
|
|
47
|
+
it('keeps every long-history and large-task observation visible deterministically', () => {
|
|
48
|
+
const history = Array.from({ length: 500 }, (_, index) => ({ id: `history.${index}`, label: `Cycle ${index}`, state: 'ok', detail: `${index} retries` }));
|
|
49
|
+
const execution = Array.from({ length: 2000 }, (_, index) => ({ id: `execution.${index}`, label: `Task ${index}`, state: 'ok' }));
|
|
50
|
+
const snapshot = (0, validate_1.validateDashboardSnapshotV1)(completeSnapshot({ sections: [
|
|
51
|
+
{ id: 'machine', availability: 'available', items: [] },
|
|
52
|
+
{ id: 'project', availability: 'available', items: [] },
|
|
53
|
+
{ id: 'planning', availability: 'available', items: [] },
|
|
54
|
+
{ id: 'execution', availability: 'available', items: execution },
|
|
55
|
+
{ id: 'qa', availability: 'available', items: [] },
|
|
56
|
+
{ id: 'retro', availability: 'available', items: [] },
|
|
57
|
+
{ id: 'history', availability: 'available', items: history },
|
|
58
|
+
] }));
|
|
59
|
+
const first = (0, render_terminal_1.renderFullTerminal)(snapshot);
|
|
60
|
+
expect((0, render_terminal_1.renderFullTerminal)(snapshot)).toBe(first);
|
|
61
|
+
expect(first).toContain('Task 1999');
|
|
62
|
+
expect(first).toContain('Cycle 499');
|
|
63
|
+
expect(first).not.toMatch(/score|ranking/i);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const write_html_1 = require("../../../src/core/dashboard/write-html");
|
|
10
|
+
describe('writeHtmlAtomically', () => {
|
|
11
|
+
let root;
|
|
12
|
+
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-html-')); });
|
|
13
|
+
afterEach(() => fs_1.default.rmSync(root, { recursive: true, force: true }));
|
|
14
|
+
it.each(['', '--flag'])('rejects invalid target %s', (target) => {
|
|
15
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target })).toThrow();
|
|
16
|
+
});
|
|
17
|
+
it('resolves relative targets and refuses overwrite without force', () => {
|
|
18
|
+
const target = (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'report.html' });
|
|
19
|
+
(0, write_html_1.writeHtmlAtomically)({ cwd: root, target, html: '<h1>one</h1>' });
|
|
20
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('<h1>one</h1>');
|
|
21
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'report.html' })).toThrow(/exists/i);
|
|
22
|
+
expect((0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'report.html', force: true })).toBe(target);
|
|
23
|
+
});
|
|
24
|
+
it('rejects directories and symlinks', () => {
|
|
25
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'dir'));
|
|
26
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'real.html'), 'x');
|
|
27
|
+
fs_1.default.symlinkSync(path_1.default.join(root, 'real.html'), path_1.default.join(root, 'link.html'));
|
|
28
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'dir', force: true })).toThrow();
|
|
29
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'link.html', force: true })).toThrow();
|
|
30
|
+
});
|
|
31
|
+
it('rejects a symlinked parent but accepts an ordinary nested directory', () => {
|
|
32
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-html-outside-'));
|
|
33
|
+
try {
|
|
34
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'safe', 'nested'), { recursive: true });
|
|
35
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(root, 'escape'));
|
|
36
|
+
expect((0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'safe/nested/report.html' })).toBe(path_1.default.join(root, 'safe', 'nested', 'report.html'));
|
|
37
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'escape/report.html' })).toThrow('HTML parent directory must not contain a symbolic link');
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
it('rejects absent parents and non-regular existing targets', () => {
|
|
44
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'missing/report.html' })).toThrow(/parent/i);
|
|
45
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'regular-dir'));
|
|
46
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'regular-dir', force: true })).toThrow(/regular/i);
|
|
47
|
+
});
|
|
48
|
+
it('rejects an injected unwritable parent before creating any temporary file', () => {
|
|
49
|
+
const operations = { ...fs_1.default, accessSync: jest.fn(() => { throw new Error('denied'); }) };
|
|
50
|
+
expect(() => (0, write_html_1.resolveHtmlTarget)({ cwd: root, target: 'report.html' }, operations)).toThrow(/writable/i);
|
|
51
|
+
expect(fs_1.default.readdirSync(root)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('accepts a new absolute target and force-replaces only regular files', () => {
|
|
54
|
+
const target = path_1.default.join(root, 'absolute.html');
|
|
55
|
+
expect((0, write_html_1.resolveHtmlTarget)({ cwd: root, target })).toBe(target);
|
|
56
|
+
fs_1.default.writeFileSync(target, 'old');
|
|
57
|
+
expect((0, write_html_1.resolveHtmlTarget)({ cwd: root, target, force: true })).toBe(target);
|
|
58
|
+
});
|
|
59
|
+
it('rejects an existing target at the exported writer boundary without force', () => {
|
|
60
|
+
const target = path_1.default.join(root, 'existing.html');
|
|
61
|
+
fs_1.default.writeFileSync(target, 'previous');
|
|
62
|
+
expect(() => (0, write_html_1.writeHtmlAtomically)({ cwd: root, target, html: 'replacement' })).toThrow(/exists.*force/i);
|
|
63
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('previous');
|
|
64
|
+
});
|
|
65
|
+
it.each(['openSync', 'writeFileSync', 'fsyncSync', 'closeSync', 'renameSync'])('preserves old target and cleans only owned temp when %s fails', (failedOperation) => {
|
|
66
|
+
const target = path_1.default.join(root, 'report.html');
|
|
67
|
+
fs_1.default.writeFileSync(target, 'previous');
|
|
68
|
+
const operations = { ...fs_1.default };
|
|
69
|
+
operations[failedOperation] = jest.fn(() => { throw new Error('injected'); });
|
|
70
|
+
expect(() => (0, write_html_1.writeHtmlAtomically)({ cwd: root, target, html: 'new', force: true }, operations)).toThrow('injected');
|
|
71
|
+
expect(fs_1.default.readFileSync(target, 'utf8')).toBe('previous');
|
|
72
|
+
expect(fs_1.default.readdirSync(root).filter((name) => name.includes('.tmp'))).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
it('uses Windows inherited-ACL open semantics without a POSIX mode', () => {
|
|
75
|
+
const target = path_1.default.join(root, 'windows.html');
|
|
76
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
77
|
+
const original = process.platform;
|
|
78
|
+
try {
|
|
79
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
80
|
+
(0, write_html_1.writeHtmlAtomically)({ cwd: root, target, html: 'ok' });
|
|
81
|
+
expect(open.mock.calls[0]).toHaveLength(2);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
Object.defineProperty(process, 'platform', { value: original, configurable: true });
|
|
85
|
+
open.mockRestore();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it('uses mode 0600 for a POSIX adjacent temporary file', () => {
|
|
89
|
+
if (process.platform === 'win32')
|
|
90
|
+
return;
|
|
91
|
+
const target = path_1.default.join(root, 'posix.html');
|
|
92
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
93
|
+
try {
|
|
94
|
+
(0, write_html_1.writeHtmlAtomically)({ cwd: root, target, html: 'ok' });
|
|
95
|
+
expect(open.mock.calls[0][2]).toBe(0o600);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
open.mockRestore();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
it.each([
|
|
102
|
+
[{ cwd: '', target: path_1.default.join(os_1.default.tmpdir(), 'report.html'), html: 'ok' }, 'writeHtmlAtomically requires a non-empty cwd'],
|
|
103
|
+
[{ target: '', html: 'ok' }, 'writeHtmlAtomically requires a non-empty absolute target'],
|
|
104
|
+
[{ target: `${path_1.default.sep}tmp${path_1.default.sep}report\0.html`, html: 'ok' }, 'writeHtmlAtomically target must not contain NUL'],
|
|
105
|
+
[{ target: 'relative.html', html: 'ok' }, 'writeHtmlAtomically requires a non-empty absolute target'],
|
|
106
|
+
[{ target: path_1.default.join(os_1.default.tmpdir(), 'report.html'), html: '' }, 'writeHtmlAtomically requires non-empty html'],
|
|
107
|
+
[{ target: path_1.default.join(os_1.default.tmpdir(), 'report.html'), html: 'ok', force: 'yes' }, 'writeHtmlAtomically force must be boolean'],
|
|
108
|
+
[{ target: path_1.default.join(os_1.default.tmpdir(), 'report.html'), html: 'ok', platform: 'bad platform' }, 'writeHtmlAtomically platform must be a valid platform'],
|
|
109
|
+
])('validates its public input before filesystem calls', (input, message) => {
|
|
110
|
+
expect(() => (0, write_html_1.writeHtmlAtomically)({ cwd: root, ...input })).toThrow(message);
|
|
111
|
+
});
|
|
112
|
+
});
|