@azure-id/orc 1.4.2 → 1.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/CHANGELOG.md +111 -0
  2. package/README.md +46 -46
  3. package/bin/cli.js +5918 -400
  4. package/bin/mockrun-catalog.js +4 -0
  5. package/bin/verify-contracts.js +4475 -4410
  6. package/bin/verify-package.js +21 -2
  7. package/bin/webui/api.js +108 -8
  8. package/bin/webui/app.html +9 -0
  9. package/bin/webui/css/06-responsive.css +14 -0
  10. package/bin/webui/css/panels/test.css +132 -0
  11. package/bin/webui/fixtures/index.js +23 -1
  12. package/bin/webui/fixtures/test.js +547 -0
  13. package/bin/webui/i18n/en/common.json +1 -0
  14. package/bin/webui/i18n/en/nav.json +1 -0
  15. package/bin/webui/i18n/en/overview.json +4 -0
  16. package/bin/webui/i18n/en/test.json +141 -0
  17. package/bin/webui/i18n/id/common.json +1 -0
  18. package/bin/webui/i18n/id/nav.json +1 -0
  19. package/bin/webui/i18n/id/overview.json +4 -0
  20. package/bin/webui/i18n/id/test.json +141 -0
  21. package/bin/webui/js/01-i18n.js +1 -0
  22. package/bin/webui/js/panels/challenge.js +8 -3
  23. package/bin/webui/js/panels/overview.js +10 -0
  24. package/bin/webui/js/panels/test.js +733 -0
  25. package/bin/webui/serve.js +572 -557
  26. package/mock-run/orc-test.md +327 -0
  27. package/package.json +1 -1
  28. package/templates/agents/MODEL-MAPPING.md +2 -0
  29. package/templates/agents/orc-test-designer-opus-5-high.md +158 -0
  30. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -0
  31. package/templates/commands/orc-test.md +66 -0
  32. package/templates/skills/_shared/extra-dispatch.md +1346 -1337
  33. package/templates/skills/_shared/live-target.md +188 -0
  34. package/templates/skills/_shared/phases/trace.md +2 -2
  35. package/templates/skills/_shared/wait.md +241 -240
  36. package/templates/skills/orc-test/SKILL.md +233 -0
