@zibby/core 0.1.48 → 0.3.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 (43) hide show
  1. package/dist/index.js +100 -100
  2. package/dist/package.json +2 -2
  3. package/dist/register-built-in-strategies.js +57 -57
  4. package/dist/strategies/assistant-strategy.js +1 -1
  5. package/dist/strategies/claude-strategy.js +3 -3
  6. package/dist/strategies/codex-strategy.js +3 -3
  7. package/dist/strategies/cursor-strategy.js +30 -30
  8. package/dist/strategies/gemini-strategy.js +13 -13
  9. package/dist/strategies/index.js +57 -57
  10. package/dist/templates/browser-test-automation/README.md +136 -0
  11. package/dist/templates/browser-test-automation/chat.mjs +36 -0
  12. package/dist/templates/browser-test-automation/graph.mjs +54 -0
  13. package/dist/templates/browser-test-automation/nodes/execute-live.mjs +222 -0
  14. package/dist/templates/browser-test-automation/nodes/generate-script.mjs +97 -0
  15. package/dist/templates/browser-test-automation/nodes/index.mjs +3 -0
  16. package/dist/templates/browser-test-automation/nodes/preflight.mjs +59 -0
  17. package/dist/templates/browser-test-automation/nodes/utils.mjs +297 -0
  18. package/dist/templates/browser-test-automation/pipeline-ids.js +12 -0
  19. package/dist/templates/browser-test-automation/result-handler.mjs +327 -0
  20. package/dist/templates/browser-test-automation/run-index.mjs +420 -0
  21. package/dist/templates/browser-test-automation/run_test.json +358 -0
  22. package/dist/templates/code-analysis/graph.js +72 -0
  23. package/dist/templates/code-analysis/index.js +18 -0
  24. package/dist/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  25. package/dist/templates/code-analysis/nodes/create-pr-node.js +175 -0
  26. package/dist/templates/code-analysis/nodes/finalize-node.js +118 -0
  27. package/dist/templates/code-analysis/nodes/generate-code-node.js +425 -0
  28. package/dist/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  29. package/dist/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  30. package/dist/templates/code-analysis/nodes/setup-node.js +142 -0
  31. package/dist/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  32. package/dist/templates/code-analysis/prompts/generate-code.md +33 -0
  33. package/dist/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  34. package/dist/templates/code-analysis/state.js +40 -0
  35. package/dist/templates/code-implementation/graph.js +35 -0
  36. package/dist/templates/code-implementation/index.js +7 -0
  37. package/dist/templates/code-implementation/state.js +14 -0
  38. package/dist/templates/global-setup.js +56 -0
  39. package/dist/templates/index.js +94 -0
  40. package/dist/templates/register-nodes.js +24 -0
  41. package/dist/utils/run-index-post-cli.js +4 -4
  42. package/package.json +2 -2
  43. package/templates/browser-test-automation/run-index.mjs +4 -2
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Browser test automation — run-index.jsonl rows (execute_live / generate_script layout).
3
+ * Used by CLI runTest; generic JSONL I/O is in @zibby/core/utils/run-registry.js.
4
+ */
5
+
6
+ import { existsSync, readdirSync, statSync } from 'fs';
7
+ import { join, relative, sep, resolve as pathResolve } from 'path';
8
+ import {
9
+ appendRunIndexRecord,
10
+ readRunIndexRecordsFromFile,
11
+ resolveRunIndexPath,
12
+ } from '../../src/utils/run-registry.js';
13
+ import { mergeSessionRunState, readSessionRunState } from '../../src/utils/run-state-session.js';
14
+ import { partitionRunIndexBySession, runIndexSessionEntryIsLive } from '../../src/utils/run-index-merge.js';
15
+ import { DEFAULT_OUTPUT_BASE, SESSIONS_DIR } from '@zibby/agent-workflow';
16
+ import { BROWSER_TEST_PIPELINE_NODE_IDS } from './pipeline-ids.js';
17
+
18
+ export { BROWSER_TEST_PIPELINE_NODE_IDS };
19
+
20
+ /** Stable id for Studio + run-index: Studio env, else session folder id (CLI/chat). */
21
+ function runIndexCorrelationId(sessionId) {
22
+ const env = process.env.ZIBBY_STUDIO_TEST_CASE_ID;
23
+ if (env != null && String(env).trim() !== '') return String(env).trim();
24
+ return sessionId != null ? String(sessionId) : '';
25
+ }
26
+
27
+ const SCRIPT_CANDIDATES = [
28
+ join('generate_script', 'generated-test.spec.js'),
29
+ join('generate_script', 'generated-test.spec.ts'),
30
+ join('generate_script', 'playwright.spec.ts'),
31
+ join('generate_script', 'test.spec.ts'),
32
+ ];
33
+
34
+ function firstExistingVideo(sessionAbs) {
35
+ const dirs = [
36
+ join(sessionAbs, 'execute_live', 'videos'),
37
+ join(sessionAbs, 'execute_live'),
38
+ sessionAbs,
39
+ ];
40
+ for (const d of dirs) {
41
+ if (!existsSync(d)) continue;
42
+ let names;
43
+ try {
44
+ names = readdirSync(d);
45
+ } catch {
46
+ continue;
47
+ }
48
+ const webm = names.find((f) => f.endsWith('.webm'));
49
+ if (webm) return join(d, webm);
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function firstExistingEvents(sessionAbs) {
55
+ const c = [join(sessionAbs, 'execute_live', 'events.json'), join(sessionAbs, 'events.json')];
56
+ for (const p of c) {
57
+ if (existsSync(p)) return p;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function firstExistingScript(sessionAbs) {
63
+ for (const rel of SCRIPT_CANDIDATES) {
64
+ const p = join(sessionAbs, rel);
65
+ if (existsSync(p)) return p;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Discover absolute artifact paths for a browser-test session directory.
72
+ * @param {string} sessionPathAbs
73
+ */
74
+ export function discoverBrowserTestSessionArtifacts(sessionPathAbs) {
75
+ if (!sessionPathAbs || !existsSync(sessionPathAbs)) {
76
+ return { videoPathAbs: null, eventsPathAbs: null, scriptPathAbs: null };
77
+ }
78
+ return {
79
+ videoPathAbs: firstExistingVideo(sessionPathAbs),
80
+ eventsPathAbs: firstExistingEvents(sessionPathAbs),
81
+ scriptPathAbs: firstExistingScript(sessionPathAbs),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * @param {object} opts
87
+ * @param {string} opts.cwd
88
+ * @param {object} opts.result - return value from agent.run / runSingleNode
89
+ * @param {boolean} opts.success
90
+ * @param {string} [opts.outputBase]
91
+ * @param {string} [opts.specPath]
92
+ * @param {string} [opts.errorMessage]
93
+ * @param {string} [opts.status] — override default completed/failed (e.g. `interrupted`)
94
+ */
95
+ export function buildBrowserTestRunIndexRecord(opts) {
96
+ const cwd = opts.cwd || process.cwd();
97
+ const outputBase = opts.outputBase || DEFAULT_OUTPUT_BASE;
98
+ const result = opts.result || {};
99
+ const state = result.state || {};
100
+ const sessionPathAbs = state.sessionPath;
101
+ if (!sessionPathAbs || typeof sessionPathAbs !== 'string') {
102
+ return null;
103
+ }
104
+
105
+ const sessionId = sessionPathAbs.split(/[/\\]/).filter(Boolean).pop();
106
+ if (!sessionId) return null;
107
+
108
+ const { videoPathAbs, eventsPathAbs, scriptPathAbs } =
109
+ discoverBrowserTestSessionArtifacts(sessionPathAbs);
110
+
111
+ const toRel = (abs) => {
112
+ if (!abs) return null;
113
+ try {
114
+ return relative(cwd, abs).split(sep).join('/');
115
+ } catch {
116
+ return null;
117
+ }
118
+ };
119
+
120
+ let specRel = null;
121
+ if (opts.specPath) {
122
+ try {
123
+ const absSpec = pathResolve(cwd, opts.specPath);
124
+ specRel = relative(cwd, absSpec).split(sep).join('/');
125
+ } catch {
126
+ specRel = String(opts.specPath).split(sep).join('/');
127
+ }
128
+ }
129
+
130
+ return {
131
+ v: 1,
132
+ recordKind: 'summary',
133
+ ts: Date.now(),
134
+ sessionId,
135
+ status: opts.status ?? (opts.success ? 'completed' : 'failed'),
136
+ cwd,
137
+ outputBase,
138
+ sessionPathAbs,
139
+ sessionDirRel: toRel(sessionPathAbs),
140
+ videoPathAbs: videoPathAbs || null,
141
+ eventsPathAbs: eventsPathAbs || null,
142
+ scriptPathAbs: scriptPathAbs || null,
143
+ videoRel: toRel(videoPathAbs),
144
+ eventsRel: toRel(eventsPathAbs),
145
+ scriptRel: toRel(scriptPathAbs),
146
+ specRel,
147
+ source: process.env.ZIBBY_RUN_SOURCE || 'cli',
148
+ studioTestCaseId: runIndexCorrelationId(sessionId) || null,
149
+ errorMessage: opts.errorMessage || null,
150
+ };
151
+ }
152
+
153
+ export function tryAppendBrowserTestRunIndex({
154
+ cwd,
155
+ config,
156
+ result,
157
+ success,
158
+ specPath,
159
+ errorMessage,
160
+ }) {
161
+ try {
162
+ const record = buildBrowserTestRunIndexRecord({
163
+ cwd: cwd || process.cwd(),
164
+ result,
165
+ success,
166
+ outputBase: config?.paths?.output || DEFAULT_OUTPUT_BASE,
167
+ specPath,
168
+ errorMessage,
169
+ });
170
+ if (record) {
171
+ appendRunIndexRecord(record);
172
+ if (record.sessionPathAbs) {
173
+ mergeSessionRunState(record.sessionPathAbs, {
174
+ sessionId: record.sessionId,
175
+ studioTestCaseId: record.studioTestCaseId || record.sessionId,
176
+ status: record.status,
177
+ activeNode: null,
178
+ activeStageIndex: null,
179
+ errorMessage: record.errorMessage || null,
180
+ runSource: record.source || 'cli',
181
+ cwd: record.cwd,
182
+ outputBase: record.outputBase,
183
+ sessionPathAbs: record.sessionPathAbs,
184
+ });
185
+ }
186
+ }
187
+ } catch (e) {
188
+ console.warn(`[zibby browser-test run-index] ${e.message}`);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Append one JSONL row when a pipeline node starts (live Mission Control).
194
+ * @param {object} payload
195
+ * @param {string} payload.currentNode — graph node id
196
+ * @param {string} payload.sessionPath — absolute session dir
197
+ * @param {string} [payload.sessionId]
198
+ * @param {string} [payload.cwd]
199
+ * @param {string} [payload.outputBase]
200
+ */
201
+ /**
202
+ * Single absolute session directory for pipeline progress + zibby-run-state.json.
203
+ * Studio spawns the CLI with ZIBBY_SESSION_PATH; cwd may still be wrong (home, app bundle, etc.).
204
+ * Never merge under join(cwd, …/sessions/id) when studio env points elsewhere — that caused duplicate dirs.
205
+ */
206
+ export function resolveBrowserTestPipelineSessionPathAbs({
207
+ sessionPath,
208
+ sessionId,
209
+ cwd,
210
+ outputBase = DEFAULT_OUTPUT_BASE,
211
+ } = {}) {
212
+ const cwd0 = cwd || process.cwd();
213
+ const ob = outputBase || DEFAULT_OUTPUT_BASE;
214
+ const sid =
215
+ sessionId != null && String(sessionId).trim() !== ''
216
+ ? String(sessionId).trim()
217
+ : null;
218
+
219
+ const pinned =
220
+ process.env.ZIBBY_PIN_SESSION_PATH === '1' ||
221
+ process.env.ZIBBY_PIN_SESSION_PATH === 'true';
222
+ const envSession = process.env.ZIBBY_SESSION_PATH && String(process.env.ZIBBY_SESSION_PATH).trim();
223
+ if (pinned && envSession) {
224
+ return pathResolve(envSession);
225
+ }
226
+
227
+ const trimmed = sessionPath && String(sessionPath).trim();
228
+ if (trimmed) {
229
+ return pathResolve(trimmed);
230
+ }
231
+
232
+ const sessionsRoot = process.env.ZIBBY_SESSIONS_ROOT && String(process.env.ZIBBY_SESSIONS_ROOT).trim();
233
+ if (sessionsRoot && sid) {
234
+ return pathResolve(join(sessionsRoot, sid));
235
+ }
236
+
237
+ if (process.env.ZIBBY_SESSION_PATH && String(process.env.ZIBBY_SESSION_PATH).trim()) {
238
+ return pathResolve(String(process.env.ZIBBY_SESSION_PATH).trim());
239
+ }
240
+
241
+ return pathResolve(join(cwd0, ob, SESSIONS_DIR, sid || 'invalid'));
242
+ }
243
+
244
+ export function tryAppendBrowserTestPipelineProgress(payload) {
245
+ try {
246
+ const currentNode = payload?.currentNode;
247
+ if (!currentNode || !BROWSER_TEST_PIPELINE_NODE_IDS.includes(currentNode)) return;
248
+
249
+ const sessionPath = payload.sessionPath;
250
+ const sessionId =
251
+ payload.sessionId ||
252
+ (sessionPath && String(sessionPath).split(/[/\\]/).filter(Boolean).pop()) ||
253
+ null;
254
+ if (!sessionId) return;
255
+
256
+ const cwd = payload.cwd || process.cwd();
257
+ const outputBase = payload.outputBase || DEFAULT_OUTPUT_BASE;
258
+ const activeStageIndex = BROWSER_TEST_PIPELINE_NODE_IDS.indexOf(currentNode);
259
+ const specPathRaw = payload?.specPath != null ? String(payload.specPath).trim() : '';
260
+ const taskDescription =
261
+ payload?.taskDescription != null ? String(payload.taskDescription) : '';
262
+ let specRel = null;
263
+ if (specPathRaw) {
264
+ try {
265
+ const absSpec = pathResolve(cwd, specPathRaw);
266
+ specRel = relative(cwd, absSpec).split(sep).join('/');
267
+ } catch {
268
+ specRel = specPathRaw.split(sep).join('/');
269
+ }
270
+ }
271
+
272
+ const sessionPathAbs = resolveBrowserTestPipelineSessionPathAbs({
273
+ sessionPath,
274
+ sessionId,
275
+ cwd,
276
+ outputBase,
277
+ });
278
+
279
+ appendRunIndexRecord({
280
+ v: 1,
281
+ recordKind: 'progress',
282
+ ts: Date.now(),
283
+ sessionId,
284
+ cwd,
285
+ outputBase,
286
+ sessionPathAbs,
287
+ activeNode: currentNode,
288
+ activeStageIndex,
289
+ specRel,
290
+ taskDescription: taskDescription || null,
291
+ studioTestCaseId: runIndexCorrelationId(sessionId) || null,
292
+ source: process.env.ZIBBY_RUN_SOURCE || 'cli',
293
+ });
294
+
295
+ mergeSessionRunState(sessionPathAbs, {
296
+ sessionId,
297
+ studioTestCaseId: runIndexCorrelationId(sessionId) || sessionId,
298
+ status: 'running',
299
+ activeNode: currentNode,
300
+ activeStageIndex,
301
+ sessionPathAbs,
302
+ cwd,
303
+ outputBase,
304
+ specPath: specRel || null,
305
+ task: taskDescription || null,
306
+ taskDescription: taskDescription || null,
307
+ runSource: process.env.ZIBBY_RUN_SOURCE || 'cli',
308
+ pid: typeof process.pid === 'number' ? process.pid : null,
309
+ });
310
+ } catch (e) {
311
+ console.warn(`[zibby browser-test run-index progress] ${e.message}`);
312
+ }
313
+ }
314
+
315
+ /** Factory for graph.run `initialState.onPipelineProgress`. */
316
+ export function createBrowserTestPipelineProgressAppender({ cwd, config } = {}) {
317
+ const cwd0 = cwd || process.cwd();
318
+ const ob0 = config?.paths?.output || DEFAULT_OUTPUT_BASE;
319
+ return (payload) => {
320
+ tryAppendBrowserTestPipelineProgress({
321
+ cwd: payload?.cwd || cwd0,
322
+ outputBase: payload?.outputBase || ob0,
323
+ sessionPath: payload?.sessionPath,
324
+ sessionId: payload?.sessionId,
325
+ currentNode: payload?.currentNode,
326
+ specPath: payload?.specPath,
327
+ taskDescription: payload?.taskDescription,
328
+ });
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Append a terminal `summary` row for every session that still looks “live” in run-index (progress newer than summary).
334
+ * Call from SIGINT/SIGTERM so Mission Control clears after Ctrl+C in the terminal.
335
+ * @param {object} [opts]
336
+ * @param {string} [opts.cwd]
337
+ * @param {object} [opts.config] — uses `config.paths.output`
338
+ */
339
+ export function tryAppendBrowserTestInterruptedRunIndex(opts = {}) {
340
+ try {
341
+ const cwd0 = opts.cwd || process.cwd();
342
+ const outputBase = opts.config?.paths?.output || opts.outputBase || DEFAULT_OUTPUT_BASE;
343
+ const indexPath = resolveRunIndexPath(cwd0, outputBase);
344
+ const records = readRunIndexRecordsFromFile(indexPath);
345
+ const partitioned = partitionRunIndexBySession(records);
346
+ const handled = new Set();
347
+
348
+ const errMsg =
349
+ opts.errorMessage ||
350
+ 'Run stopped (SIGINT/SIGTERM) before a normal summary was written.';
351
+
352
+ const flushInterrupted = (sessionId, sessionPathAbs) => {
353
+ if (!sessionId || !sessionPathAbs) return;
354
+ if (handled.has(sessionId)) return;
355
+ handled.add(sessionId);
356
+ const record = buildBrowserTestRunIndexRecord({
357
+ cwd: cwd0,
358
+ outputBase,
359
+ result: { state: { sessionPath: sessionPathAbs } },
360
+ success: false,
361
+ specPath: null,
362
+ status: 'interrupted',
363
+ errorMessage: errMsg,
364
+ });
365
+ if (record) {
366
+ appendRunIndexRecord(record);
367
+ mergeSessionRunState(sessionPathAbs, {
368
+ sessionId,
369
+ studioTestCaseId: record.studioTestCaseId || sessionId,
370
+ status: 'interrupted',
371
+ activeNode: null,
372
+ activeStageIndex: null,
373
+ errorMessage: record.errorMessage || null,
374
+ runSource: record.source || 'cli',
375
+ cwd: cwd0,
376
+ outputBase,
377
+ sessionPathAbs,
378
+ });
379
+ }
380
+ };
381
+
382
+ // 1) JSONL “live” sessions (progress newer than summary)
383
+ for (const [folderSessionId, entry] of partitioned) {
384
+ if (!runIndexSessionEntryIsLive(entry)) continue;
385
+ const prog = entry.progress;
386
+ if (!prog) continue;
387
+ const sessionId = String(folderSessionId);
388
+ const sessionPathAbs =
389
+ (prog.sessionPathAbs && String(prog.sessionPathAbs)) ||
390
+ join(cwd0, outputBase, SESSIONS_DIR, sessionId);
391
+ flushInterrupted(sessionId, sessionPathAbs);
392
+ }
393
+
394
+ // 2) Disk-only live: Studio (or early CLI) wrote zibby-run-state.json as running before any
395
+ // run-index progress line — JSONL partition omits them, so Ctrl+C must clear by scanning sessions/.
396
+ const sessionsRoot = join(cwd0, outputBase, SESSIONS_DIR);
397
+ if (!existsSync(sessionsRoot)) return;
398
+ let names;
399
+ try {
400
+ names = readdirSync(sessionsRoot);
401
+ } catch {
402
+ return;
403
+ }
404
+ for (const name of names) {
405
+ const sessionPathAbs = join(sessionsRoot, name);
406
+ let st;
407
+ try {
408
+ st = statSync(sessionPathAbs);
409
+ } catch {
410
+ continue;
411
+ }
412
+ if (!st.isDirectory()) continue;
413
+ const doc = readSessionRunState(sessionPathAbs);
414
+ if (!doc || doc.status !== 'running') continue;
415
+ flushInterrupted(String(name), sessionPathAbs);
416
+ }
417
+ } catch (e) {
418
+ console.warn(`[zibby browser-test run-index interrupt] ${e.message}`);
419
+ }
420
+ }