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
@@ -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,191 @@
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('&lt;script&gt;alert(1)&lt;/script&gt;');
35
+ expect(html).toContain('&lt;img src=x onerror=alert(1)&gt;');
36
+ expect(html).toContain('awm sync &amp;&amp; echo &lt;unsafe&gt;');
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 &amp; 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
+ it('renders supplied plans and evidence facts in the dense project panels', () => {
176
+ const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
177
+ confidence: 'supported',
178
+ sections: [
179
+ { id: 'machine', availability: 'available', items: [] }, { id: 'project', availability: 'available', items: [] },
180
+ { id: 'planning', availability: 'available', items: [{ id: 'plan.alpha', label: 'Impact plan', state: 'ok', detail: 'executed' }] },
181
+ { id: 'execution', availability: 'available', items: [] }, { id: 'qa', availability: 'available', items: [] }, { id: 'retro', availability: 'available', items: [] },
182
+ { id: 'history', availability: 'available', items: [{ id: 'history.cycle.abc', label: 'Cycle abc', state: 'ok', detail: 'plan executed; tasks 3; retries 2; QA 2/2; first-pass yes; cures supported' }] },
183
+ ],
184
+ })));
185
+ expect(html).toContain('Confidence: supported');
186
+ expect(html).toContain('Impact plan');
187
+ expect(html).toContain('Cycle abc');
188
+ expect(html).toContain('retries 2; QA 2/2; first-pass yes; cures supported');
189
+ expect(html).not.toContain('Progreso: sin observación');
190
+ });
191
+ });
@@ -0,0 +1,72 @@
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
+ it('renders explicit confidence and every evidence metric without improvement claims', () => {
66
+ const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot({ confidence: 'supported' })));
67
+ expect(output).toContain('Confidence: supported');
68
+ expect(output).toContain('Eligible evidence rows: 1');
69
+ expect(output).toContain('5 minutes');
70
+ expect(output).not.toMatch(/trend|percentage|improvement/i);
71
+ });
72
+ });
@@ -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
+ });
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const capture_1 = require("../../../src/core/evidence/capture");
4
+ describe('captureCycleEvidence', () => {
5
+ test('derives counts and opaque signatures without journal prose or identities', () => {
6
+ const evidence = (0, capture_1.captureCycleEvidence)({
7
+ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git',
8
+ planPath: 'plans/release.md',
9
+ journal: {
10
+ cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:10.000Z' },
11
+ journalId: 'local-repository-identity',
12
+ tasks: [{ id: 'task-a', attempts: 3 }],
13
+ verdicts: [{ result: 'fail', fingerprint: 'unsafe-input', detail: 'Alice saw secret prompt', receivedAt: '2026-08-22T10:00:05.000Z' }],
14
+ fixes: [{ verdictId: 'v1', closed: true }],
15
+ },
16
+ gates: [{ required: true, passed: true }],
17
+ ledger: [{ signature: 'unsafe-input', polarity: 'win', ts: '2026-08-22T10:00:06.000Z' }],
18
+ pr: { provider: 'github', number: 12 },
19
+ });
20
+ expect(evidence).toMatchObject({
21
+ schema: 1, cycleState: 'completed', durationMs: 10_000,
22
+ plan: { ref: 'plans/release.md' }, tasks: [{ id: 'task-a', attempts: 3, retries: 2 }],
23
+ qa: { findings: 1, fixes: 1 }, gates: { required: 1, firstEvaluationsPassed: [true], firstPass: true },
24
+ pr: { provider: 'github', number: 12 },
25
+ });
26
+ expect(evidence.cycleId).toMatch(/^[a-f0-9]{64}$/);
27
+ expect(evidence.qa.signatures[0]).toMatch(/^[a-f0-9]{64}$/);
28
+ expect(evidence.cures[0]).toMatchObject({ signature: expect.stringMatching(/^[a-f0-9]{64}$/) });
29
+ expect(evidence.cures[0].signature).toBe(evidence.qa.signatures[0]);
30
+ expect(JSON.stringify(evidence)).not.toContain('Alice');
31
+ expect(JSON.stringify(evidence)).not.toContain('secret prompt');
32
+ });
33
+ test('derives cycle identity from stable repository identity, not checkout or journal identity', () => {
34
+ const source = {
35
+ planPath: 'plans/release.md', journal: { journalId: 'repository-identity', cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [] }, gates: [], ledger: [],
36
+ };
37
+ const first = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git' });
38
+ const second = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', journal: { ...source.journal, journalId: 'different-journal-identity' } });
39
+ const distinct = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:other/repository.git' });
40
+ expect(first.cycleId).toBe(second.cycleId);
41
+ expect(first.cycleId).not.toBe(distinct.cycleId);
42
+ expect(first.cycleState).toBe('blocked');
43
+ });
44
+ test('captures a blocked cycle from its durable controller heartbeat when completedAt is absent', () => {
45
+ const evidence = (0, capture_1.captureCycleEvidence)({ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md', journal: { cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z' }, controllerHeartbeatAt: '2026-08-22T10:00:03.000Z', tasks: [], verdicts: [], fixes: [] }, gates: [], ledger: [] });
46
+ expect(evidence).toMatchObject({ cycleState: 'blocked', endedAt: '2026-08-22T10:00:03.000Z', durationMs: 3_000 });
47
+ });
48
+ });
@@ -0,0 +1,37 @@
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 evidence_1 = require("../../../src/commands/evidence");
10
+ describe('evidence capture CLI boundary', () => {
11
+ let root;
12
+ beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-command-')); });
13
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
14
+ test('returns exit 2 for a missing or invalid plan', () => {
15
+ expect((0, evidence_1.runEvidenceCapture)(root, undefined).code).toBe(2);
16
+ expect((0, evidence_1.runEvidenceCapture)(root, '../secret.md').code).toBe(2);
17
+ });
18
+ test('returns exactly the captured cycle id on success', () => {
19
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
20
+ expect((0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] })).toEqual(expect.objectContaining({ code: 0, stdout: expect.stringMatching(/^[a-f0-9]{64}\n$/) }));
21
+ });
22
+ test('uses the first attempted gate evaluation instead of final satisfaction', () => {
23
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
24
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], cycleVerificationPlan: [{ id: 'gate', kind: 'test', satisfiedBy: 'retry' }], jobs: { first: { id: 'first', satisfies: ['gate'], verdict: 'fail', phaseTimestamps: { received: '2026-08-22T10:00:00.000Z' } }, retry: { id: 'retry', satisfies: ['gate'], attemptOf: 'first', verdict: 'pass', phaseTimestamps: { received: '2026-08-22T10:00:01.000Z' } } } }, ledger: [] });
25
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
26
+ });
27
+ test('orders independent root evaluations by durable timestamp then id', () => {
28
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
29
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:03.000Z' }, tasks: [], verdicts: [], fixes: [], cycleVerificationPlan: [{ id: 'gate', kind: 'test', satisfiedBy: 'late-pass' }], jobs: { late: { id: 'late-pass', satisfies: ['gate'], verdict: 'pass', phaseTimestamps: { received: '2026-08-22T10:00:02.000Z' } }, early: { id: 'early-fail', satisfies: ['gate'], verdict: 'fail', phaseTimestamps: { received: '2026-08-22T10:00:01.000Z' } } } }, ledger: [] });
30
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
31
+ });
32
+ test('uses the first review verdict rather than satisfiedBy final verdict', () => {
33
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
34
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], fixes: [], cycleVerificationPlan: [{ id: 'review-gate', kind: 'review', satisfiedBy: 'final' }], jobs: {}, verdicts: [{ id: 'first', obligationId: 'review-gate', result: 'fail', fingerprint: 'review-gate', receivedAt: '2026-08-22T10:00:00.000Z' }, { id: 'final', obligationId: 'review-gate', result: 'pass', fingerprint: 'review-gate', receivedAt: '2026-08-22T10:00:01.000Z' }] }, ledger: [] });
35
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
36
+ });
37
+ });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const history_1 = require("../../../src/core/evidence/history");
4
+ const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
5
+ describe('evidence history', () => {
6
+ it.each([[0, 'none'], [1, 'provisional'], [2, 'observing'], [4, 'observing'], [5, 'supported']])('classifies %i eligible cycles as %s', (count, expected) => expect((0, history_1.confidenceForCycles)(count)).toBe(expected));
7
+ it.each([
8
+ [0, false, 'awaiting_observation'], [1, false, 'observing'], [2, false, 'observing'],
9
+ [3, false, 'supported'], [1, true, 'recurred'],
10
+ ])('classifies cure observation honestly', (laterEligibleCycles, recurred, expected) => {
11
+ expect((0, history_1.classifyCure)({ laterEligibleCycles, recurred })).toBe(expected);
12
+ });
13
+ it('validates every record, retains every eligible row, and sorts stably by timestamp and cycle id', () => {
14
+ const later = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'd'.repeat(64), startedAt: '2026-08-22T11:00:00.000Z', endedAt: '2026-08-22T11:01:00.000Z', cures: [] };
15
+ const sameTimestamp = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: '0'.repeat(64), cures: [] };
16
+ const history = (0, history_1.buildEvidenceHistory)([later, (0, evidence_fixtures_1.cycleEvidenceFixture)(), sameTimestamp]);
17
+ expect(history.confidence).toBe('observing');
18
+ expect(history.empty).toBe(false);
19
+ expect(history.cycles.map((cycle) => cycle.cycleId)).toEqual(['0'.repeat(64), 'a'.repeat(64), 'd'.repeat(64)]);
20
+ expect(() => (0, history_1.buildEvidenceHistory)([{ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), durationMs: -1 }])).toThrow(/duration/i);
21
+ });
22
+ it('reports zero cycles explicitly without a trend, percentage, or improvement claim', () => {
23
+ const history = (0, history_1.buildEvidenceHistory)([]);
24
+ expect(history).toEqual(expect.objectContaining({ empty: true, confidence: 'none', cycles: [] }));
25
+ expect(JSON.stringify(history)).not.toMatch(/trend|percentage|improvement/i);
26
+ });
27
+ it('retains blocked records but excludes them from confidence and cure observation windows', () => {
28
+ const blocked = Array.from({ length: 5 }, (_, index) => ({
29
+ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: `${index}`.repeat(64), cycleState: 'blocked', cures: [],
30
+ }));
31
+ const history = (0, history_1.buildEvidenceHistory)(blocked);
32
+ expect(history.cycles).toHaveLength(5);
33
+ expect(history.confidence).toBe('none');
34
+ const cured = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'c'.repeat(64), endedAt: '2026-08-22T10:01:00.000Z' };
35
+ const laterBlocked = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'd'.repeat(64), cycleState: 'blocked', startedAt: '2026-08-22T11:00:00.000Z', endedAt: '2026-08-22T11:01:00.000Z', cures: [] };
36
+ const laterCompleted = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'e'.repeat(64), startedAt: '2026-08-22T12:00:00.000Z', endedAt: '2026-08-22T12:01:00.000Z', cures: [] };
37
+ const mixed = (0, history_1.buildEvidenceHistory)([cured, laterBlocked, laterCompleted]);
38
+ expect(mixed.confidence).toBe('observing');
39
+ expect(mixed.cycles[0].cureEfficacy[0].efficacy).toBe('observing');
40
+ });
41
+ });
@@ -0,0 +1,32 @@
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 evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
10
+ const store_1 = require("../../../src/core/evidence/store");
11
+ describe('cycle evidence store', () => {
12
+ let root;
13
+ beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-')); });
14
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
15
+ test('durably replaces one observation for the same opaque cycle id', () => {
16
+ const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
17
+ (0, store_1.writeCycleEvidence)(root, evidence);
18
+ (0, store_1.writeCycleEvidence)(root, { ...evidence, qa: { ...evidence.qa, fixes: 0 } });
19
+ const dir = path_1.default.join(root, '.awm', 'evidence', 'cycles');
20
+ expect(fs_1.default.readdirSync(dir)).toEqual([`${evidence.cycleId}.json`]);
21
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, `${evidence.cycleId}.json`), 'utf8')).qa.fixes).toBe(0);
22
+ });
23
+ test('rejects symlinked evidence ancestors without writing outside root', () => {
24
+ if (process.platform === 'win32')
25
+ return;
26
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-outside-'));
27
+ fs_1.default.symlinkSync(outside, path_1.default.join(root, '.awm'));
28
+ expect(() => (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)())).toThrow(/symlink/);
29
+ expect(fs_1.default.readdirSync(outside)).toEqual([]);
30
+ fs_1.default.rmSync(outside, { recursive: true, force: true });
31
+ });
32
+ });