hazo_ui 2.17.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +56 -0
- package/README.md +170 -0
- package/SETUP_CHECKLIST.md +10 -0
- package/dist/index.cjs +937 -477
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +301 -2
- package/dist/index.d.ts +301 -2
- package/dist/index.js +866 -423
- package/dist/index.js.map +1 -1
- package/dist/test-harness/index.cjs +1006 -0
- package/dist/test-harness/index.cjs.map +1 -0
- package/dist/test-harness/index.d.cts +144 -0
- package/dist/test-harness/index.d.ts +144 -0
- package/dist/test-harness/index.js +984 -0
- package/dist/test-harness/index.js.map +1 -0
- package/package.json +13 -4
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
import React, { createContext, useState, useRef, useCallback, useContext } from 'react';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
import { optional_import } from 'hazo_core';
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
11
|
+
}) : x)(function(x) {
|
|
12
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
15
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
16
|
+
function cn(...inputs) {
|
|
17
|
+
return twMerge(clsx(inputs));
|
|
18
|
+
}
|
|
19
|
+
function SidebarLayout({
|
|
20
|
+
sidebar,
|
|
21
|
+
children
|
|
22
|
+
}) {
|
|
23
|
+
return /* @__PURE__ */ jsxs("div", { className: "cls_sidebar_layout flex h-screen w-full overflow-hidden bg-gray-50", children: [
|
|
24
|
+
/* @__PURE__ */ jsx("aside", { className: "cls_sidebar_aside flex-none w-60 h-full flex flex-col bg-white border-r border-gray-200 overflow-y-auto", children: sidebar }),
|
|
25
|
+
/* @__PURE__ */ jsx("main", { className: "cls_sidebar_main flex-1 h-full overflow-y-auto", children })
|
|
26
|
+
] });
|
|
27
|
+
}
|
|
28
|
+
function ActiveLink({
|
|
29
|
+
item,
|
|
30
|
+
current_path
|
|
31
|
+
}) {
|
|
32
|
+
const is_active = item.href === "/" ? current_path === "/" : current_path === item.href || current_path.startsWith(item.href + "/");
|
|
33
|
+
return /* @__PURE__ */ jsxs(
|
|
34
|
+
"a",
|
|
35
|
+
{
|
|
36
|
+
href: item.href,
|
|
37
|
+
className: cn(
|
|
38
|
+
"flex items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
39
|
+
is_active ? "bg-blue-50 text-blue-700" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
40
|
+
),
|
|
41
|
+
children: [
|
|
42
|
+
item.icon && /* @__PURE__ */ jsx("span", { className: cn("flex-none text-base", is_active ? "text-blue-600" : "text-gray-400"), children: item.icon }),
|
|
43
|
+
item.label
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function AppSidebar({
|
|
49
|
+
pkg,
|
|
50
|
+
navItems
|
|
51
|
+
}) {
|
|
52
|
+
const [collapsed, set_collapsed] = useState(false);
|
|
53
|
+
const [current_path, set_current_path] = useState("/");
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
function update_path() {
|
|
56
|
+
set_current_path(window.location.pathname);
|
|
57
|
+
}
|
|
58
|
+
update_path();
|
|
59
|
+
window.addEventListener("popstate", update_path);
|
|
60
|
+
return () => window.removeEventListener("popstate", update_path);
|
|
61
|
+
}, []);
|
|
62
|
+
if (collapsed) {
|
|
63
|
+
return /* @__PURE__ */ jsxs("div", { className: "cls_app_sidebar_collapsed flex flex-col items-center py-3 gap-2 w-12 bg-white border-r border-gray-200 h-full", children: [
|
|
64
|
+
/* @__PURE__ */ jsx(
|
|
65
|
+
"button",
|
|
66
|
+
{
|
|
67
|
+
onClick: () => set_collapsed(false),
|
|
68
|
+
className: "p-1.5 rounded hover:bg-gray-100 text-gray-500",
|
|
69
|
+
"aria-label": "Expand sidebar",
|
|
70
|
+
children: "\u25B6"
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
navItems.map((item) => /* @__PURE__ */ jsx(
|
|
74
|
+
"a",
|
|
75
|
+
{
|
|
76
|
+
href: item.href,
|
|
77
|
+
className: "p-1.5 rounded hover:bg-gray-100 text-gray-500 text-base",
|
|
78
|
+
title: item.label,
|
|
79
|
+
children: item.icon ?? item.label.slice(0, 1)
|
|
80
|
+
},
|
|
81
|
+
item.href
|
|
82
|
+
))
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
return /* @__PURE__ */ jsxs("div", { className: "cls_app_sidebar flex flex-col h-full", children: [
|
|
86
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100", children: [
|
|
87
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-sm text-gray-800 truncate", children: pkg }),
|
|
88
|
+
/* @__PURE__ */ jsx(
|
|
89
|
+
"button",
|
|
90
|
+
{
|
|
91
|
+
onClick: () => set_collapsed(true),
|
|
92
|
+
className: "flex-none p-1 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600",
|
|
93
|
+
"aria-label": "Collapse sidebar",
|
|
94
|
+
children: "\u25C0"
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ jsx("nav", { className: "flex-1 overflow-y-auto px-2 py-2 space-y-0.5", children: navItems.map((item) => /* @__PURE__ */ jsx(ActiveLink, { item, current_path }, item.href)) }),
|
|
99
|
+
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-t border-gray-100", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: "Press ? for shortcuts" }) })
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/test-harness/scenarios/api.ts
|
|
104
|
+
var registry = /* @__PURE__ */ new Map();
|
|
105
|
+
function parse_caller_path(stack) {
|
|
106
|
+
if (!stack) return void 0;
|
|
107
|
+
const lines = stack.split("\n");
|
|
108
|
+
for (const line of lines.slice(1)) {
|
|
109
|
+
const match = line.match(/\(([^)]+)\)/) || line.match(/at\s+(\/[^\s:]+)/);
|
|
110
|
+
if (!match) continue;
|
|
111
|
+
const raw = match[1] ?? match[0];
|
|
112
|
+
const path_part = raw.split(":")[0];
|
|
113
|
+
if (!path_part) continue;
|
|
114
|
+
if (path_part.includes("node:") || path_part.includes("test-harness/scenarios/api") || path_part.includes("<anonymous>")) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
return path_part;
|
|
118
|
+
}
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
function registerScenario(id, opts) {
|
|
122
|
+
const stack = new Error().stack;
|
|
123
|
+
const filePath = parse_caller_path(stack);
|
|
124
|
+
registry.set(id, {
|
|
125
|
+
id,
|
|
126
|
+
name: opts.name,
|
|
127
|
+
pkg: opts.pkg,
|
|
128
|
+
filePath,
|
|
129
|
+
cases: opts.cases
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function getRegistry() {
|
|
133
|
+
return registry;
|
|
134
|
+
}
|
|
135
|
+
function clearRegistry() {
|
|
136
|
+
registry.clear();
|
|
137
|
+
}
|
|
138
|
+
var AutoTestContext = createContext(null);
|
|
139
|
+
function build_initial_state() {
|
|
140
|
+
const registry2 = getRegistry();
|
|
141
|
+
const map = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const [id, scenario] of registry2) {
|
|
143
|
+
map.set(id, {
|
|
144
|
+
id,
|
|
145
|
+
name: scenario.name,
|
|
146
|
+
status: "pending",
|
|
147
|
+
cases: scenario.cases.map((c) => ({
|
|
148
|
+
name: c.name,
|
|
149
|
+
status: "pending"
|
|
150
|
+
}))
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return map;
|
|
154
|
+
}
|
|
155
|
+
function AutoTestProvider({
|
|
156
|
+
pkg: _pkg,
|
|
157
|
+
children
|
|
158
|
+
}) {
|
|
159
|
+
const [scenarios, set_scenarios] = useState(
|
|
160
|
+
() => build_initial_state()
|
|
161
|
+
);
|
|
162
|
+
const scenarios_ref = useRef(scenarios);
|
|
163
|
+
scenarios_ref.current = scenarios;
|
|
164
|
+
const update_scenario = useCallback(
|
|
165
|
+
(id, updater) => {
|
|
166
|
+
set_scenarios((prev) => {
|
|
167
|
+
const next = new Map(prev);
|
|
168
|
+
const existing = next.get(id);
|
|
169
|
+
if (existing) {
|
|
170
|
+
next.set(id, updater(existing));
|
|
171
|
+
}
|
|
172
|
+
return next;
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
[]
|
|
176
|
+
);
|
|
177
|
+
const runScenario = useCallback(
|
|
178
|
+
async (id) => {
|
|
179
|
+
const registry2 = getRegistry();
|
|
180
|
+
const scenario = registry2.get(id);
|
|
181
|
+
if (!scenario) return;
|
|
182
|
+
update_scenario(id, (prev) => ({
|
|
183
|
+
...prev,
|
|
184
|
+
status: "running",
|
|
185
|
+
cases: prev.cases.map((c) => ({ ...c, status: "pending" }))
|
|
186
|
+
}));
|
|
187
|
+
let all_passed = true;
|
|
188
|
+
for (let i = 0; i < scenario.cases.length; i++) {
|
|
189
|
+
const case_def = scenario.cases[i];
|
|
190
|
+
if (!case_def) continue;
|
|
191
|
+
update_scenario(id, (prev) => {
|
|
192
|
+
const updated_cases = [...prev.cases];
|
|
193
|
+
updated_cases[i] = { ...updated_cases[i], status: "running" };
|
|
194
|
+
return { ...prev, cases: updated_cases };
|
|
195
|
+
});
|
|
196
|
+
const start_ts = Date.now();
|
|
197
|
+
let result_status = "passed";
|
|
198
|
+
let error;
|
|
199
|
+
let expected;
|
|
200
|
+
let actual;
|
|
201
|
+
try {
|
|
202
|
+
await case_def.run();
|
|
203
|
+
} catch (err) {
|
|
204
|
+
result_status = "failed";
|
|
205
|
+
all_passed = false;
|
|
206
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
207
|
+
if (error && "expected" in error) expected = error.expected;
|
|
208
|
+
if (error && "actual" in error) actual = error.actual;
|
|
209
|
+
}
|
|
210
|
+
const duration_ms = Date.now() - start_ts;
|
|
211
|
+
update_scenario(id, (prev) => {
|
|
212
|
+
const updated_cases = [...prev.cases];
|
|
213
|
+
updated_cases[i] = {
|
|
214
|
+
...updated_cases[i],
|
|
215
|
+
scenarioId: id,
|
|
216
|
+
status: result_status,
|
|
217
|
+
durationMs: duration_ms,
|
|
218
|
+
error,
|
|
219
|
+
expected,
|
|
220
|
+
actual
|
|
221
|
+
};
|
|
222
|
+
return { ...prev, cases: updated_cases };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
update_scenario(id, (prev) => ({
|
|
226
|
+
...prev,
|
|
227
|
+
status: all_passed ? "passed" : "failed"
|
|
228
|
+
}));
|
|
229
|
+
},
|
|
230
|
+
[update_scenario]
|
|
231
|
+
);
|
|
232
|
+
const runAll = useCallback(async () => {
|
|
233
|
+
const ids = Array.from(getRegistry().keys());
|
|
234
|
+
for (const id of ids) {
|
|
235
|
+
await runScenario(id);
|
|
236
|
+
}
|
|
237
|
+
}, [runScenario]);
|
|
238
|
+
const reset = useCallback(() => {
|
|
239
|
+
set_scenarios(build_initial_state());
|
|
240
|
+
}, []);
|
|
241
|
+
const value = {
|
|
242
|
+
scenarios,
|
|
243
|
+
runScenario,
|
|
244
|
+
runAll,
|
|
245
|
+
reset
|
|
246
|
+
};
|
|
247
|
+
return /* @__PURE__ */ jsx(AutoTestContext.Provider, { value, children });
|
|
248
|
+
}
|
|
249
|
+
function useAutoTest() {
|
|
250
|
+
const ctx = useContext(AutoTestContext);
|
|
251
|
+
if (!ctx) {
|
|
252
|
+
throw new Error("useAutoTest must be used within an <AutoTestProvider>");
|
|
253
|
+
}
|
|
254
|
+
return ctx;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/test-harness/scenarios/assertions.ts
|
|
258
|
+
function capture_call_site(stack) {
|
|
259
|
+
if (!stack) return void 0;
|
|
260
|
+
const lines = stack.split("\n");
|
|
261
|
+
for (const line of lines.slice(1)) {
|
|
262
|
+
const match = line.match(/\(([^)]+)\)/) || line.match(/at\s+(\/[^\s]+)/);
|
|
263
|
+
if (!match) continue;
|
|
264
|
+
const raw = match[1] ?? match[0];
|
|
265
|
+
if (raw.includes("test-harness/scenarios/assertions") || raw.includes("node:")) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
return raw;
|
|
269
|
+
}
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
var HazoAssertionError = class extends Error {
|
|
273
|
+
constructor(message, expected, actual, callSite) {
|
|
274
|
+
super(message);
|
|
275
|
+
__publicField(this, "expected");
|
|
276
|
+
__publicField(this, "actual");
|
|
277
|
+
__publicField(this, "callSite");
|
|
278
|
+
this.name = "HazoAssertionError";
|
|
279
|
+
this.expected = expected;
|
|
280
|
+
this.actual = actual;
|
|
281
|
+
this.callSite = callSite;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
function assertEqual(actual, expected) {
|
|
285
|
+
const callSite = capture_call_site(new Error().stack);
|
|
286
|
+
const a_str = JSON.stringify(actual);
|
|
287
|
+
const e_str = JSON.stringify(expected);
|
|
288
|
+
if (a_str !== e_str) {
|
|
289
|
+
throw new HazoAssertionError(
|
|
290
|
+
`assertEqual failed: expected ${e_str}, got ${a_str}`,
|
|
291
|
+
expected,
|
|
292
|
+
actual,
|
|
293
|
+
callSite
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function assertThrows(fn, ErrorClass) {
|
|
298
|
+
const callSite = capture_call_site(new Error().stack);
|
|
299
|
+
let thrown = void 0;
|
|
300
|
+
let did_throw = false;
|
|
301
|
+
try {
|
|
302
|
+
fn();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
thrown = err;
|
|
305
|
+
did_throw = true;
|
|
306
|
+
}
|
|
307
|
+
if (!did_throw) {
|
|
308
|
+
throw new HazoAssertionError(
|
|
309
|
+
`assertThrows failed: expected function to throw but it returned without throwing`,
|
|
310
|
+
ErrorClass ? ErrorClass.name : "Error",
|
|
311
|
+
"no throw",
|
|
312
|
+
callSite
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (ErrorClass && !(thrown instanceof ErrorClass)) {
|
|
316
|
+
const actual_name = thrown instanceof Error ? thrown.constructor.name : String(thrown);
|
|
317
|
+
throw new HazoAssertionError(
|
|
318
|
+
`assertThrows failed: expected ${ErrorClass.name} but got ${actual_name}`,
|
|
319
|
+
ErrorClass.name,
|
|
320
|
+
actual_name,
|
|
321
|
+
callSite
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function assertResolves(promise) {
|
|
326
|
+
const callSite = capture_call_site(new Error().stack);
|
|
327
|
+
try {
|
|
328
|
+
await promise;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
331
|
+
throw new HazoAssertionError(
|
|
332
|
+
`assertResolves failed: promise rejected with "${msg}"`,
|
|
333
|
+
"resolved",
|
|
334
|
+
`rejected: ${msg}`,
|
|
335
|
+
callSite
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function assertRejects(promise, ErrorClass) {
|
|
340
|
+
const callSite = capture_call_site(new Error().stack);
|
|
341
|
+
let thrown = void 0;
|
|
342
|
+
let did_reject = false;
|
|
343
|
+
try {
|
|
344
|
+
await promise;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
thrown = err;
|
|
347
|
+
did_reject = true;
|
|
348
|
+
}
|
|
349
|
+
if (!did_reject) {
|
|
350
|
+
throw new HazoAssertionError(
|
|
351
|
+
`assertRejects failed: expected promise to reject but it resolved`,
|
|
352
|
+
ErrorClass ? ErrorClass.name : "rejection",
|
|
353
|
+
"resolved",
|
|
354
|
+
callSite
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (ErrorClass && !(thrown instanceof ErrorClass)) {
|
|
358
|
+
const actual_name = thrown instanceof Error ? thrown.constructor.name : String(thrown);
|
|
359
|
+
throw new HazoAssertionError(
|
|
360
|
+
`assertRejects failed: expected ${ErrorClass.name} but got ${actual_name}`,
|
|
361
|
+
ErrorClass.name,
|
|
362
|
+
actual_name,
|
|
363
|
+
callSite
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function assertMatch(str, pattern) {
|
|
368
|
+
const callSite = capture_call_site(new Error().stack);
|
|
369
|
+
if (!pattern.test(str)) {
|
|
370
|
+
throw new HazoAssertionError(
|
|
371
|
+
`assertMatch failed: "${str}" did not match ${pattern}`,
|
|
372
|
+
pattern.toString(),
|
|
373
|
+
str,
|
|
374
|
+
callSite
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function assertIncludes(arr, value) {
|
|
379
|
+
const callSite = capture_call_site(new Error().stack);
|
|
380
|
+
const value_str = JSON.stringify(value);
|
|
381
|
+
const found = arr.some((item) => JSON.stringify(item) === value_str);
|
|
382
|
+
if (!found) {
|
|
383
|
+
throw new HazoAssertionError(
|
|
384
|
+
`assertIncludes failed: array does not contain ${value_str}`,
|
|
385
|
+
value,
|
|
386
|
+
arr,
|
|
387
|
+
callSite
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/test-harness/scenarios/format.ts
|
|
393
|
+
var SECRET_PATTERNS = [
|
|
394
|
+
/AKIA[0-9A-Z]{16}/,
|
|
395
|
+
/-----BEGIN.*PRIVATE KEY/,
|
|
396
|
+
/eyJ[a-zA-Z0-9_-]+\.eyJ/
|
|
397
|
+
];
|
|
398
|
+
function redact_secrets(value) {
|
|
399
|
+
let result = value;
|
|
400
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
401
|
+
result = result.replace(new RegExp(pattern.source, pattern.flags + "g"), "<redacted>");
|
|
402
|
+
}
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
function parse_stack_frames(stack) {
|
|
406
|
+
if (!stack) return [];
|
|
407
|
+
const frames = [];
|
|
408
|
+
const lines = stack.split("\n");
|
|
409
|
+
for (const line of lines.slice(1)) {
|
|
410
|
+
const m = line.match(/at\s+(?:\S+\s+)?\(([^)]+):(\d+):(\d+)\)/) || line.match(/at\s+(\/[^\s:]+):(\d+):(\d+)/);
|
|
411
|
+
if (!m) continue;
|
|
412
|
+
const file = m[1];
|
|
413
|
+
const ln = parseInt(m[2], 10);
|
|
414
|
+
const col = parseInt(m[3], 10);
|
|
415
|
+
if (file && ln) {
|
|
416
|
+
frames.push({ file, line: ln, col });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return frames;
|
|
420
|
+
}
|
|
421
|
+
function find_src_frame(frames) {
|
|
422
|
+
const src_frames = frames.filter(
|
|
423
|
+
(f) => f.file.includes("/src/") && !f.file.includes("test-harness")
|
|
424
|
+
);
|
|
425
|
+
return src_frames[src_frames.length - 1];
|
|
426
|
+
}
|
|
427
|
+
function read_file_lines(file_path) {
|
|
428
|
+
try {
|
|
429
|
+
const fs = __require("fs");
|
|
430
|
+
const content = fs.readFileSync(file_path, "utf-8");
|
|
431
|
+
return content.split("\n");
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function extract_context_lines(lines, target_line, context, mark_text) {
|
|
437
|
+
const start = Math.max(0, target_line - context - 1);
|
|
438
|
+
const end = Math.min(lines.length - 1, target_line + context - 1);
|
|
439
|
+
const result = [];
|
|
440
|
+
for (let i = start; i <= end; i++) {
|
|
441
|
+
const line_num = i + 1;
|
|
442
|
+
const line_content = lines[i] ?? "";
|
|
443
|
+
if (line_num === target_line) {
|
|
444
|
+
result.push(`${line_content} ${mark_text}`);
|
|
445
|
+
} else {
|
|
446
|
+
result.push(line_content);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return result.join("\n");
|
|
450
|
+
}
|
|
451
|
+
function compute_diff(expected, actual) {
|
|
452
|
+
if (typeof expected === "object" && expected !== null && typeof actual === "object" && actual !== null) {
|
|
453
|
+
const e_obj = expected;
|
|
454
|
+
const a_obj = actual;
|
|
455
|
+
const all_keys = /* @__PURE__ */ new Set([...Object.keys(e_obj), ...Object.keys(a_obj)]);
|
|
456
|
+
const lines = [];
|
|
457
|
+
for (const key of all_keys) {
|
|
458
|
+
const e_val = JSON.stringify(e_obj[key]);
|
|
459
|
+
const a_val = JSON.stringify(a_obj[key]);
|
|
460
|
+
if (e_val === a_val) {
|
|
461
|
+
lines.push(` ${key}: ${e_val}`);
|
|
462
|
+
} else if (key in e_obj && !(key in a_obj)) {
|
|
463
|
+
lines.push(`- ${key}: ${e_val}`);
|
|
464
|
+
} else if (!(key in e_obj) && key in a_obj) {
|
|
465
|
+
lines.push(`+ ${key}: ${a_val}`);
|
|
466
|
+
} else {
|
|
467
|
+
lines.push(`- ${key}: ${e_val}`);
|
|
468
|
+
lines.push(`+ ${key}: ${a_val}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return lines.join("\n");
|
|
472
|
+
}
|
|
473
|
+
if (typeof expected === "string" && typeof actual === "string") {
|
|
474
|
+
const e_lines = expected.split("\n");
|
|
475
|
+
const a_lines = actual.split("\n");
|
|
476
|
+
const lines = [];
|
|
477
|
+
const max = Math.max(e_lines.length, a_lines.length);
|
|
478
|
+
for (let i = 0; i < max; i++) {
|
|
479
|
+
const e_line = e_lines[i];
|
|
480
|
+
const a_line = a_lines[i];
|
|
481
|
+
if (e_line === a_line) {
|
|
482
|
+
lines.push(` ${e_line ?? ""}`);
|
|
483
|
+
} else {
|
|
484
|
+
if (e_line !== void 0) lines.push(`- ${e_line}`);
|
|
485
|
+
if (a_line !== void 0) lines.push(`+ ${a_line}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return lines.join("\n");
|
|
489
|
+
}
|
|
490
|
+
return `- ${JSON.stringify(expected)}
|
|
491
|
+
+ ${JSON.stringify(actual)}`;
|
|
492
|
+
}
|
|
493
|
+
function build_task_footer(fc, src_frame, assertion_type) {
|
|
494
|
+
const loc = src_frame ? `\`${src_frame.file}:${src_frame.line}\`` : "the source code";
|
|
495
|
+
const scenario = fc.scenarioId;
|
|
496
|
+
const pkg = fc.pkg;
|
|
497
|
+
let body;
|
|
498
|
+
if (assertion_type === "assertEqual") {
|
|
499
|
+
body = `The test expects ${JSON.stringify(fc.expected)}; the code returned ${JSON.stringify(fc.actual)}. The suspect line in ${loc} returns the wrong value. Fix and re-run the \`${scenario}\` scenario.`;
|
|
500
|
+
} else if (assertion_type.startsWith("assertThrows")) {
|
|
501
|
+
const msg = fc.error.message ?? "";
|
|
502
|
+
if (msg.includes("returned without throwing")) {
|
|
503
|
+
body = `The test expects this call to throw ${JSON.stringify(fc.expected)} but it returned ${JSON.stringify(fc.actual)} without throwing. Add the missing error case in ${loc}.`;
|
|
504
|
+
} else {
|
|
505
|
+
body = `Expected ${JSON.stringify(fc.expected)} but got ${JSON.stringify(fc.actual)}. The error type/code in ${loc} is wrong.`;
|
|
506
|
+
}
|
|
507
|
+
} else if (assertion_type === "assertResolves") {
|
|
508
|
+
body = `The test expects the promise to resolve but it rejected. Check ${loc} for the rejection source.`;
|
|
509
|
+
} else if (assertion_type === "assertRejects") {
|
|
510
|
+
if (fc.actual === "resolved") {
|
|
511
|
+
body = `The test expects the promise to reject but it resolved without error. Add the missing rejection case in ${loc}.`;
|
|
512
|
+
} else {
|
|
513
|
+
body = `Expected a rejection of type ${JSON.stringify(fc.expected)} but got ${JSON.stringify(fc.actual)}. Fix the error type in ${loc}.`;
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
body = `The assertion in ${loc} failed. Check the values and fix the implementation.`;
|
|
517
|
+
}
|
|
518
|
+
return `## TASK
|
|
519
|
+
|
|
520
|
+
${body}
|
|
521
|
+
|
|
522
|
+
Verify your fix by re-running:
|
|
523
|
+
\`\`\`bash
|
|
524
|
+
cd ${pkg}/test-app && npm run dev # then Run scenario in the test-app
|
|
525
|
+
\`\`\`
|
|
526
|
+
|
|
527
|
+
Or via the autotest runner: click "Run" next to the \`${scenario}\` scenario.`;
|
|
528
|
+
}
|
|
529
|
+
async function formatAsClaudePrompt(fc, opts = {}) {
|
|
530
|
+
const {
|
|
531
|
+
includeRingBuffer = true,
|
|
532
|
+
includeCodeContext = true,
|
|
533
|
+
maxRingBufferEntries = 15
|
|
534
|
+
} = opts;
|
|
535
|
+
const sections = [];
|
|
536
|
+
const cid = fc.correlationId ?? "n/a";
|
|
537
|
+
const duration_str = fc.durationMs != null ? `${fc.durationMs}ms` : "n/a";
|
|
538
|
+
const scenario_loc = fc.scenarioFilePath ? ` (\`${fc.scenarioFilePath}\`)` : "";
|
|
539
|
+
sections.push(
|
|
540
|
+
`# Failed: ${fc.pkg} / ${fc.scenarioId} / "${fc.caseName}"
|
|
541
|
+
|
|
542
|
+
**Package:** ${fc.pkg}
|
|
543
|
+
**Scenario:** ${fc.scenarioId}${scenario_loc}
|
|
544
|
+
**Case:** "${fc.caseName}"
|
|
545
|
+
**Correlation ID:** ${cid}
|
|
546
|
+
**Duration:** ${duration_str}
|
|
547
|
+
**Failed at:** ${fc.failedAt.toISOString()}`
|
|
548
|
+
);
|
|
549
|
+
sections.push(`## What went wrong
|
|
550
|
+
|
|
551
|
+
${fc.error.message}`);
|
|
552
|
+
const has_assertion = fc.error instanceof HazoAssertionError;
|
|
553
|
+
const expected = has_assertion ? fc.error.expected : fc.expected;
|
|
554
|
+
const actual = has_assertion ? fc.error.actual : fc.actual;
|
|
555
|
+
let exp_actual_section = `## Expected
|
|
556
|
+
|
|
557
|
+
${JSON.stringify(expected, null, 2)}
|
|
558
|
+
|
|
559
|
+
## Actual
|
|
560
|
+
|
|
561
|
+
${JSON.stringify(actual, null, 2)}
|
|
562
|
+
|
|
563
|
+
## Diff (computed)
|
|
564
|
+
|
|
565
|
+
\`\`\`
|
|
566
|
+
${compute_diff(expected, actual)}
|
|
567
|
+
\`\`\``;
|
|
568
|
+
sections.push(exp_actual_section);
|
|
569
|
+
if (includeCodeContext) {
|
|
570
|
+
let test_code_section = "## Test code\n\n";
|
|
571
|
+
const call_site = fc.error instanceof HazoAssertionError ? fc.error.callSite : void 0;
|
|
572
|
+
if (fc.scenarioFilePath && call_site) {
|
|
573
|
+
const call_site_parts = call_site.split(":");
|
|
574
|
+
const line_num = parseInt(call_site_parts[call_site_parts.length - 2] ?? "0", 10);
|
|
575
|
+
const file_lines = read_file_lines(fc.scenarioFilePath);
|
|
576
|
+
if (file_lines && line_num > 0) {
|
|
577
|
+
const start = Math.max(0, line_num - 5 - 1);
|
|
578
|
+
const end = Math.min(file_lines.length - 1, line_num + 5 - 1);
|
|
579
|
+
const context = extract_context_lines(file_lines, line_num, 5, "// \u2190 failed here");
|
|
580
|
+
test_code_section += `\`${fc.scenarioFilePath}:${start + 1}-${end + 1}\`
|
|
581
|
+
|
|
582
|
+
\`\`\`ts
|
|
583
|
+
${context}
|
|
584
|
+
\`\`\``;
|
|
585
|
+
} else {
|
|
586
|
+
test_code_section += `\`${fc.scenarioFilePath}\`
|
|
587
|
+
|
|
588
|
+
source unavailable`;
|
|
589
|
+
}
|
|
590
|
+
} else if (fc.scenarioFilePath) {
|
|
591
|
+
test_code_section += `\`${fc.scenarioFilePath}\`
|
|
592
|
+
|
|
593
|
+
source unavailable (no call-site captured)`;
|
|
594
|
+
} else {
|
|
595
|
+
test_code_section += "source unavailable";
|
|
596
|
+
}
|
|
597
|
+
sections.push(test_code_section);
|
|
598
|
+
}
|
|
599
|
+
if (includeCodeContext) {
|
|
600
|
+
let code_section = "## Code under test\n\n";
|
|
601
|
+
const frames = parse_stack_frames(fc.error.stack);
|
|
602
|
+
const src_frame = find_src_frame(frames);
|
|
603
|
+
if (src_frame) {
|
|
604
|
+
const src_lines = read_file_lines(src_frame.file);
|
|
605
|
+
if (src_lines) {
|
|
606
|
+
const start = Math.max(0, src_frame.line - 8 - 1);
|
|
607
|
+
const end = Math.min(src_lines.length - 1, src_frame.line + 7 - 1);
|
|
608
|
+
const context = extract_context_lines(src_lines, src_frame.line, 8, "// \u2190 suspect line");
|
|
609
|
+
code_section += `\`${src_frame.file}:${start + 1}-${end + 1}\`
|
|
610
|
+
|
|
611
|
+
\`\`\`ts
|
|
612
|
+
${context}
|
|
613
|
+
\`\`\``;
|
|
614
|
+
const all_src = frames.filter(
|
|
615
|
+
(f) => f.file.includes("/src/") && !f.file.includes("test-harness")
|
|
616
|
+
);
|
|
617
|
+
if (all_src.length > 1) {
|
|
618
|
+
code_section += "\n\n**Stack (in-package frames):**\n" + all_src.map((f) => `- \`${f.file}:${f.line}\``).join("\n");
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
code_section += `\`${src_frame.file}:${src_frame.line}\`
|
|
622
|
+
|
|
623
|
+
source unavailable`;
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
code_section += "source unavailable";
|
|
627
|
+
}
|
|
628
|
+
sections.push(code_section);
|
|
629
|
+
}
|
|
630
|
+
const chain_lines = [];
|
|
631
|
+
let current = fc.error;
|
|
632
|
+
let depth = 0;
|
|
633
|
+
const include_stack = process.env.NODE_ENV !== "production" || process.env.HAZO_INCLUDE_STACK === "1";
|
|
634
|
+
while (current instanceof Error && depth < 10) {
|
|
635
|
+
const prefix = depth === 0 ? "Top" : "caused by";
|
|
636
|
+
let entry = `- ${prefix}: \`${current.constructor.name}: ${current.message}\``;
|
|
637
|
+
if (include_stack && current.stack) {
|
|
638
|
+
entry += `
|
|
639
|
+
\`\`\`
|
|
640
|
+
${current.stack.split("\n").slice(0, 4).join("\n ")}
|
|
641
|
+
\`\`\``;
|
|
642
|
+
}
|
|
643
|
+
chain_lines.push(entry);
|
|
644
|
+
current = current.cause;
|
|
645
|
+
depth++;
|
|
646
|
+
}
|
|
647
|
+
if (chain_lines.length === 0) {
|
|
648
|
+
chain_lines.push("- (no error captured)");
|
|
649
|
+
} else if (!(current instanceof Error) && depth > 0 && current === void 0) {
|
|
650
|
+
chain_lines.push("- (No `cause`)");
|
|
651
|
+
}
|
|
652
|
+
sections.push(`## Error chain
|
|
653
|
+
|
|
654
|
+
${chain_lines.join("\n")}`);
|
|
655
|
+
const node_env = redact_secrets(process.env.NODE_ENV ?? "undefined");
|
|
656
|
+
const hazo_env = redact_secrets(process.env.HAZO_ENV ?? "unset");
|
|
657
|
+
const node_ver = process.version ?? "unknown";
|
|
658
|
+
const ctx_lines = [
|
|
659
|
+
`- Node: ${node_ver}`,
|
|
660
|
+
`- NODE_ENV: ${node_env}`,
|
|
661
|
+
`- HAZO_ENV: ${hazo_env}`
|
|
662
|
+
];
|
|
663
|
+
if (fc.adapter) ctx_lines.push(`- Adapter: ${fc.adapter}`);
|
|
664
|
+
if (fc.relevantConfig && Object.keys(fc.relevantConfig).length > 0) {
|
|
665
|
+
ctx_lines.push("- Relevant config:");
|
|
666
|
+
for (const [k, v] of Object.entries(fc.relevantConfig)) {
|
|
667
|
+
ctx_lines.push(` - \`${k} = "${redact_secrets(v)}"\``);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
sections.push(`## Context at test run
|
|
671
|
+
|
|
672
|
+
${ctx_lines.join("\n")}`);
|
|
673
|
+
if (includeRingBuffer) {
|
|
674
|
+
let ring_section = `## Ring buffer (last ${maxRingBufferEntries} events on this correlation ID)
|
|
675
|
+
|
|
676
|
+
`;
|
|
677
|
+
try {
|
|
678
|
+
const hazo_logs = await optional_import("hazo_logs");
|
|
679
|
+
const get_ring = hazo_logs?.["getRingBuffer"];
|
|
680
|
+
if (!hazo_logs || typeof get_ring !== "function") {
|
|
681
|
+
ring_section += "ring buffer not available (hazo_logs >= 2.0.0 required)";
|
|
682
|
+
} else if (fc.correlationId) {
|
|
683
|
+
const entries = get_ring(fc.correlationId, maxRingBufferEntries);
|
|
684
|
+
if (!entries || entries.length === 0) {
|
|
685
|
+
ring_section += "no buffered events";
|
|
686
|
+
} else {
|
|
687
|
+
ring_section += "```\n" + entries.map((e) => JSON.stringify(e)).join("\n") + "\n```";
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
ring_section += "no correlation ID \u2014 ring buffer not queried";
|
|
691
|
+
}
|
|
692
|
+
} catch {
|
|
693
|
+
ring_section += "ring buffer not available (hazo_logs >= 2.0.0 required)";
|
|
694
|
+
}
|
|
695
|
+
sections.push(ring_section);
|
|
696
|
+
}
|
|
697
|
+
const frames_for_task = parse_stack_frames(fc.error.stack);
|
|
698
|
+
const src_frame_for_task = find_src_frame(frames_for_task);
|
|
699
|
+
let assertion_type = "unknown";
|
|
700
|
+
if (fc.error instanceof HazoAssertionError) {
|
|
701
|
+
const msg = fc.error.message;
|
|
702
|
+
if (msg.startsWith("assertEqual")) assertion_type = "assertEqual";
|
|
703
|
+
else if (msg.startsWith("assertThrows")) assertion_type = "assertThrows";
|
|
704
|
+
else if (msg.startsWith("assertResolves")) assertion_type = "assertResolves";
|
|
705
|
+
else if (msg.startsWith("assertRejects")) assertion_type = "assertRejects";
|
|
706
|
+
else if (msg.startsWith("assertMatch")) assertion_type = "assertMatch";
|
|
707
|
+
else if (msg.startsWith("assertIncludes")) assertion_type = "assertIncludes";
|
|
708
|
+
}
|
|
709
|
+
sections.push(build_task_footer(fc, src_frame_for_task, assertion_type));
|
|
710
|
+
return sections.join("\n\n---\n\n");
|
|
711
|
+
}
|
|
712
|
+
async function build_copy_all_text(failed_cases) {
|
|
713
|
+
const registry2 = getRegistry();
|
|
714
|
+
const scenario_ids = Array.from(registry2.keys());
|
|
715
|
+
const failed_with_context = [];
|
|
716
|
+
for (const cr of failed_cases) {
|
|
717
|
+
if (!cr.error) continue;
|
|
718
|
+
const scenario_id = cr.scenarioId ?? scenario_ids.find(
|
|
719
|
+
(sid) => registry2.get(sid)?.cases.some((c) => c.name === cr.name)
|
|
720
|
+
);
|
|
721
|
+
if (!scenario_id) continue;
|
|
722
|
+
const scenario = registry2.get(scenario_id);
|
|
723
|
+
if (!scenario) continue;
|
|
724
|
+
failed_with_context.push({
|
|
725
|
+
fc: {
|
|
726
|
+
pkg: scenario.pkg,
|
|
727
|
+
scenarioId: scenario_id,
|
|
728
|
+
caseName: cr.name,
|
|
729
|
+
failedAt: /* @__PURE__ */ new Date(),
|
|
730
|
+
error: cr.error,
|
|
731
|
+
durationMs: cr.durationMs,
|
|
732
|
+
scenarioFilePath: scenario.filePath,
|
|
733
|
+
expected: cr.expected,
|
|
734
|
+
actual: cr.actual
|
|
735
|
+
},
|
|
736
|
+
scenario_id
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (failed_with_context.length === 0) return "";
|
|
740
|
+
const unique_scenarios = new Set(failed_with_context.map((x) => x.scenario_id));
|
|
741
|
+
const toc_lines = failed_with_context.map(
|
|
742
|
+
({ fc }, i) => `${i + 1}. \`${fc.pkg} / ${fc.scenarioId} / "${fc.caseName}"\``
|
|
743
|
+
);
|
|
744
|
+
const header = `# Test failures (${failed_with_context.length} case${failed_with_context.length === 1 ? "" : "s"} failed across ${unique_scenarios.size} scenario${unique_scenarios.size === 1 ? "" : "s"})
|
|
745
|
+
|
|
746
|
+
## Failed cases
|
|
747
|
+
|
|
748
|
+
` + toc_lines.join("\n");
|
|
749
|
+
const blocks = await Promise.all(
|
|
750
|
+
failed_with_context.map(({ fc }) => formatAsClaudePrompt(fc))
|
|
751
|
+
);
|
|
752
|
+
return [header, ...blocks].join("\n\n---\n\n");
|
|
753
|
+
}
|
|
754
|
+
function CopyAllFailuresButton({
|
|
755
|
+
failedCases
|
|
756
|
+
}) {
|
|
757
|
+
const [state, set_state] = useState("idle");
|
|
758
|
+
async function handle_click() {
|
|
759
|
+
set_state("copying");
|
|
760
|
+
try {
|
|
761
|
+
const text = await build_copy_all_text(failedCases);
|
|
762
|
+
if (!text) {
|
|
763
|
+
set_state("idle");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
await navigator.clipboard.writeText(text);
|
|
767
|
+
set_state("copied");
|
|
768
|
+
setTimeout(() => set_state("idle"), 2500);
|
|
769
|
+
} catch {
|
|
770
|
+
set_state("error");
|
|
771
|
+
setTimeout(() => set_state("idle"), 2500);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const label_map = {
|
|
775
|
+
idle: `Copy ${failedCases.length} failure${failedCases.length === 1 ? "" : "s"}`,
|
|
776
|
+
copying: "Copying...",
|
|
777
|
+
copied: "Copied!",
|
|
778
|
+
error: "Failed to copy"
|
|
779
|
+
};
|
|
780
|
+
return /* @__PURE__ */ jsx(
|
|
781
|
+
"button",
|
|
782
|
+
{
|
|
783
|
+
onClick: handle_click,
|
|
784
|
+
disabled: state === "copying" || failedCases.length === 0,
|
|
785
|
+
className: cn(
|
|
786
|
+
"px-3 py-1.5 rounded text-xs font-medium transition-colors border",
|
|
787
|
+
state === "copied" ? "bg-green-50 border-green-300 text-green-700" : state === "error" ? "bg-red-50 border-red-300 text-red-700" : "bg-orange-50 border-orange-300 text-orange-700 hover:bg-orange-100"
|
|
788
|
+
),
|
|
789
|
+
children: label_map[state]
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
function StatusBadge({ status }) {
|
|
794
|
+
const map = {
|
|
795
|
+
pending: "bg-gray-100 text-gray-500",
|
|
796
|
+
running: "bg-blue-100 text-blue-600 animate-pulse",
|
|
797
|
+
passed: "bg-green-100 text-green-700",
|
|
798
|
+
failed: "bg-red-100 text-red-700"
|
|
799
|
+
};
|
|
800
|
+
const label_map = {
|
|
801
|
+
pending: "pending",
|
|
802
|
+
running: "running",
|
|
803
|
+
passed: "passed",
|
|
804
|
+
failed: "failed"
|
|
805
|
+
};
|
|
806
|
+
return /* @__PURE__ */ jsx(
|
|
807
|
+
"span",
|
|
808
|
+
{
|
|
809
|
+
className: cn(
|
|
810
|
+
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
|
811
|
+
map[status] ?? map["pending"]
|
|
812
|
+
),
|
|
813
|
+
children: label_map[status] ?? status
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
function StatusIcon({ status }) {
|
|
818
|
+
if (status === "passed") return /* @__PURE__ */ jsx("span", { className: "text-green-600 font-bold", children: "\u2713" });
|
|
819
|
+
if (status === "failed") return /* @__PURE__ */ jsx("span", { className: "text-red-600 font-bold", children: "\u2717" });
|
|
820
|
+
if (status === "running") return /* @__PURE__ */ jsx("span", { className: "text-blue-500", children: "\u27F3" });
|
|
821
|
+
return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "\u25CB" });
|
|
822
|
+
}
|
|
823
|
+
function CopySinglePromptButton({
|
|
824
|
+
scenario_id,
|
|
825
|
+
case_result,
|
|
826
|
+
pkg
|
|
827
|
+
}) {
|
|
828
|
+
const [copied, set_copied] = useState(false);
|
|
829
|
+
async function handle_copy() {
|
|
830
|
+
const scenario = getRegistry().get(scenario_id);
|
|
831
|
+
if (!case_result.error) return;
|
|
832
|
+
const fc = {
|
|
833
|
+
pkg,
|
|
834
|
+
scenarioId: scenario_id,
|
|
835
|
+
caseName: case_result.name,
|
|
836
|
+
failedAt: /* @__PURE__ */ new Date(),
|
|
837
|
+
error: case_result.error,
|
|
838
|
+
durationMs: case_result.durationMs,
|
|
839
|
+
scenarioFilePath: scenario?.filePath,
|
|
840
|
+
expected: case_result.expected,
|
|
841
|
+
actual: case_result.actual
|
|
842
|
+
};
|
|
843
|
+
const text = await formatAsClaudePrompt(fc);
|
|
844
|
+
await navigator.clipboard.writeText(text);
|
|
845
|
+
set_copied(true);
|
|
846
|
+
setTimeout(() => set_copied(false), 2e3);
|
|
847
|
+
}
|
|
848
|
+
return /* @__PURE__ */ jsx(
|
|
849
|
+
"button",
|
|
850
|
+
{
|
|
851
|
+
onClick: handle_copy,
|
|
852
|
+
className: "ml-2 text-xs text-blue-600 hover:text-blue-800 underline",
|
|
853
|
+
children: copied ? "Copied!" : "Copy prompt"
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
function ScenarioRow({
|
|
858
|
+
scenario,
|
|
859
|
+
pkg
|
|
860
|
+
}) {
|
|
861
|
+
const { runScenario } = useAutoTest();
|
|
862
|
+
const [expanded, set_expanded] = useState(false);
|
|
863
|
+
const is_running = scenario.status === "running";
|
|
864
|
+
return /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 rounded-lg overflow-hidden", children: [
|
|
865
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 bg-gray-50", children: [
|
|
866
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
867
|
+
/* @__PURE__ */ jsx(
|
|
868
|
+
"button",
|
|
869
|
+
{
|
|
870
|
+
onClick: () => set_expanded((v) => !v),
|
|
871
|
+
className: "text-gray-500 hover:text-gray-700 text-sm font-mono",
|
|
872
|
+
"aria-label": expanded ? "Collapse cases" : "Expand cases",
|
|
873
|
+
children: expanded ? "\u25BE" : "\u25B8"
|
|
874
|
+
}
|
|
875
|
+
),
|
|
876
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-sm text-gray-800", children: scenario.name }),
|
|
877
|
+
/* @__PURE__ */ jsx(StatusBadge, { status: scenario.status }),
|
|
878
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-400", children: [
|
|
879
|
+
scenario.cases.filter((c) => c.status === "passed").length,
|
|
880
|
+
"/",
|
|
881
|
+
scenario.cases.length,
|
|
882
|
+
" passed"
|
|
883
|
+
] })
|
|
884
|
+
] }),
|
|
885
|
+
/* @__PURE__ */ jsx(
|
|
886
|
+
"button",
|
|
887
|
+
{
|
|
888
|
+
disabled: is_running,
|
|
889
|
+
onClick: () => runScenario(scenario.id),
|
|
890
|
+
className: cn(
|
|
891
|
+
"px-3 py-1 rounded text-xs font-medium transition-colors",
|
|
892
|
+
is_running ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "bg-blue-600 text-white hover:bg-blue-700 cursor-pointer"
|
|
893
|
+
),
|
|
894
|
+
children: is_running ? "Running..." : "Run"
|
|
895
|
+
}
|
|
896
|
+
)
|
|
897
|
+
] }),
|
|
898
|
+
expanded && /* @__PURE__ */ jsx("div", { className: "divide-y divide-gray-100", children: scenario.cases.map((c, i) => /* @__PURE__ */ jsxs("div", { className: "px-6 py-2", children: [
|
|
899
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
|
|
900
|
+
/* @__PURE__ */ jsx(StatusIcon, { status: c.status }),
|
|
901
|
+
/* @__PURE__ */ jsx("span", { className: cn(
|
|
902
|
+
"flex-1",
|
|
903
|
+
c.status === "failed" ? "text-red-700" : "text-gray-700"
|
|
904
|
+
), children: c.name }),
|
|
905
|
+
c.durationMs != null && /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-400", children: [
|
|
906
|
+
c.durationMs,
|
|
907
|
+
"ms"
|
|
908
|
+
] }),
|
|
909
|
+
c.status === "failed" && c.error && /* @__PURE__ */ jsx(
|
|
910
|
+
CopySinglePromptButton,
|
|
911
|
+
{
|
|
912
|
+
scenario_id: scenario.id,
|
|
913
|
+
case_result: c,
|
|
914
|
+
pkg
|
|
915
|
+
}
|
|
916
|
+
)
|
|
917
|
+
] }),
|
|
918
|
+
c.status === "failed" && c.error && /* @__PURE__ */ jsx("div", { className: "mt-1 ml-5 text-xs text-red-600 bg-red-50 rounded px-2 py-1 font-mono", children: c.error.message })
|
|
919
|
+
] }, i)) })
|
|
920
|
+
] });
|
|
921
|
+
}
|
|
922
|
+
function AutoTestRunner() {
|
|
923
|
+
const { scenarios, runAll, reset } = useAutoTest();
|
|
924
|
+
const registry2 = getRegistry();
|
|
925
|
+
const first_scenario = Array.from(registry2.values())[0];
|
|
926
|
+
const pkg = first_scenario?.pkg ?? "unknown";
|
|
927
|
+
const scenario_list = Array.from(scenarios.values());
|
|
928
|
+
const any_running = scenario_list.some((s) => s.status === "running");
|
|
929
|
+
const total_cases = scenario_list.reduce((acc, s) => acc + s.cases.length, 0);
|
|
930
|
+
const passed_cases = scenario_list.reduce(
|
|
931
|
+
(acc, s) => acc + s.cases.filter((c) => c.status === "passed").length,
|
|
932
|
+
0
|
|
933
|
+
);
|
|
934
|
+
const failed_cases_flat = scenario_list.flatMap(
|
|
935
|
+
(s) => s.cases.filter((c) => c.status === "failed")
|
|
936
|
+
);
|
|
937
|
+
return /* @__PURE__ */ jsxs("div", { className: "cls_auto_test_runner p-4 space-y-4", children: [
|
|
938
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
939
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
940
|
+
/* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-gray-800", children: "Test Scenarios" }),
|
|
941
|
+
total_cases > 0 && /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 mt-0.5", children: [
|
|
942
|
+
passed_cases,
|
|
943
|
+
"/",
|
|
944
|
+
total_cases,
|
|
945
|
+
" cases passed",
|
|
946
|
+
failed_cases_flat.length > 0 && /* @__PURE__ */ jsxs("span", { className: "text-red-600 ml-2", children: [
|
|
947
|
+
"\xB7 ",
|
|
948
|
+
failed_cases_flat.length,
|
|
949
|
+
" failed"
|
|
950
|
+
] })
|
|
951
|
+
] })
|
|
952
|
+
] }),
|
|
953
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
954
|
+
failed_cases_flat.length > 0 && /* @__PURE__ */ jsx(CopyAllFailuresButton, { failedCases: failed_cases_flat }),
|
|
955
|
+
/* @__PURE__ */ jsx(
|
|
956
|
+
"button",
|
|
957
|
+
{
|
|
958
|
+
onClick: reset,
|
|
959
|
+
disabled: any_running,
|
|
960
|
+
className: "px-3 py-1.5 rounded text-xs font-medium border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50",
|
|
961
|
+
children: "Reset"
|
|
962
|
+
}
|
|
963
|
+
),
|
|
964
|
+
/* @__PURE__ */ jsx(
|
|
965
|
+
"button",
|
|
966
|
+
{
|
|
967
|
+
onClick: runAll,
|
|
968
|
+
disabled: any_running,
|
|
969
|
+
className: cn(
|
|
970
|
+
"px-4 py-1.5 rounded text-xs font-medium transition-colors",
|
|
971
|
+
any_running ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "bg-green-600 text-white hover:bg-green-700 cursor-pointer"
|
|
972
|
+
),
|
|
973
|
+
children: any_running ? "Running..." : "Run All"
|
|
974
|
+
}
|
|
975
|
+
)
|
|
976
|
+
] })
|
|
977
|
+
] }),
|
|
978
|
+
scenario_list.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-500 italic py-8 text-center", children: "No scenarios registered. Import your scenario files to populate this runner." }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: scenario_list.map((s) => /* @__PURE__ */ jsx(ScenarioRow, { scenario: s, pkg }, s.id)) })
|
|
979
|
+
] });
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export { AppSidebar, AutoTestProvider, AutoTestRunner, CopyAllFailuresButton, HazoAssertionError, SidebarLayout, assertEqual, assertIncludes, assertMatch, assertRejects, assertResolves, assertThrows, clearRegistry, formatAsClaudePrompt, getRegistry, registerScenario, useAutoTest };
|
|
983
|
+
//# sourceMappingURL=index.js.map
|
|
984
|
+
//# sourceMappingURL=index.js.map
|