agentic-workflow-manager 8.5.0 → 8.5.1
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/evidence/index.js +66 -1
- package/dist/src/core/dashboard/collect.js +148 -5
- package/dist/src/core/evidence/capture.js +25 -5
- package/dist/tests/core/dashboard/collect.test.js +10 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +89 -0
- package/dist/tests/core/evidence/capture.test.js +35 -1
- package/dist/tests/core/evidence/command.test.js +23 -0
- package/dist/tests/core/evidence/types.test.js +17 -0
- package/dist/tests/structural/release-registry-provenance.test.js +21 -0
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ const capture_1 = require("../../core/evidence/capture");
|
|
|
12
12
|
const store_1 = require("../../core/evidence/store");
|
|
13
13
|
const store_2 = require("../../core/journal/store");
|
|
14
14
|
const store_3 = require("../../core/ledger/store");
|
|
15
|
+
const plan_state_1 = require("../../core/dashboard/plan-state");
|
|
15
16
|
function assertRepoRelativePlan(value) {
|
|
16
17
|
if (typeof value !== 'string' || !value || value.startsWith('--') || path_1.default.isAbsolute(value)
|
|
17
18
|
|| value.includes('\\') || value.split('/').some((part) => part === '' || part === '.' || part === '..')) {
|
|
@@ -19,6 +20,70 @@ function assertRepoRelativePlan(value) {
|
|
|
19
20
|
}
|
|
20
21
|
return value;
|
|
21
22
|
}
|
|
23
|
+
function currentRelease(lines) {
|
|
24
|
+
const releases = lines.flatMap((line) => {
|
|
25
|
+
const match = /^\s*\d+\.\s+\*\*Release\s+([A-Za-z0-9][A-Za-z0-9_-]*)\s*\/\s*#\d+:\*\*/.exec(line);
|
|
26
|
+
return match ? [match[1]] : [];
|
|
27
|
+
});
|
|
28
|
+
return releases.length > 1 ? releases.at(-1) : undefined;
|
|
29
|
+
}
|
|
30
|
+
function marker(lines, name, release) {
|
|
31
|
+
const expression = new RegExp(`^\\s*<!--\\s*${name}(?:\\s*:\\s*[^\\r\\n]*?)?\\s*-->\\s*$`);
|
|
32
|
+
if (release === undefined)
|
|
33
|
+
return lines.some((line) => expression.test(line));
|
|
34
|
+
const releaseExpression = new RegExp(`\\bRelease\\s+${release.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\b`, 'i');
|
|
35
|
+
return lines.some((line) => expression.test(line) && releaseExpression.test(line));
|
|
36
|
+
}
|
|
37
|
+
/** Reads only structural lifecycle syntax; plan prose never crosses into evidence. */
|
|
38
|
+
function planState(root, planPath, journal) {
|
|
39
|
+
const file = path_1.default.join(root, planPath);
|
|
40
|
+
const stat = fs_1.default.lstatSync(file);
|
|
41
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
42
|
+
throw new Error('--plan must reference a regular file');
|
|
43
|
+
let source;
|
|
44
|
+
try {
|
|
45
|
+
source = new TextDecoder('utf-8', { fatal: true }).decode(fs_1.default.readFileSync(file));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error('--plan must be valid UTF-8');
|
|
49
|
+
}
|
|
50
|
+
if (source.includes('\0'))
|
|
51
|
+
throw new Error('--plan must not contain NUL bytes');
|
|
52
|
+
let fenced = false;
|
|
53
|
+
let total = 0;
|
|
54
|
+
let completed = 0;
|
|
55
|
+
const visibleLines = [];
|
|
56
|
+
for (const line of source.split(/\r?\n/)) {
|
|
57
|
+
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
58
|
+
fenced = !fenced;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (fenced)
|
|
62
|
+
continue;
|
|
63
|
+
visibleLines.push(line);
|
|
64
|
+
if (!/^\s*[-*+]\s+\[/.test(line))
|
|
65
|
+
continue;
|
|
66
|
+
const candidate = /^\s*[-*+]\s+\[([^\]])\](?:\s+(.*))?$/.exec(line);
|
|
67
|
+
if (!candidate)
|
|
68
|
+
throw new Error('--plan contains an invalid checklist task');
|
|
69
|
+
if ((candidate[1] !== ' ' && candidate[1] !== 'x' && candidate[1] !== 'X') || !candidate[2]?.trim())
|
|
70
|
+
throw new Error('--plan contains an invalid checklist task');
|
|
71
|
+
total += 1;
|
|
72
|
+
if (candidate[1] === 'x' || candidate[1] === 'X')
|
|
73
|
+
completed += 1;
|
|
74
|
+
}
|
|
75
|
+
if (fenced)
|
|
76
|
+
throw new Error('--plan contains an unclosed fenced block');
|
|
77
|
+
const status = journal.cycle?.status;
|
|
78
|
+
if (status !== 'IN_PROGRESS' && status !== 'COMPLETE' && status !== 'BLOCKED')
|
|
79
|
+
throw new Error('journal cycle status is invalid');
|
|
80
|
+
const release = currentRelease(visibleLines);
|
|
81
|
+
return (0, plan_state_1.classifyPlanState)({
|
|
82
|
+
...(status === 'IN_PROGRESS' ? { journal: { state: 'active' } } : status === 'BLOCKED' ? { journal: { state: 'blocked' } } : {}),
|
|
83
|
+
markers: { qaComplete: marker(visibleLines, 'awm-qa-complete', release), retroComplete: marker(visibleLines, 'awm-retro-complete', release) },
|
|
84
|
+
tasks: { total, completed },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
22
87
|
function registerEvidenceCommand(program) {
|
|
23
88
|
const evidence = program.command('evidence').description('durable privacy-preserving cycle observations');
|
|
24
89
|
evidence.command('capture')
|
|
@@ -86,7 +151,7 @@ function runEvidenceCapture(root, plan, overrides) {
|
|
|
86
151
|
throw new Error('--pr-provider and --pr-number must be supplied together');
|
|
87
152
|
pr = { provider: overrides.prProvider, number: Number(overrides.prNumber) };
|
|
88
153
|
}
|
|
89
|
-
const saved = (0, store_1.writeCycleEvidence)(root, (0, capture_1.captureCycleEvidence)({ root, repositoryIdentity: repositoryIdentity(root, overrides?.repositoryIdentity), planPath, journal: read.state, gates: firstEvaluationGates(read.state), ledger: overrides?.ledger ?? (0, store_3.listEntries)(root, branch), pr }));
|
|
154
|
+
const saved = (0, store_1.writeCycleEvidence)(root, (0, capture_1.captureCycleEvidence)({ root, repositoryIdentity: repositoryIdentity(root, overrides?.repositoryIdentity), planPath, journal: read.state, gates: firstEvaluationGates(read.state), ledger: overrides?.ledger ?? (0, store_3.listEntries)(root, branch), pr, planState: planState(root, planPath, read.state) }));
|
|
90
155
|
return { code: 0, stdout: `${saved.cycleId}\n` };
|
|
91
156
|
}
|
|
92
157
|
catch (error) {
|
|
@@ -11,6 +11,9 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const history_1 = require("../evidence/history");
|
|
13
13
|
const types_1 = require("../evidence/types");
|
|
14
|
+
const store_1 = require("../ledger/store");
|
|
15
|
+
const paths_1 = require("../journal/paths");
|
|
16
|
+
const store_2 = require("../journal/store");
|
|
14
17
|
const sanitize_1 = require("./sanitize");
|
|
15
18
|
const validate_1 = require("./validate");
|
|
16
19
|
const plan_state_1 = require("./plan-state");
|
|
@@ -23,6 +26,7 @@ exports.REMEDIATION_BY_FINDING_ID = {
|
|
|
23
26
|
'planning.source.unavailable': 'awm preflight',
|
|
24
27
|
'execution.source.unavailable': 'awm sensors status',
|
|
25
28
|
};
|
|
29
|
+
const BLOCKED_CYCLE_REMEDIATION = 'awm preflight';
|
|
26
30
|
const EMPTY_ADAPTERS = {
|
|
27
31
|
machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined,
|
|
28
32
|
};
|
|
@@ -84,8 +88,88 @@ function productionDashboardAdapters(context) {
|
|
|
84
88
|
return {
|
|
85
89
|
machine: () => ({ findings: machineFindings }),
|
|
86
90
|
project: () => ({ label: 'Project detected', findings: projectFindings }),
|
|
87
|
-
plans: () =>
|
|
88
|
-
|
|
91
|
+
plans: ({ root }) => {
|
|
92
|
+
const history = readEvidenceHistory(root);
|
|
93
|
+
const journal = readJournalOverlay(root);
|
|
94
|
+
const latestCycleId = history.cycles.at(-1)?.cycleId;
|
|
95
|
+
const latestByPlan = new Map();
|
|
96
|
+
for (const cycle of history.cycles)
|
|
97
|
+
latestByPlan.set(cycle.plan.ref, cycle);
|
|
98
|
+
if (latestByPlan.size === 0 && journal)
|
|
99
|
+
return [journalPlanFinding(journal)];
|
|
100
|
+
return [...latestByPlan.values()].map((cycle) => {
|
|
101
|
+
const overlay = cycle.cycleId === latestCycleId ? journal : undefined;
|
|
102
|
+
const blocked = overlay?.state === 'blocked' || cycle.plan.state === 'blocked';
|
|
103
|
+
return {
|
|
104
|
+
id: `planning.cycle.${cycle.cycleId}`,
|
|
105
|
+
// The plan reference is intentionally never rendered: a plan path
|
|
106
|
+
// can expose repository-specific names. Its opaque cycle ID gives
|
|
107
|
+
// the dashboard deterministic identity without exporting it.
|
|
108
|
+
label: 'Project context',
|
|
109
|
+
state: blocked ? 'attention' : 'ok',
|
|
110
|
+
...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
|
|
111
|
+
lifecycle: overlay ? lifecycleForJournal(overlay) : lifecycleForCycle(cycle),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
execution: ({ root }) => {
|
|
116
|
+
const journal = readJournalOverlay(root);
|
|
117
|
+
if (!evidenceDirectoryExists(root))
|
|
118
|
+
return journal ? journalExecutionFinding(journal) : undefined;
|
|
119
|
+
const history = readEvidenceHistory(root);
|
|
120
|
+
if (history.cycles.length === 0)
|
|
121
|
+
return journal ? journalExecutionFinding(journal) : undefined;
|
|
122
|
+
return {
|
|
123
|
+
execution: history.cycles.map((cycle) => cycleFinding('execution', 'Static preflight', cycle, cycle.cycleState === 'blocked')),
|
|
124
|
+
qa: history.cycles.map((cycle) => cycleFinding('qa', 'Sensors', cycle, cycle.qa.fixes < cycle.qa.findings)),
|
|
125
|
+
retro: history.cycles.map((cycle) => cycleFinding('retro', 'Project context', cycle, cycle.plan.state !== 'executed')),
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** A journal has no durable plan reference. Use a generic fixed row so its
|
|
131
|
+
* authoritative lifecycle remains visible without publishing branch or path. */
|
|
132
|
+
function journalPlanFinding(overlay) {
|
|
133
|
+
const blocked = overlay.state === 'blocked';
|
|
134
|
+
return {
|
|
135
|
+
id: 'planning.current-journal', label: 'Project context', state: blocked ? 'attention' : 'ok',
|
|
136
|
+
...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
|
|
137
|
+
lifecycle: lifecycleForJournal(overlay),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function journalExecutionFinding(overlay) {
|
|
141
|
+
const blocked = overlay.state === 'blocked';
|
|
142
|
+
return {
|
|
143
|
+
execution: [{
|
|
144
|
+
id: 'execution.current-journal', label: 'Static preflight', state: blocked ? 'attention' : 'ok',
|
|
145
|
+
...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
|
|
146
|
+
}],
|
|
147
|
+
qa: [], retro: [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function lifecycleForJournal(overlay) {
|
|
151
|
+
return { journal: { state: overlay.state }, markers: { qaComplete: false, retroComplete: false }, tasks: overlay.tasks };
|
|
152
|
+
}
|
|
153
|
+
function lifecycleForCycle(cycle) {
|
|
154
|
+
const total = cycle.tasks.length;
|
|
155
|
+
if (cycle.plan.state === 'blocked' || cycle.cycleState === 'blocked')
|
|
156
|
+
return { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
|
|
157
|
+
if (cycle.plan.state === 'active')
|
|
158
|
+
return { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
|
|
159
|
+
if (cycle.plan.state === 'executed')
|
|
160
|
+
return { markers: { qaComplete: true, retroComplete: true }, tasks: { total, completed: total } };
|
|
161
|
+
if (cycle.plan.state === 'retro_pending')
|
|
162
|
+
return { markers: { qaComplete: true, retroComplete: false }, tasks: { total, completed: total } };
|
|
163
|
+
if (cycle.plan.state === 'qa_pending')
|
|
164
|
+
return { markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: total } };
|
|
165
|
+
return { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
|
|
166
|
+
}
|
|
167
|
+
function cycleFinding(section, label, cycle, actionable) {
|
|
168
|
+
return {
|
|
169
|
+
id: `${section}.cycle.${cycle.cycleId}`,
|
|
170
|
+
label,
|
|
171
|
+
state: actionable ? 'attention' : 'ok',
|
|
172
|
+
...(actionable ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
|
|
89
173
|
};
|
|
90
174
|
}
|
|
91
175
|
function findings(items, optional = false) {
|
|
@@ -125,7 +209,62 @@ function canonicalOptionalFailure(failure) {
|
|
|
125
209
|
return [];
|
|
126
210
|
return [{ id: failure.findingId, label: 'Optional source unavailable', state: 'unavailable', remediation: exports.REMEDIATION_BY_FINDING_ID[failure.findingId] }];
|
|
127
211
|
}
|
|
128
|
-
function
|
|
212
|
+
function evidenceDirectoryExists(root) {
|
|
213
|
+
try {
|
|
214
|
+
return fs_1.default.lstatSync(path_1.default.join(root, '.awm', 'evidence', 'cycles')).isDirectory();
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT')
|
|
218
|
+
return false;
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Reads only the current branch journal. Missing journals are not observations;
|
|
223
|
+
* corrupt or redirected journal paths fail closed rather than changing lifecycle
|
|
224
|
+
* state from untrusted content. */
|
|
225
|
+
function readJournalOverlay(root) {
|
|
226
|
+
const branch = (0, store_1.detectBranch)(root);
|
|
227
|
+
const file = (0, paths_1.statePath)(root, branch);
|
|
228
|
+
if (!safeJournalStateExists(root, branch, file))
|
|
229
|
+
return undefined;
|
|
230
|
+
const journal = (0, store_2.readJournal)(root, branch);
|
|
231
|
+
if (journal.corrupt || !journal.state)
|
|
232
|
+
throw new Error('current journal is unavailable or corrupt');
|
|
233
|
+
if (journal.state.cycle.status === 'COMPLETE')
|
|
234
|
+
return undefined;
|
|
235
|
+
return {
|
|
236
|
+
state: journal.state.cycle.status === 'BLOCKED' ? 'blocked' : 'active',
|
|
237
|
+
tasks: { total: journal.state.tasks.length, completed: journal.state.tasks.filter((task) => task.status === 'done').length },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function safeJournalStateExists(root, branch, file) {
|
|
241
|
+
for (const directory of [path_1.default.join(root, '.awm'), path_1.default.join(root, '.awm', 'journal'), (0, paths_1.journalDir)(root, branch)]) {
|
|
242
|
+
let stat;
|
|
243
|
+
try {
|
|
244
|
+
stat = fs_1.default.lstatSync(directory);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT')
|
|
248
|
+
return false;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
252
|
+
throw new Error('journal directory is unsafe');
|
|
253
|
+
}
|
|
254
|
+
let stat;
|
|
255
|
+
try {
|
|
256
|
+
stat = fs_1.default.lstatSync(file);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT')
|
|
260
|
+
return false;
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
264
|
+
throw new Error('journal state is unsafe');
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
function readEvidenceHistory(root) {
|
|
129
268
|
const awmDirectory = path_1.default.join(root, '.awm');
|
|
130
269
|
const evidenceDirectory = path_1.default.join(awmDirectory, 'evidence');
|
|
131
270
|
const directory = path_1.default.join(evidenceDirectory, 'cycles');
|
|
@@ -136,7 +275,7 @@ function evidenceHistoryItems(root) {
|
|
|
136
275
|
}
|
|
137
276
|
catch (error) {
|
|
138
277
|
if (error && typeof error === 'object' && error.code === 'ENOENT')
|
|
139
|
-
return
|
|
278
|
+
return (0, history_1.buildEvidenceHistory)([]);
|
|
140
279
|
throw error;
|
|
141
280
|
}
|
|
142
281
|
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
@@ -157,7 +296,10 @@ function evidenceHistoryItems(root) {
|
|
|
157
296
|
seenCycleIds.add(evidence.cycleId);
|
|
158
297
|
return evidence;
|
|
159
298
|
});
|
|
160
|
-
|
|
299
|
+
return (0, history_1.buildEvidenceHistory)(records);
|
|
300
|
+
}
|
|
301
|
+
function evidenceHistoryItems(root) {
|
|
302
|
+
const history = readEvidenceHistory(root);
|
|
161
303
|
return {
|
|
162
304
|
confidence: history.confidence,
|
|
163
305
|
items: history.cycles.map((cycle) => {
|
|
@@ -166,6 +308,7 @@ function evidenceHistoryItems(root) {
|
|
|
166
308
|
id: `history.cycle.${cycle.cycleId}`,
|
|
167
309
|
label: `Cycle ${cycle.cycleId.slice(0, 12)}`,
|
|
168
310
|
state: cycle.cycleState === 'blocked' ? 'attention' : 'ok',
|
|
311
|
+
...(cycle.cycleState === 'blocked' ? { remediation: BLOCKED_CYCLE_REMEDIATION } : {}),
|
|
169
312
|
detail: `plan ${cycle.plan.state}; tasks ${cycle.tasks.length}; retries ${cycle.retries}; QA ${cycle.qa.findings}/${cycle.qa.fixes}; first-pass ${cycle.gates.firstPass ? 'yes' : 'no'}; cures ${cures}`,
|
|
170
313
|
};
|
|
171
314
|
}),
|
|
@@ -31,12 +31,32 @@ function captureCycleEvidence(input) {
|
|
|
31
31
|
throw new Error('capture sources are invalid');
|
|
32
32
|
const tasks = journal.tasks.map((item, index) => { const task = object(item, `task ${index}`); if (typeof task.id !== 'string')
|
|
33
33
|
throw new Error('task id is invalid'); const attempts = nonNegative(task.attempts, 'task attempts'); return { id: task.id, attempts, retries: Math.max(attempts - 1, 0) }; });
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
34
|
+
const verdictIds = new Set();
|
|
35
|
+
const adverseVerdictIds = new Set();
|
|
36
|
+
const adverseVerdicts = journal.verdicts.filter((item, index) => {
|
|
37
|
+
const verdict = object(item, `verdict ${index}`);
|
|
38
|
+
if (typeof verdict.id !== 'string' || !verdict.id)
|
|
39
|
+
throw new Error('verdict id is invalid');
|
|
40
|
+
if (verdictIds.has(verdict.id))
|
|
41
|
+
throw new Error('verdict id is duplicated');
|
|
42
|
+
verdictIds.add(verdict.id);
|
|
43
|
+
if (verdict.result !== 'pass' && verdict.result !== 'fail' && verdict.result !== 'inconclusive')
|
|
44
|
+
throw new Error('verdict result is invalid');
|
|
45
|
+
iso(verdict.receivedAt, 'verdict receivedAt');
|
|
46
|
+
if (verdict.result !== 'pass')
|
|
47
|
+
adverseVerdictIds.add(verdict.id);
|
|
48
|
+
return verdict.result !== 'pass';
|
|
49
|
+
});
|
|
50
|
+
const signatures = adverseVerdicts.map((item) => { const verdict = object(item, 'verdict'); if (typeof verdict.fingerprint !== 'string' || !verdict.fingerprint)
|
|
37
51
|
throw new Error('verdict fingerprint is required'); return hash(`signature:${verdict.fingerprint}`); });
|
|
38
|
-
const fixes = journal.fixes.filter((item, index) => {
|
|
39
|
-
|
|
52
|
+
const fixes = journal.fixes.filter((item, index) => {
|
|
53
|
+
const fix = object(item, `fix ${index}`);
|
|
54
|
+
if (typeof fix.closed !== 'boolean')
|
|
55
|
+
throw new Error('fix closed is invalid');
|
|
56
|
+
if (typeof fix.verdictId !== 'string' || !fix.verdictId || !adverseVerdictIds.has(fix.verdictId))
|
|
57
|
+
throw new Error('fix verdictId must reference an adverse verdict');
|
|
58
|
+
return fix.closed;
|
|
59
|
+
}).length;
|
|
40
60
|
const gates = input.gates.map((item, index) => { const gate = object(item, `gate ${index}`); if (typeof gate.required !== 'boolean' || typeof gate.passed !== 'boolean')
|
|
41
61
|
throw new Error('gate is invalid'); return gate; }).filter((gate) => gate.required === true);
|
|
42
62
|
const cures = input.ledger.filter((item, index) => { const entry = object(item, `ledger ${index}`); if (typeof entry.signature !== 'string' || (entry.polarity !== 'finding' && entry.polarity !== 'win'))
|
|
@@ -161,6 +161,16 @@ describe('collectDashboardSnapshot', () => {
|
|
|
161
161
|
expect(history?.items[0]?.detail).toContain('retries 1; QA 1/1; first-pass yes; cures awaiting_observation');
|
|
162
162
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
163
163
|
});
|
|
164
|
+
it('renders a blocked evidence cycle as an actionable history item with a verified remedy', () => {
|
|
165
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
166
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
167
|
+
(0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleState: 'blocked', plan: { ref: 'docs/plans/current.md', state: 'blocked' } });
|
|
168
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
169
|
+
expect(snapshot.sections.find((section) => section.id === 'history')?.items).toEqual([
|
|
170
|
+
expect.objectContaining({ state: 'attention', remediation: 'awm preflight' }),
|
|
171
|
+
]);
|
|
172
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
173
|
+
});
|
|
164
174
|
it.each(['.awm', 'evidence', 'cycles'])('does not follow a symlinked %s evidence ancestor', (ancestor) => {
|
|
165
175
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
166
176
|
const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const collect_1 = require("../../../src/core/dashboard/collect");
|
|
7
|
+
const store_1 = require("../../../src/core/evidence/store");
|
|
8
|
+
const store_2 = require("../../../src/core/journal/store");
|
|
9
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
10
|
+
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
4
15
|
function context() {
|
|
5
16
|
return {
|
|
6
17
|
machine: {},
|
|
@@ -67,4 +78,82 @@ describe('productionDashboardAdapters', () => {
|
|
|
67
78
|
]));
|
|
68
79
|
expect(JSON.stringify(snapshot)).not.toMatch(/remediationVerified|private|0\.145\.0|token/i);
|
|
69
80
|
});
|
|
81
|
+
it('derives plan lifecycle and execution evidence from validated local cycle records', () => {
|
|
82
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-production-evidence-'));
|
|
83
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
84
|
+
(0, store_1.writeCycleEvidence)(root, {
|
|
85
|
+
...(0, evidence_fixtures_1.cycleEvidenceFixture)(),
|
|
86
|
+
plan: { ref: 'docs/plans/current.md', state: 'executed' },
|
|
87
|
+
tasks: [{ id: 'task-1', attempts: 2, retries: 1 }],
|
|
88
|
+
qa: { findings: 1, fixes: 1, signatures: ['a'.repeat(64)] },
|
|
89
|
+
});
|
|
90
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
91
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
92
|
+
});
|
|
93
|
+
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
|
|
94
|
+
expect.objectContaining({ detail: 'executed', state: 'ok' }),
|
|
95
|
+
]);
|
|
96
|
+
expect(snapshot.sections.find((section) => section.id === 'execution')?.items).toEqual([
|
|
97
|
+
expect.objectContaining({ state: 'ok' }),
|
|
98
|
+
]);
|
|
99
|
+
expect(snapshot.sections.find((section) => section.id === 'qa')?.items).toEqual([
|
|
100
|
+
expect.objectContaining({ state: 'ok' }),
|
|
101
|
+
]);
|
|
102
|
+
expect(snapshot.sections.find((section) => section.id === 'retro')?.items).toEqual([
|
|
103
|
+
expect.objectContaining({ state: 'ok' }),
|
|
104
|
+
]);
|
|
105
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
it.each([
|
|
108
|
+
['IN_PROGRESS', 'active', 'ok'],
|
|
109
|
+
['BLOCKED', 'blocked', 'attention'],
|
|
110
|
+
])('overlays the current branch journal %s over prior executed evidence', (status, expectedLifecycle, expectedState) => {
|
|
111
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-journal-overlay-'));
|
|
112
|
+
const branch = 'dashboard-overlay';
|
|
113
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
114
|
+
(0, child_process_1.execFileSync)('git', ['init', '--initial-branch', branch], { cwd: root, stdio: 'ignore' });
|
|
115
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.email', 'dashboard@example.test'], { cwd: root, stdio: 'ignore' });
|
|
116
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.name', 'Dashboard Test'], { cwd: root, stdio: 'ignore' });
|
|
117
|
+
(0, child_process_1.execFileSync)('git', ['commit', '--allow-empty', '-m', 'fixture'], { cwd: root, stdio: 'ignore' });
|
|
118
|
+
(0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), plan: { ref: 'docs/plans/current.md', state: 'executed' } });
|
|
119
|
+
(0, store_2.initJournal)(root, branch);
|
|
120
|
+
const journal = (0, types_1.emptyState)(branch);
|
|
121
|
+
journal.cycle.status = status;
|
|
122
|
+
(0, store_2.writeJournal)(root, branch, journal);
|
|
123
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
124
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
125
|
+
});
|
|
126
|
+
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
|
|
127
|
+
expect.objectContaining({ detail: expectedLifecycle, state: expectedState }),
|
|
128
|
+
]);
|
|
129
|
+
expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-overlay|dashboard@example|Dashboard Test/i);
|
|
130
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
it.each([
|
|
133
|
+
['IN_PROGRESS', 'active', 'ok'],
|
|
134
|
+
['BLOCKED', 'blocked', 'attention'],
|
|
135
|
+
])('renders the current branch journal %s without prior evidence', (status, expectedLifecycle, expectedState) => {
|
|
136
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-journal-only-'));
|
|
137
|
+
const branch = 'dashboard-journal-only';
|
|
138
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
139
|
+
(0, child_process_1.execFileSync)('git', ['init', '--initial-branch', branch], { cwd: root, stdio: 'ignore' });
|
|
140
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.email', 'dashboard@example.test'], { cwd: root, stdio: 'ignore' });
|
|
141
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.name', 'Dashboard Test'], { cwd: root, stdio: 'ignore' });
|
|
142
|
+
(0, child_process_1.execFileSync)('git', ['commit', '--allow-empty', '-m', 'fixture'], { cwd: root, stdio: 'ignore' });
|
|
143
|
+
(0, store_2.initJournal)(root, branch);
|
|
144
|
+
const journal = (0, types_1.emptyState)(branch);
|
|
145
|
+
journal.cycle.status = status;
|
|
146
|
+
(0, store_2.writeJournal)(root, branch, journal);
|
|
147
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
148
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
149
|
+
});
|
|
150
|
+
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
|
|
151
|
+
expect.objectContaining({ detail: expectedLifecycle, state: expectedState }),
|
|
152
|
+
]);
|
|
153
|
+
expect(snapshot.sections.find((section) => section.id === 'execution')).toEqual(expect.objectContaining({
|
|
154
|
+
availability: 'available', items: [expect.objectContaining({ state: expectedState })],
|
|
155
|
+
}));
|
|
156
|
+
expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-journal-only|dashboard@example|Dashboard Test/i);
|
|
157
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
158
|
+
});
|
|
70
159
|
});
|
|
@@ -10,7 +10,7 @@ describe('captureCycleEvidence', () => {
|
|
|
10
10
|
cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:10.000Z' },
|
|
11
11
|
journalId: 'local-repository-identity',
|
|
12
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' }],
|
|
13
|
+
verdicts: [{ id: 'v1', result: 'fail', fingerprint: 'unsafe-input', detail: 'Alice saw secret prompt', receivedAt: '2026-08-22T10:00:05.000Z' }],
|
|
14
14
|
fixes: [{ verdictId: 'v1', closed: true }],
|
|
15
15
|
},
|
|
16
16
|
gates: [{ required: true, passed: true }],
|
|
@@ -45,4 +45,38 @@ describe('captureCycleEvidence', () => {
|
|
|
45
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
46
|
expect(evidence).toMatchObject({ cycleState: 'blocked', endedAt: '2026-08-22T10:00:03.000Z', durationMs: 3_000 });
|
|
47
47
|
});
|
|
48
|
+
test('counts inconclusive verdicts as adverse findings because they create fix obligations', () => {
|
|
49
|
+
const evidence = (0, capture_1.captureCycleEvidence)({
|
|
50
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
|
|
51
|
+
journal: {
|
|
52
|
+
cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' },
|
|
53
|
+
tasks: [], verdicts: [{ id: 'v1', result: 'inconclusive', fingerprint: 'probe-unavailable', receivedAt: '2026-08-22T10:00:00.000Z' }],
|
|
54
|
+
fixes: [{ verdictId: 'v1', closed: true }],
|
|
55
|
+
}, gates: [], ledger: [],
|
|
56
|
+
});
|
|
57
|
+
expect(evidence.qa).toMatchObject({ findings: 1, fixes: 1, signatures: [expect.stringMatching(/^[a-f0-9]{64}$/)] });
|
|
58
|
+
});
|
|
59
|
+
test('counts only closed fixes that reference an adverse verdict', () => {
|
|
60
|
+
const evidence = (0, capture_1.captureCycleEvidence)({
|
|
61
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
|
|
62
|
+
journal: {
|
|
63
|
+
cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [],
|
|
64
|
+
verdicts: [
|
|
65
|
+
{ id: 'v-fail', result: 'fail', fingerprint: 'failure', receivedAt: '2026-08-22T10:00:00.000Z' },
|
|
66
|
+
{ id: 'v-inconclusive', result: 'inconclusive', fingerprint: 'unknown', receivedAt: '2026-08-22T10:00:00.000Z' },
|
|
67
|
+
],
|
|
68
|
+
fixes: [{ verdictId: 'v-fail', closed: true }, { verdictId: 'v-inconclusive', closed: false }],
|
|
69
|
+
}, gates: [], ledger: [],
|
|
70
|
+
});
|
|
71
|
+
expect(evidence.qa).toMatchObject({ findings: 2, fixes: 1 });
|
|
72
|
+
});
|
|
73
|
+
test.each([
|
|
74
|
+
{ verdicts: [], verdictId: 'missing' },
|
|
75
|
+
{ verdicts: [{ id: 'v-pass', result: 'pass', fingerprint: 'passed', receivedAt: '2026-08-22T10:00:00.000Z' }], verdictId: 'v-pass' },
|
|
76
|
+
])('rejects a fix whose verdict reference is absent or not adverse', ({ verdicts, verdictId }) => {
|
|
77
|
+
expect(() => (0, capture_1.captureCycleEvidence)({
|
|
78
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
|
|
79
|
+
journal: { cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts, fixes: [{ verdictId, closed: true }] }, gates: [], ledger: [],
|
|
80
|
+
})).toThrow(/fix verdictId/i);
|
|
81
|
+
});
|
|
48
82
|
});
|
|
@@ -34,4 +34,27 @@ describe('evidence capture CLI boundary', () => {
|
|
|
34
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
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
36
|
});
|
|
37
|
+
test('derives retro_pending from completed checklist tasks and the QA marker in the plan file', () => {
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: 2026-08-22 -->\n- [x] Build\n- [X] Verify\n');
|
|
39
|
+
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: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
40
|
+
expect(result.code).toBe(0);
|
|
41
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('retro_pending');
|
|
42
|
+
});
|
|
43
|
+
test('uses a blocked journal state over completed plan markers', () => {
|
|
44
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: 2026-08-22 -->\n<!-- awm-retro-complete: 2026-08-22 -->\n- [x] Build\n');
|
|
45
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
46
|
+
expect(result.code).toBe(0);
|
|
47
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('blocked');
|
|
48
|
+
});
|
|
49
|
+
test('does not let Release A markers classify a multi-release plan whose current Release B has no lifecycle markers', () => {
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: Release A / #86 -->\n<!-- awm-retro-complete: Release A / #86 -->\n\n## Delivery order\n1. **Release A / #86:** dashboard\n2. **Release B / #87:** evidence\n\n- [x] Release B task\n');
|
|
51
|
+
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: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
52
|
+
expect(result.code).toBe(0);
|
|
53
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('qa_pending');
|
|
54
|
+
});
|
|
55
|
+
test('rejects a malformed checklist instead of silently classifying a plan', () => {
|
|
56
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n- [z] Unknown state\n');
|
|
57
|
+
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: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
58
|
+
expect(result).toEqual(expect.objectContaining({ code: 2, error: expect.stringMatching(/invalid checklist/i) }));
|
|
59
|
+
});
|
|
37
60
|
});
|
|
@@ -20,6 +20,23 @@ describe('CycleEvidenceV1 validation', () => {
|
|
|
20
20
|
const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
|
|
21
21
|
expect((0, types_1.validateCycleEvidence)({ ...evidence, gates: { required: 2, firstEvaluationsPassed: [true, false], firstPass: false } }).gates.firstEvaluationsPassed).toEqual([true, false]);
|
|
22
22
|
});
|
|
23
|
+
test.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, -1])('rejects a non-finite or negative duration: %p', (durationMs) => {
|
|
24
|
+
const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
|
|
25
|
+
expect(() => (0, types_1.validateCycleEvidence)({
|
|
26
|
+
...evidence,
|
|
27
|
+
startedAt: evidence.endedAt,
|
|
28
|
+
durationMs,
|
|
29
|
+
})).toThrow('durationMs must be a non-negative integer');
|
|
30
|
+
});
|
|
31
|
+
test.each([
|
|
32
|
+
['task attempts', (evidence) => ({ ...evidence, tasks: [{ ...evidence.tasks[0], attempts: -1 }] })],
|
|
33
|
+
['task retries', (evidence) => ({ ...evidence, tasks: [{ ...evidence.tasks[0], retries: -1 }] })],
|
|
34
|
+
['QA findings', (evidence) => ({ ...evidence, qa: { ...evidence.qa, findings: -1 } })],
|
|
35
|
+
['QA fixes', (evidence) => ({ ...evidence, qa: { ...evidence.qa, fixes: -1 } })],
|
|
36
|
+
['required gates', (evidence) => ({ ...evidence, gates: { ...evidence.gates, required: -1 } })],
|
|
37
|
+
])('rejects a negative %s count', (_label, patch) => {
|
|
38
|
+
expect(() => (0, types_1.validateCycleEvidence)(patch((0, evidence_fixtures_1.cycleEvidenceFixture)()))).toThrow('must be a non-negative integer');
|
|
39
|
+
});
|
|
23
40
|
test('permits every dashboard plan state and an absent PR', () => {
|
|
24
41
|
for (const state of ['active', 'blocked', 'qa_pending', 'retro_pending', 'executed', 'legacy_unverifiable']) {
|
|
25
42
|
const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
|
|
@@ -0,0 +1,21 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
8
|
+
const CLI_ROOT = path_1.default.resolve(__dirname, '..', '..');
|
|
9
|
+
const RELEASE_WORKFLOW = path_1.default.join(CLI_ROOT, '..', '.github', 'workflows', 'release.yml');
|
|
10
|
+
describe('release registry provenance', () => {
|
|
11
|
+
const workflow = fs_1.default.readFileSync(RELEASE_WORKFLOW, 'utf8');
|
|
12
|
+
it('resolves the immutable registry tag before publishing the CLI', () => {
|
|
13
|
+
expect(workflow).toContain('Resolve immutable Task 7 registry commit');
|
|
14
|
+
expect(workflow).toContain('refs/tags/${REGISTRY_TAG}^{}');
|
|
15
|
+
expect(workflow).toContain('AWM_PUBLISHED_REGISTRY_TAG: v3.4.0');
|
|
16
|
+
expect(workflow).not.toContain('AWM_REGISTRY_V3_4_0_COMMIT');
|
|
17
|
+
expect(workflow.indexOf('Resolve immutable Task 7 registry commit'))
|
|
18
|
+
.toBeLessThan(workflow.indexOf('- name: Release'));
|
|
19
|
+
expect(workflow).toMatch(/REGISTRY_TAG: v3\.4\.0[\s\S]*AWM_PUBLISHED_REGISTRY_TAG: v3\.4\.0/);
|
|
20
|
+
});
|
|
21
|
+
});
|