@@ -0,0 +1,733 @@
1
+ "use strict";
2
+ /* panels/test.js — orc ui client
3
+ /orc-test: the lane that RUNS the test. What was measured, what was NOT, and
4
+ with whose permission.
5
+
6
+ FIVE TABS, on the Knowledge/Extra precedent: Targets · Surface · Cases ·
7
+ Runs · Findings. The header strip and the findings caution stay OUTSIDE the
8
+ tabs — a caution you must hunt for is a caution nobody reads — and the tab
9
+ survives a re-render (`KN_TAB`'s rule).
10
+
11
+ THE PANEL DERIVES NOTHING. Not a state word, not a severity, not an OWASP
12
+ id, not a tier, not a verdict, not the code-vs-live diff. `orc test show
13
+ --json` is the whole computed object and this file renders it. A test greps
14
+ this panel for the literals it must not own.
15
+
16
+ TWO LINES THIS PANEL DOES NOT CROSS.
17
+ · A FREE ACTION GETS A BUTTON — `surface`, `env`, `report`. Every one costs
18
+ zero model tokens and every one is a real CLI command.
19
+ · A PAID ACTION GETS A COPY-ABLE COMMAND. So does anything that SENDS
20
+ TRAFFIC: `orc test run` is where the requests are, and a page that can
21
+ start a scan against a live host is a page that can start one by
22
+ accident.
23
+
24
+ And the reads are reads in the strict sense: opening this page re-scans
25
+ nothing and probes nothing. `orc test env` is the one probe here and it is
26
+ behind a button you press, the `orc extra ping` shape.
27
+
28
+ Loaded by app.html in the order its numeric prefix names. Classic
29
+ script, no import/export: an ES module import carries no query string,
30
+ and every static request here needs the per-launch session token. */
31
+
32
+ /* ==================================================================== TEST == */
33
+
34
+ // Which tab was open, and which run was selected, so a write that re-renders
35
+ // the panel does not throw the user back to the first of either.
36
+ let TEST_TAB = "targets";
37
+ let TEST_SLUG = null;
38
+ // The last environment reading, per slug, held in this page's memory only. A
39
+ // health state is NOT stored on disk — it is computed fresh on every read (the
40
+ // computeWikiFreshness rule) — so the panel must never render a stale word as
41
+ // if it were current, and must never persist one.
42
+ const TEST_ENV_SEEN = {};
43
+
44
+ // The kind map for the CLI's own state words. The WORDS are the CLI's and are
45
+ // rendered as they come; only the colour is ours.
46
+ const TEST_STATE_KIND = { RED: "bad", UNREADABLE: "bad", OBSERVED: "ok", PARTIAL: "warn" };
47
+ const TEST_VERDICT_KIND = { pass: "ok", fail: "bad", unknown: "warn" };
48
+ const TEST_ENV_KIND = { ready: "ok", starting: "warn", down: "warn", unhealthy: "bad", absent: "bad" };
49
+ const TEST_SEC_KIND = { FOUND: "bad", "observed-clean": "ok", "partly-observed": "warn", unchecked: "warn", "not-observed": "warn", "not-run": "" };
50
+ const TEST_SEV_KIND = { critical: "bad", high: "bad", medium: "warn", low: "" };
51
+ const TEST_TOOL_KIND = { ready: "ok", outdated: "warn", unauthenticated: "warn", absent: "bad" };
52
+
53
+ PANELS.test = function (host) {
54
+ head(host, t("test.title"), t("test.sub"));
55
+ const body = el("div", "stack");
56
+ host.append(body);
57
+ renderTest(body);
58
+ };
59
+
60
+ async function renderTest(body) {
61
+ body.replaceChildren(skeleton(6));
62
+ const [listRes, uiRes] = await Promise.all([
63
+ read("/api/test").catch((e) => ({ data: null, error: e })),
64
+ read("/api/test/ui").catch((e) => ({ data: null, error: e })),
65
+ ]);
66
+ if (!listRes.data) {
67
+ body.replaceChildren(failBox(listRes.error));
68
+ return;
69
+ }
70
+ const list = listRes.data;
71
+ const tools = (uiRes.data && uiRes.data.ui) || null;
72
+
73
+ if (!list.runs || !list.runs.length) {
74
+ const out = frag();
75
+ const c = card(t("test.title"));
76
+ c.append(empty(t("test.none"), t("test.noneHint")));
77
+ c.append(
78
+ laneCommand(
79
+ 'orc test init <slug> --kind be --env local --start auto --destructive deny --reason "<why>"',
80
+ t("test.initCmd"),
81
+ t("common.runInTerminal")
82
+ )
83
+ );
84
+ out.append(c);
85
+ if (tools) out.append(testToolsCard(tools));
86
+ body.replaceChildren(out);
87
+ return;
88
+ }
89
+
90
+ // The selected run. An unknown remembered slug falls back to the first row
91
+ // rather than rendering nothing.
92
+ const slugs = list.runs.map((r) => r.slug);
93
+ if (!TEST_SLUG || slugs.indexOf(TEST_SLUG) === -1) TEST_SLUG = slugs[0];
94
+
95
+ const showRes = await read("/api/test/one?slug=" + encodeURIComponent(TEST_SLUG)).catch((e) => ({ data: null, error: e }));
96
+ const d = showRes.data;
97
+
98
+ const out = frag();
99
+ out.append(testPicker(list, body));
100
+
101
+ // An UNREADABLE ledger is a ROW, never a crash and never a silent gap. It
102
+ // never reaches a tab, because there is nothing measured behind it.
103
+ if (!d || d.state === "UNREADABLE") {
104
+ const c = card(TEST_SLUG);
105
+ c.append(chip(String((d && d.state) || "UNREADABLE"), "bad"));
106
+ c.append(el("div", "note", t("test.target.unreadable")));
107
+ if (d && d.error) c.append(el("pre", "block wrap", esc(d.error)));
108
+ out.append(c);
109
+ if (tools) out.append(testToolsCard(tools));
110
+ body.replaceChildren(out);
111
+ return;
112
+ }
113
+
114
+ // THE HEADER STRIP and THE CAUTION, above the tabs and on every one of them.
115
+ out.append(testStrip(d));
116
+ const caution = testCaution(d);
117
+ if (caution) out.append(caution);
118
+
119
+ const tabs = el("div", "tabs");
120
+ const pane = el("div", "tab-pane stack");
121
+ const views = {
122
+ targets: () => testTargetsTab(d, tools, body),
123
+ surface: () => testSurfaceTab(d, body),
124
+ cases: () => testCasesTab(d),
125
+ runs: () => testRunsTab(d),
126
+ findings: () => testFindingsTab(d, body),
127
+ };
128
+ const select = (which) => {
129
+ TEST_TAB = which;
130
+ for (const b of tabs.children) b.setAttribute("aria-selected", String(b.dataset.tab === which));
131
+ pane.replaceChildren(views[which]());
132
+ };
133
+ // Keys are written out in full, never assembled from the tab id — a key built
134
+ // from a fragment is invisible to every check that looks for one.
135
+ for (const [which, label] of [
136
+ ["targets", t("test.tab.targets")],
137
+ ["surface", t("test.tab.surface")],
138
+ ["cases", t("test.tab.cases")],
139
+ ["runs", t("test.tab.runs")],
140
+ ["findings", t("test.tab.findings")],
141
+ ]) {
142
+ const b = el("button", null, label);
143
+ b.type = "button";
144
+ b.dataset.tab = which;
145
+ b.addEventListener("click", () => select(which));
146
+ tabs.append(b);
147
+ }
148
+ out.append(tabs, pane);
149
+ body.replaceChildren(out);
150
+ select(views[TEST_TAB] ? TEST_TAB : "targets");
151
+ }
152
+
153
+ /* THE RUN PICKER. Every run keeps its row, including the UNREADABLE one: a
154
+ listing that drops it is a listing that hides the one run that needs
155
+ attention. */
156
+ function testPicker(list, body) {
157
+ const c = card(t("test.pick"));
158
+ const bar = el("div", "toolbar");
159
+ const sel = el("select", "text-input");
160
+ for (const r of list.runs) {
161
+ const o = el("option", null, r.slug + " — " + r.state);
162
+ o.value = r.slug;
163
+ if (r.slug === TEST_SLUG) o.selected = true;
164
+ sel.append(o);
165
+ }
166
+ sel.addEventListener("change", () => {
167
+ TEST_SLUG = sel.value;
168
+ renderTest(body);
169
+ });
170
+ bar.append(sel);
171
+ bar.append(el("span", "toolbar-result", list.dir));
172
+ c.append(bar);
173
+ c.append(el("div", "note", t("test.pickHint")));
174
+ return c;
175
+ }
176
+
177
+ /* THE HEADER STRIP. Six values, all of them the CLI's. */
178
+ function testStrip(d) {
179
+ const strip = el("div", "ts-strip");
180
+ const item = (label, value, kind) => {
181
+ const box = el("div", "ts-strip-item" + (kind ? " ts-" + kind : ""));
182
+ box.append(el("span", "ts-strip-value", value));
183
+ box.append(el("span", "ts-strip-label", label));
184
+ strip.append(box);
185
+ };
186
+ const counts = d.counts || {};
187
+ item(t("test.strip.state"), d.state, TEST_STATE_KIND[d.state] || "");
188
+ item(t("test.strip.cases"), String((d.cases || []).length));
189
+ item(t("test.strip.runs"), String((d.runs || []).length));
190
+ const observed = (d.findings || []).filter((f) => f.observed).length;
191
+ item(t("test.strip.findings"), String((d.findings || []).length), observed ? "bad" : "");
192
+ item(t("test.strip.observed"), String(observed), observed ? "bad" : "");
193
+ // The tier word is the CLI's. Never a friendlier synonym.
194
+ item(t("test.strip.security"), (d.security && d.security.tier) || "—", d.security && d.security.found ? "bad" : "");
195
+ return strip;
196
+ }
197
+
198
+ /* THE CAUTION, outside the tabs. It fires only on something the run actually
199
+ MEASURED — a red case or an OBSERVED finding — and it routes to the tab that
200
+ can show the evidence. */
201
+ function testCaution(d) {
202
+ const red = (d.counts && d.counts.fail) || 0;
203
+ const observed = (d.findings || []).filter((f) => f.observed).length;
204
+ if (!red && !observed) return null;
205
+ const box = el("div", "ts-caution");
206
+ const line = el("div", "ts-caution-line");
207
+ line.append(chip(d.state, TEST_STATE_KIND[d.state] || "bad"));
208
+ line.append(el("span", null, d.where));
209
+ box.append(line);
210
+ box.append(el("div", "note", t("test.findings.evidenceNote")));
211
+ return box;
212
+ }
213
+
214
+ /* ------------------------------------------------------------- 1. TARGETS -- */
215
+
216
+ function testTargetsTab(d, tools, body) {
217
+ const out = frag();
218
+ out.append(el("div", "note", t("test.free")));
219
+
220
+ const c = card(t("test.target.title"));
221
+ const g = d.target || {};
222
+ c.append(
223
+ kvList([
224
+ [t("test.target.kind"), g.kind],
225
+ [t("test.target.env"), g.env],
226
+ [t("test.target.baseUrl"), g.base_url],
227
+ [t("test.target.origin"), g.origin],
228
+ ])
229
+ );
230
+ // THE AUTHORIZATION STATEMENT AND THE DESTRUCTIVE DECISION RENDER ALWAYS, and
231
+ // verbatim. A statement nobody filled in is a STATE, said out loud — never an
232
+ // empty row and never a cheerful default.
233
+ const auth = el("div", "ts-auth" + (d.authorized ? "" : " ts-auth-empty"));
234
+ auth.append(el("div", "ts-auth-label", t("test.target.authorized")));
235
+ auth.append(el("div", "ts-auth-value", d.authorized || t("test.target.authorizedEmpty")));
236
+ auth.append(el("div", "note", t("test.target.authorizedNote")));
237
+ c.append(auth);
238
+ const de = d.destructive || {};
239
+ const dbox = el("div", "row-actions");
240
+ dbox.append(el("span", "ts-label", t("test.target.destructive")));
241
+ dbox.append(chip(de.mode || "—", de.mode === "allow" ? "warn" : ""));
242
+ c.append(dbox);
243
+ if (de.reason) c.append(kvList([[t("test.target.destructiveReason"), de.reason]]));
244
+ c.append(el("div", "note", t("test.target.frozen")));
245
+ if (g.env === "remote") c.append(el("div", "note", t("test.target.remoteNote")));
246
+ out.append(c);
247
+
248
+ // Identities. The one-identity sentence is stated wherever the count is
249
+ // shown, because it decides what the security tier can honestly report.
250
+ const ic = card(t("test.target.identities"));
251
+ if (!(d.identities || []).length) ic.append(empty(t("test.target.oneIdentity")));
252
+ for (const i of d.identities || []) {
253
+ const row = el("div", "row-actions");
254
+ row.append(el("strong", null, i.name));
255
+ row.append(chip(i.role, ""));
256
+ row.append(chip(i.source, ""));
257
+ if (i.verified_at) row.append(chip(i.verified_at, "ok"));
258
+ ic.append(row);
259
+ }
260
+ if ((d.identities || []).length && !d.two_identities) ic.append(el("div", "note", t("test.target.oneIdentity")));
261
+ out.append(ic);
262
+
263
+ out.append(testEnvCard(d, body));
264
+ if (tools) out.append(testToolsCard(tools));
265
+ return out;
266
+ }
267
+
268
+ /* THE ENVIRONMENT. A button, and the outcome is the state. It is not fetched on
269
+ open: a health state is not stored, so rendering one on open would mean
270
+ probing the target every time somebody looks at a page. */
271
+ function testEnvCard(d, body) {
272
+ const c = card(t("test.env.title"));
273
+ const seen = TEST_ENV_SEEN[d.slug] || null;
274
+
275
+ const bar = el("div", "row-actions");
276
+ const b = el("button", "btn btn-sm", t("test.env.check"));
277
+ b.type = "button";
278
+ b.addEventListener("click", async () => {
279
+ b.disabled = true;
280
+ setBusy(true);
281
+ try {
282
+ const r = await post("/api/test/env", { slug: d.slug });
283
+ TEST_ENV_SEEN[d.slug] = r.data || { state: null, refused: r };
284
+ } catch (e) {
285
+ TEST_ENV_SEEN[d.slug] = { error: (e && e.message) || String(e) };
286
+ } finally {
287
+ setBusy(false);
288
+ b.disabled = false;
289
+ renderTest(body);
290
+ }
291
+ });
292
+ bar.append(b);
293
+ c.append(bar);
294
+ c.append(el("div", "note", t("test.env.checkWhy")));
295
+
296
+ if (!seen) {
297
+ c.append(el("div", "note", t("test.env.notStored")));
298
+ return c;
299
+ }
300
+ if (seen.error) {
301
+ c.append(el("pre", "block wrap", esc(seen.error)));
302
+ return c;
303
+ }
304
+ // The REMOTE refusal. ORC will not start or stop anything on a host you do
305
+ // not own, and it says so by name rather than rendering an empty card.
306
+ if (seen.ok === false) {
307
+ // The CLI's own reason word, and NO fallback: a refusal ORC could not name
308
+ // is a refusal this panel must not name for it. With no reason the hint
309
+ // stands alone rather than under a word nobody computed.
310
+ if (seen.reason) c.append(chip(String(seen.reason), "warn"));
311
+ c.append(el("div", "note", esc(seen.hint || "")));
312
+ return c;
313
+ }
314
+
315
+ const row = el("div", "row-actions");
316
+ row.append(chip(String(seen.state), TEST_ENV_KIND[seen.state] || ""));
317
+ if (seen.next && seen.next.action) row.append(el("span", null, seen.next.action));
318
+ c.append(row);
319
+ if (seen.next && seen.next.why) c.append(el("div", "note", esc(seen.next.why)));
320
+ if (seen.state === "unhealthy") c.append(el("div", "ts-handback", t("test.env.handBack")));
321
+
322
+ const rows = [];
323
+ if (seen.detected && seen.detected.command) rows.push([t("test.env.detected"), seen.detected.command + " (" + seen.detected.rung + ")"]);
324
+ if (seen.process) rows.push([t("test.env.process"), "pid " + seen.process.pid + (seen.process.alive ? "" : " — not alive")]);
325
+ if (rows.length) c.append(kvList(rows));
326
+
327
+ if (seen.health && (seen.health.tried || []).length) {
328
+ const h = collapsible({
329
+ title: t("test.env.health"),
330
+ count: String(seen.health.tried.length),
331
+ desc: seen.health.why || "",
332
+ collapsed: true,
333
+ content: (() => {
334
+ const box = el("div", "stack stack-sm");
335
+ for (const x of seen.health.tried) {
336
+ const r2 = el("div", "row-actions");
337
+ r2.append(el("code", null, x.path));
338
+ r2.append(chip(x.status === null ? String(x.reason || "—") : String(x.status), x.status && x.status < 400 ? "ok" : "warn"));
339
+ box.append(r2);
340
+ }
341
+ return box;
342
+ })(),
343
+ });
344
+ c.append(h);
345
+ }
346
+ if (!seen.detected || !seen.detected.command) {
347
+ if ((seen.looked_for || []).length) c.append(kvList([[t("test.env.lookedFor"), seen.looked_for.join(", ")]]));
348
+ }
349
+ if (seen.env_vars && (seen.env_vars.missing || []).length) {
350
+ c.append(kvList([[t("test.env.vars"), seen.env_vars.missing.join(", ")]]));
351
+ c.append(el("div", "note", t("test.env.varsNote")));
352
+ }
353
+ if ((seen.log_tail || []).length) {
354
+ c.append(el("div", "ts-label", t("test.env.logs")));
355
+ c.append(el("pre", "block wrap", seen.log_tail.join("\n")));
356
+ }
357
+ return c;
358
+ }
359
+
360
+ /* THE FRONT-END DRIVER. A tool can simply be absent, and that is a STATE before
361
+ it is a failure. ORC NAMES the install command and never runs it. */
362
+ function testToolsCard(tools) {
363
+ const c = card(t("test.ui.title"));
364
+ const row = el("div", "row-actions");
365
+ row.append(chip(String(tools.state), TEST_TOOL_KIND[tools.state] || ""));
366
+ c.append(row);
367
+ if (tools.why) c.append(el("div", "note", esc(tools.why)));
368
+ if (tools.disabled) c.append(el("div", "note", t("test.ui.off")));
369
+ const rows = [];
370
+ if (tools.bin) rows.push([t("test.ui.binary"), tools.bin]);
371
+ if (tools.from) rows.push([t("test.ui.from"), tools.from]);
372
+ if (tools.version) rows.push([t("test.ui.version"), tools.version]);
373
+ if (tools.min_version) rows.push([t("test.ui.minVersion"), tools.min_version]);
374
+ if ((tools.browsers || []).length) rows.push([t("test.ui.browsers"), tools.browsers.join(", ")]);
375
+ if (rows.length) c.append(kvList(rows));
376
+ if (!tools.disabled && tools.state !== "ready" && tools.next) {
377
+ c.append(laneCommand(tools.next, t("test.ui.installNote"), t("common.runInTerminal")));
378
+ // `no_install_alternative: null` MEANS there is none — never that ORC
379
+ // forgot to look.
380
+ if (tools.no_install_alternative === null) c.append(el("div", "note", t("test.ui.noAlternative")));
381
+ }
382
+ c.append(el("div", "note", t("test.ui.fresh")));
383
+ return c;
384
+ }
385
+
386
+ /* ------------------------------------------------------------- 2. SURFACE -- */
387
+
388
+ function testSurfaceTab(d, body) {
389
+ const out = frag();
390
+ const s = d.surface;
391
+
392
+ const bar = card(t("test.surface.take"));
393
+ const row = el("div", "row-actions");
394
+ const b = el("button", "btn btn-sm", t("test.surface.take"));
395
+ b.type = "button";
396
+ b.addEventListener("click", async () => {
397
+ b.disabled = true;
398
+ setBusy(true);
399
+ try {
400
+ await post("/api/test/surface", { slug: d.slug });
401
+ } catch (e) {
402
+ toast(String((e && e.message) || e), "bad");
403
+ } finally {
404
+ setBusy(false);
405
+ renderTest(body);
406
+ }
407
+ });
408
+ row.append(b);
409
+ bar.append(row);
410
+ bar.append(el("div", "note", t("test.surface.takeWhy")));
411
+ out.append(bar);
412
+
413
+ if (!s) {
414
+ out.append(empty(t("test.surface.none"), t("test.surface.takeWhy")));
415
+ return out;
416
+ }
417
+
418
+ const sum = card(t("test.surface.title"));
419
+ sum.append(
420
+ kvList([
421
+ [t("test.surface.specDisk"), s.spec_on_disk ? s.spec_on_disk.file + (s.spec_on_disk.parsed ? "" : " — " + s.spec_on_disk.why) : "—"],
422
+ [t("test.surface.specTarget"), s.spec_at_target && s.spec_at_target.found ? s.spec_at_target.found : "—"],
423
+ [t("test.surface.frameworks"), (s.frameworks || []).join(", ") || "—"],
424
+ [t("test.surface.files"), String(s.files_scanned)],
425
+ [t("test.surface.routes"), String((s.routes || []).length)],
426
+ [t("test.surface.mounts"), (s.mounts || []).map((m) => m.prefix).join(", ") || "—"],
427
+ ])
428
+ );
429
+ if ((s.mounts || []).length) sum.append(el("div", "note", t("test.surface.mountsNote")));
430
+ out.append(sum);
431
+
432
+ // THE CODE-VS-LIVE DIFF. A `null` source means NOT MEASURED and carries its
433
+ // own reason — the one thing that must never render as "no shadow APIs".
434
+ const dc = card(t("test.surface.diff"));
435
+ if (s.diff && s.diff.source) {
436
+ const r1 = el("div", "row-actions");
437
+ r1.append(el("span", "ts-label", t("test.surface.codeOnly")));
438
+ r1.append(chip(String(s.diff.code_only.length), s.diff.code_only.length ? "warn" : ""));
439
+ dc.append(r1);
440
+ for (const x of s.diff.code_only) dc.append(el("div", "ts-route-min", x.key + (x.file ? " " + x.file + (x.line ? ":" + x.line : "") : "")));
441
+ const r2 = el("div", "row-actions");
442
+ r2.append(el("span", "ts-label", t("test.surface.liveOnly")));
443
+ r2.append(chip(String(s.diff.live_only.length), s.diff.live_only.length ? "bad" : ""));
444
+ dc.append(r2);
445
+ for (const x of s.diff.live_only) dc.append(el("div", "ts-route-min ts-bad", x.key));
446
+ dc.append(el("div", "note", esc(s.diff.note)));
447
+ } else {
448
+ dc.append(chip(t("test.surface.notMeasured"), "warn"));
449
+ dc.append(el("div", "note", esc((s.diff && s.diff.why) || "")));
450
+ }
451
+ out.append(dc);
452
+
453
+ const rc = card(t("test.surface.routes"));
454
+ for (const r of s.routes || []) {
455
+ const row2 = el("div", "ts-route");
456
+ row2.append(el("span", "ts-method", r.method));
457
+ row2.append(el("span", "ts-path", r.path));
458
+ row2.append(chip(r.in_code ? t("test.surface.inCode") : t("test.surface.notInCode"), r.in_code ? "" : "bad"));
459
+ // `live: null` is "we did not look", which is a different fact from "it is
460
+ // not there" — and only one of them is a finding.
461
+ row2.append(chip(r.live === null ? t("test.surface.liveUnknown") : r.live ? t("test.surface.liveYes") : t("test.surface.notLive"), r.live === null ? "" : r.live ? "ok" : "warn"));
462
+ const where = el("span", "ts-where", r.file ? r.file + (r.line ? ":" + r.line : "") : r.source);
463
+ row2.append(where);
464
+ rc.append(row2);
465
+ }
466
+ out.append(rc);
467
+
468
+ if ((s.unresolved || []).length) {
469
+ const uc = card(t("test.surface.unresolved"));
470
+ for (const u of s.unresolved) uc.append(el("div", "ts-route-min", u.file + ":" + u.line + " " + u.text));
471
+ uc.append(el("div", "note", t("test.surface.unresolvedNote")));
472
+ out.append(uc);
473
+ }
474
+ return out;
475
+ }
476
+
477
+ /* --------------------------------------------------------------- 3. CASES -- */
478
+
479
+ function testCasesTab(d) {
480
+ const out = frag();
481
+ const cases = d.cases || [];
482
+
483
+ const head2 = card(t("test.cases.budget"));
484
+ head2.append(el("div", "note", t("test.cases.derived")));
485
+ if (d.case_budget) {
486
+ head2.append(kvList([[t("test.cases.budget"), String(d.case_budget.budget)]]));
487
+ if (d.case_budget.reached) {
488
+ head2.append(el("div", "note", t("test.cases.budgetReached")));
489
+ if ((d.case_budget.unfinished || []).length)
490
+ head2.append(kvList([[t("test.cases.unfinished"), d.case_budget.unfinished.join(", ")]]));
491
+ }
492
+ }
493
+ const gaps = cases.filter((c) => (c.gaps || []).length).length;
494
+ if (gaps) {
495
+ const gr = el("div", "row-actions");
496
+ gr.append(chip(gaps + " " + t("test.cases.gap"), "warn"));
497
+ head2.append(gr);
498
+ head2.append(el("div", "note", t("test.cases.gapNote")));
499
+ }
500
+ head2.append(laneCommand("orc test case derive " + d.slug, t("test.paidWhy"), t("common.runInTerminal")));
501
+ out.append(head2);
502
+
503
+ if (!cases.length) {
504
+ out.append(empty(t("test.cases.none"), t("test.cases.derived")));
505
+ } else {
506
+ const lc = card(t("test.cases.title"));
507
+ for (const c of cases) {
508
+ const row = el("div", "ts-case");
509
+ row.append(el("span", "ts-case-id", c.id));
510
+ row.append(chip(c.tier, ""));
511
+ row.append(chip(c.verdict || t("test.cases.notRun"), TEST_VERDICT_KIND[c.verdict] || ""));
512
+ const mid = el("div", "ts-case-mid");
513
+ mid.append(el("div", "ts-case-target", c.target));
514
+ mid.append(el("div", "ts-case-why", c.why));
515
+ if (c.verdict_why) mid.append(el("div", "ts-case-why", c.verdict_why));
516
+ if ((c.gaps || []).length) for (const g of c.gaps) mid.append(el("div", "ts-case-gap", g));
517
+ // The EVIDENCE PATH, never the evidence BODY. A captured response body is
518
+ // the most sensitive thing this lane writes to disk, and it stays on the
519
+ // disk: this panel names where it is and does not open it.
520
+ if (c.evidence) mid.append(el("div", "ts-case-ev", t("test.cases.evidence") + ": " + c.evidence));
521
+ row.append(mid);
522
+ const tail = el("div", "ts-case-tail");
523
+ if (c.owasp) tail.append(chip(c.owasp, ""));
524
+ if (c.identity) tail.append(chip(t("test.cases.identity") + " " + c.identity, ""));
525
+ if (c.mutates) tail.append(chip(t("test.cases.mutates"), "warn"));
526
+ row.append(tail);
527
+ lc.append(row);
528
+ }
529
+ out.append(lc);
530
+ }
531
+
532
+ out.append(testSecurityCard(d));
533
+ return out;
534
+ }
535
+
536
+ /* THE CLOSED OWASP SET. Every one of the ten rows renders, in every state:
537
+ `unchecked` KEEPS ITS SLOT with its reason, never becomes a pass, and never
538
+ raises the exit code. The ids, the names, the state words and the severities
539
+ are all the CLI's. */
540
+ function testSecurityCard(d) {
541
+ const sec = d.security || {};
542
+ const c = card(t("test.security.title"));
543
+ const bar = el("div", "row-actions");
544
+ bar.append(el("span", "ts-label", t("test.security.tier")));
545
+ bar.append(chip(String(sec.tier), sec.tier === "off" ? "warn" : ""));
546
+ if (sec.tier_source && sec.tier_source.source) bar.append(el("span", "ts-where", sec.tier_source.source));
547
+ c.append(bar);
548
+ c.append(el("div", "note", t("test.security.tierDerived")));
549
+ for (const note of sec.notes || []) c.append(el("div", "ts-handback", esc(note)));
550
+
551
+ for (const r of sec.rows || []) {
552
+ const row = el("div", "ts-owasp");
553
+ row.append(el("span", "ts-owasp-id", r.owasp));
554
+ row.append(chip(r.state, TEST_SEC_KIND[r.state] || ""));
555
+ const mid = el("div", "ts-owasp-mid");
556
+ mid.append(el("div", "ts-owasp-name", r.name));
557
+ mid.append(el("div", "ts-case-why", t("test.security.needs") + ": " + r.needs));
558
+ mid.append(el("div", "ts-case-why", t("test.security.probe") + ": " + r.probe));
559
+ if (r.state === "FOUND" && r.severity) {
560
+ const sv = el("div", "row-actions");
561
+ sv.append(chip(r.severity.severity, TEST_SEV_KIND[r.severity.severity] || "bad"));
562
+ sv.append(el("span", "ts-case-why", r.severity.why));
563
+ mid.append(sv);
564
+ if (r.severity.cvss) mid.append(el("div", "ts-case-ev", r.severity.cvss));
565
+ for (const x of (r.cases || []).filter((y) => y.verdict === "fail")) {
566
+ mid.append(el("div", "ts-case-why", x.id + " " + x.why));
567
+ if (x.evidence) mid.append(el("div", "ts-case-ev", x.evidence));
568
+ }
569
+ }
570
+ for (const u of r.unchecked || []) mid.append(el("div", "ts-owasp-unchecked", esc(u.why)));
571
+ row.append(mid);
572
+ c.append(row);
573
+ }
574
+ for (const s of sec.statics || []) {
575
+ c.append(el("div", "ts-label", s.owasp + " — " + s.what));
576
+ for (const r of s.rows || []) c.append(el("div", "ts-route-min", (r.at || "—") + " " + r.what + " (" + r.target + ")"));
577
+ c.append(el("div", "note", t("test.security.statics")));
578
+ }
579
+ c.append(el("div", "note", t("test.security.uncheckedNote")));
580
+ c.append(el("div", "note", t("test.security.closed")));
581
+ c.append(el("div", "note", t("test.security.detect")));
582
+ return c;
583
+ }
584
+
585
+ /* ---------------------------------------------------------------- 4. RUNS -- */
586
+
587
+ function testRunsTab(d) {
588
+ const out = frag();
589
+ const c = card(t("test.runs.title"));
590
+ // `orc test run` is where the traffic is. It is a COPY-ABLE COMMAND and never
591
+ // a button here.
592
+ c.append(laneCommand("orc test run " + d.slug, t("test.trafficWhy"), t("common.runInTerminal")));
593
+ out.append(c);
594
+
595
+ const runs = d.runs || [];
596
+ if (!runs.length) {
597
+ out.append(empty(t("test.runs.none"), t("test.runs.cmd")));
598
+ return out;
599
+ }
600
+
601
+ const list = el("div", "run-list");
602
+ const rows = [];
603
+ const setOpen = (entry, open) => {
604
+ entry.row.classList.toggle("open", open);
605
+ entry.head.setAttribute("aria-expanded", String(open));
606
+ };
607
+ for (const r of runs.slice().reverse()) {
608
+ const row = el("div", "run-row");
609
+ // A row that EXPANDS IN PLACE, one at a time. `.run-card` DECLARES its
610
+ // column count — a card whose child count changes with its state cannot
611
+ // declare its rows.
612
+ const headBtn = el("button", "run-card has-extra");
613
+ headBtn.type = "button";
614
+ headBtn.setAttribute("aria-expanded", "false");
615
+ headBtn.append(el("span", "run-caret", "▸"));
616
+ headBtn.append(chip("run " + r.n, r.counts.fail ? "bad" : "ok"));
617
+ // The stop chip KEEPS ITS SLOT when there was no stop — an empty span, not
618
+ // an em dash. A run that ran to the end and a run whose stop was not
619
+ // computed are different facts, and a placeholder must assert neither.
620
+ headBtn.append(r.stopped ? chip(t("test.runs.stopped") + ": " + r.stopped, "warn") : el("span"));
621
+ const mid = el("div", "run-mid");
622
+ mid.append(el("div", "run-slug", r.counts.pass + " pass · " + r.counts.fail + " fail · " + r.counts.unknown + " unknown"));
623
+ mid.append(el("div", "run-where", r.dir));
624
+ headBtn.append(mid, el("div", "run-age", esc(r.at)));
625
+
626
+ const pane = el("div", "run-pane stack stack-sm");
627
+ if (r.stopped) pane.append(el("div", "ts-handback", t("test.runs.stoppedWhy")));
628
+ pane.append(kvList([[t("test.runs.dir"), r.dir]], true));
629
+ const mine = (d.cases || []).filter((c2) => c2.evidence && c2.evidence.indexOf("/" + r.dir + "/") !== -1);
630
+ for (const c2 of mine) {
631
+ const line = el("div", "ts-case");
632
+ line.append(el("span", "ts-case-id", c2.id));
633
+ line.append(chip(c2.verdict || t("test.cases.notRun"), TEST_VERDICT_KIND[c2.verdict] || ""));
634
+ const m2 = el("div", "ts-case-mid");
635
+ m2.append(el("div", "ts-case-target", c2.target));
636
+ if (c2.verdict_why) m2.append(el("div", "ts-case-why", c2.verdict_why));
637
+ line.append(m2, el("div", "ts-case-tail"));
638
+ pane.append(line);
639
+ }
640
+ const inner = el("div", "run-body-inner");
641
+ inner.append(pane);
642
+ const fold = el("div", "run-body");
643
+ fold.append(inner);
644
+
645
+ const entry = { row, head: headBtn, pane };
646
+ rows.push(entry);
647
+ headBtn.addEventListener("click", () => {
648
+ const isOpen = row.classList.contains("open");
649
+ for (const other of rows) if (other !== entry) setOpen(other, false);
650
+ setOpen(entry, !isOpen);
651
+ });
652
+ row.append(headBtn, fold);
653
+ list.append(row);
654
+ }
655
+ out.append(list);
656
+
657
+ // A FLAKE IS RECORDED, NEVER RETRIED AWAY.
658
+ if ((d.flakes || []).length) {
659
+ const fc = card(t("test.runs.flakes"));
660
+ for (const f of d.flakes)
661
+ fc.append(el("div", "ts-route-min", "run " + f.run + " " + f.id + " " + t("test.runs.was") + " " + f.was + " · " + t("test.runs.now") + " " + f.now));
662
+ fc.append(el("div", "note", t("test.runs.flakesNote")));
663
+ out.append(fc);
664
+ }
665
+ return out;
666
+ }
667
+
668
+ /* ------------------------------------------------------------ 5. FINDINGS -- */
669
+
670
+ function testFindingsTab(d, body) {
671
+ const out = frag();
672
+
673
+ const c = card(t("test.findings.title"));
674
+ const bar = el("div", "row-actions");
675
+ const b = el("button", "btn btn-sm", t("test.findings.report"));
676
+ b.type = "button";
677
+ b.addEventListener("click", async () => {
678
+ b.disabled = true;
679
+ setBusy(true);
680
+ try {
681
+ await post("/api/test/report", { slug: d.slug });
682
+ toast(t("test.findings.report"), "ok");
683
+ } catch (e) {
684
+ toast(String((e && e.message) || e), "bad");
685
+ } finally {
686
+ setBusy(false);
687
+ renderTest(body);
688
+ }
689
+ });
690
+ bar.append(b);
691
+ c.append(bar);
692
+ c.append(el("div", "note", t("test.findings.reportWhy")));
693
+ if (d.report_exists) c.append(kvList([[t("test.findings.reportAt"), d.paths.report]], true));
694
+ else c.append(el("div", "note", t("test.findings.reportMissing")));
695
+ c.append(el("div", "ts-handback", t("test.findings.neverStaged")));
696
+ out.append(c);
697
+
698
+ if (!(d.findings || []).length) {
699
+ out.append(empty(t("test.findings.none"), t("test.findings.evidenceNote")));
700
+ return out;
701
+ }
702
+
703
+ for (const f of d.findings) {
704
+ const fc = card(f.title);
705
+ const row = el("div", "row-actions");
706
+ // OBSERVED and NOT OBSERVED are the two things a report must never blur.
707
+ row.append(chip(f.observed ? t("test.findings.observed") : t("test.findings.notObserved"), f.observed ? "bad" : "warn"));
708
+ row.append(chip(f.severity, TEST_SEV_KIND[f.severity] || ""));
709
+ if (f.owasp) row.append(chip(f.owasp, ""));
710
+ fc.append(row);
711
+ fc.append(el("div", "ts-case-why", f.what));
712
+ // ORC'S SEVERITY IS THE ONE THAT COUNTS. The model's own is kept beside it
713
+ // and printed only when the two disagree.
714
+ fc.append(el("div", "note", esc(f.severity_why)));
715
+ if (f.claimed_severity && String(f.claimed_severity).toLowerCase() !== String(f.severity).toLowerCase()) {
716
+ fc.append(el("div", "note", t("test.security.claimed") + ' "' + f.claimed_severity + '"'));
717
+ fc.append(el("div", "note", t("test.security.severityWho")));
718
+ }
719
+ if (!f.observed) fc.append(el("div", "note", t("test.findings.notObservedNote")));
720
+ const rows = [];
721
+ if (f.case) rows.push([t("test.findings.case"), f.case]);
722
+ if (f.target) rows.push([t("test.findings.target"), f.target]);
723
+ if (f.evidence) rows.push([t("test.cases.evidence"), f.evidence]);
724
+ if (f.cvss) rows.push(["CVSS", f.cvss]);
725
+ if (f.impact) rows.push([t("test.findings.impact"), f.impact]);
726
+ if (f.fix) rows.push([t("test.findings.fix"), f.fix]);
727
+ fc.append(kvList(rows));
728
+ if (f.cvss_note) fc.append(el("div", "note", esc(f.cvss_note)));
729
+ out.append(fc);
730
+ }
731
+ out.append(el("div", "note", t("test.findings.evidenceNote")));
732
+ return out;
733
+ }