donobu 5.26.0 → 5.27.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 (87) hide show
  1. package/dist/cli/donobu-cli.js +203 -251
  2. package/dist/codegen/CodeGenerator.js +12 -16
  3. package/dist/esm/cli/donobu-cli.js +203 -251
  4. package/dist/esm/codegen/CodeGenerator.js +12 -16
  5. package/dist/esm/managers/DonobuFlowsManager.js +2 -1
  6. package/dist/esm/managers/TestsManager.js +2 -2
  7. package/dist/esm/models/CreateTest.d.ts +1 -1
  8. package/dist/esm/models/CreateTest.js +6 -0
  9. package/dist/esm/persistence/DonobuSqliteDb.js +102 -0
  10. package/dist/esm/persistence/TestConfigHash.d.ts +11 -0
  11. package/dist/esm/persistence/TestConfigHash.js +31 -0
  12. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts +0 -9
  13. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +4 -33
  14. package/dist/esm/persistence/normalizeFlowMetadata.d.ts +16 -0
  15. package/dist/esm/persistence/normalizeFlowMetadata.js +34 -0
  16. package/dist/esm/reporter/buildReport.d.ts +22 -0
  17. package/dist/esm/reporter/buildReport.js +106 -0
  18. package/dist/esm/reporter/html.d.ts +5 -9
  19. package/dist/esm/reporter/html.js +25 -101
  20. package/dist/esm/reporter/markdown.d.ts +33 -0
  21. package/dist/esm/reporter/markdown.js +62 -0
  22. package/dist/esm/reporter/merge.d.ts +33 -0
  23. package/dist/esm/reporter/merge.js +229 -0
  24. package/dist/esm/reporter/model.d.ts +101 -0
  25. package/dist/esm/reporter/model.js +27 -0
  26. package/dist/{cli/playwright-json-to-html.d.ts → esm/reporter/render.d.ts} +9 -14
  27. package/dist/esm/{cli/playwright-json-to-html.js → reporter/render.js} +52 -152
  28. package/dist/esm/reporter/renderMarkdown.d.ts +11 -0
  29. package/dist/esm/{cli/playwright-json-to-markdown.js → reporter/renderMarkdown.js} +31 -110
  30. package/dist/esm/reporter/renderSlack.d.ts +17 -0
  31. package/dist/esm/reporter/renderSlack.js +100 -0
  32. package/dist/esm/reporter/reportWalk.d.ts +28 -0
  33. package/dist/esm/reporter/reportWalk.js +61 -0
  34. package/dist/esm/reporter/slack.d.ts +93 -0
  35. package/dist/esm/reporter/slack.js +150 -0
  36. package/dist/esm/reporter/stateFile.d.ts +31 -0
  37. package/dist/esm/reporter/stateFile.js +70 -0
  38. package/dist/esm/tools/AssertPageTool.d.ts +2 -2
  39. package/dist/esm/utils/MiscUtils.d.ts +0 -13
  40. package/dist/esm/utils/MiscUtils.js +0 -21
  41. package/dist/esm/utils/displayName.d.ts +16 -0
  42. package/dist/esm/utils/displayName.js +28 -0
  43. package/dist/managers/DonobuFlowsManager.js +2 -1
  44. package/dist/managers/TestsManager.js +2 -2
  45. package/dist/models/CreateTest.d.ts +1 -1
  46. package/dist/models/CreateTest.js +6 -0
  47. package/dist/persistence/DonobuSqliteDb.js +102 -0
  48. package/dist/persistence/TestConfigHash.d.ts +11 -0
  49. package/dist/persistence/TestConfigHash.js +31 -0
  50. package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts +0 -9
  51. package/dist/persistence/flows/FlowsPersistenceSqlite.js +4 -33
  52. package/dist/persistence/normalizeFlowMetadata.d.ts +16 -0
  53. package/dist/persistence/normalizeFlowMetadata.js +34 -0
  54. package/dist/reporter/buildReport.d.ts +22 -0
  55. package/dist/reporter/buildReport.js +106 -0
  56. package/dist/reporter/html.d.ts +5 -9
  57. package/dist/reporter/html.js +25 -101
  58. package/dist/reporter/markdown.d.ts +33 -0
  59. package/dist/reporter/markdown.js +62 -0
  60. package/dist/reporter/merge.d.ts +33 -0
  61. package/dist/reporter/merge.js +229 -0
  62. package/dist/reporter/model.d.ts +101 -0
  63. package/dist/reporter/model.js +27 -0
  64. package/dist/{esm/cli/playwright-json-to-html.d.ts → reporter/render.d.ts} +9 -14
  65. package/dist/{cli/playwright-json-to-html.js → reporter/render.js} +52 -152
  66. package/dist/reporter/renderMarkdown.d.ts +11 -0
  67. package/dist/{cli/playwright-json-to-markdown.js → reporter/renderMarkdown.js} +31 -110
  68. package/dist/reporter/renderSlack.d.ts +17 -0
  69. package/dist/reporter/renderSlack.js +100 -0
  70. package/dist/reporter/reportWalk.d.ts +28 -0
  71. package/dist/reporter/reportWalk.js +61 -0
  72. package/dist/reporter/slack.d.ts +93 -0
  73. package/dist/reporter/slack.js +150 -0
  74. package/dist/reporter/stateFile.d.ts +31 -0
  75. package/dist/reporter/stateFile.js +70 -0
  76. package/dist/tools/AssertPageTool.d.ts +2 -2
  77. package/dist/utils/MiscUtils.d.ts +0 -13
  78. package/dist/utils/MiscUtils.js +0 -21
  79. package/dist/utils/displayName.d.ts +16 -0
  80. package/dist/utils/displayName.js +28 -0
  81. package/package.json +11 -5
  82. package/dist/cli/playwright-json-to-markdown.d.ts +0 -43
  83. package/dist/cli/playwright-json-to-slack-json.d.ts +0 -3
  84. package/dist/cli/playwright-json-to-slack-json.js +0 -214
  85. package/dist/esm/cli/playwright-json-to-markdown.d.ts +0 -43
  86. package/dist/esm/cli/playwright-json-to-slack-json.d.ts +0 -3
  87. package/dist/esm/cli/playwright-json-to-slack-json.js +0 -214
@@ -4,7 +4,7 @@ exports.TestsManager = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const CannotDeleteRunningFlowException_1 = require("../exceptions/CannotDeleteRunningFlowException");
6
6
  const TestNotFoundException_1 = require("../exceptions/TestNotFoundException");
7
- const MiscUtils_1 = require("../utils/MiscUtils");
7
+ const displayName_1 = require("../utils/displayName");
8
8
  const FederatedPagination_1 = require("./FederatedPagination");
9
9
  class TestsManager {
10
10
  constructor(testsPersistenceRegistry, flowsManager) {
@@ -22,7 +22,7 @@ class TestsManager {
22
22
  const testMetadata = {
23
23
  id: testId,
24
24
  metadataVersion: 1,
25
- name: MiscUtils_1.MiscUtils.getDisplayName({ name: params.name ?? null, web }),
25
+ name: (0, displayName_1.getDisplayName)({ name: params.name ?? null, web }),
26
26
  suiteId: params.suiteId ?? null,
27
27
  nextRunMode: params.nextRunMode ?? 'AUTONOMOUS',
28
28
  target: params.target,
@@ -13,7 +13,6 @@ export declare const CreateTestSchema: z.ZodObject<{
13
13
  javascript: z.ZodString;
14
14
  }, z.core.$strip>>>>;
15
15
  overallObjective: z.ZodOptional<z.ZodNullable<z.ZodString>>;
16
- allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
16
  resultJsonSchema: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
18
17
  callbackUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
18
  maxToolCalls: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
@@ -140,6 +139,7 @@ export declare const CreateTestSchema: z.ZodObject<{
140
139
  INSTRUCT: "INSTRUCT";
141
140
  DETERMINISTIC: "DETERMINISTIC";
142
141
  }>>>;
142
+ allowedTools: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
143
143
  }, z.core.$loose>;
144
144
  export type CreateTest = z.infer<typeof CreateTestSchema>;
145
145
  //# sourceMappingURL=CreateTest.d.ts.map
@@ -36,6 +36,12 @@ exports.CreateTestSchema = RunConfig_1.RunConfigSchema.partial()
36
36
  nextRunMode: RunMode_1.RunModeSchema.nullable()
37
37
  .optional()
38
38
  .describe('The run mode to use when creating flows from this test. Defaults to AUTONOMOUS.'),
39
+ // `allowedTools` is the only field in RunConfig that isn't already
40
+ // nullable at the base, so `.partial()` above only adds `.optional()`
41
+ // (accepts `undefined`, not `null`). Manual-test creation sends `null`
42
+ // here — mirror the nullable override that CreateDonobuFlowSchema uses
43
+ // so the two creation endpoints accept the same payload shape.
44
+ allowedTools: RunConfig_1.RunConfigSchema.shape.allowedTools.nullable().optional(),
39
45
  })
40
46
  .loose();
41
47
  //# sourceMappingURL=CreateTest.js.map
@@ -42,8 +42,13 @@ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
42
42
  const fs = __importStar(require("fs/promises"));
43
43
  const path_1 = __importDefault(require("path"));
44
44
  const LockFile = __importStar(require("proper-lockfile"));
45
+ const v4_1 = require("zod/v4");
46
+ const FlowMetadata_1 = require("../models/FlowMetadata");
47
+ const displayName_1 = require("../utils/displayName");
45
48
  const Logger_1 = require("../utils/Logger");
46
49
  const MiscUtils_1 = require("../utils/MiscUtils");
50
+ const normalizeFlowMetadata_1 = require("./normalizeFlowMetadata");
51
+ const TestConfigHash_1 = require("./TestConfigHash");
47
52
  let instance = null;
48
53
  /**
49
54
  * ##################################################################
@@ -415,6 +420,103 @@ CREATE INDEX IF NOT EXISTS idx_ai_queries_flow_id_started_at ON ai_queries(flow_
415
420
  `);
416
421
  },
417
422
  },
423
+ {
424
+ // Back-fill tests for orphan flows (test_id IS NULL). Flows with identical
425
+ // RunConfig-derived fields are grouped under a single new test whose id is
426
+ // the MD5 hash of the canonical-sorted config JSON. See TestConfigHash.ts.
427
+ version: 12,
428
+ up: (db) => {
429
+ // 1. Fetch all flows with null test_id, oldest first.
430
+ const flows = v4_1.z
431
+ .array(v4_1.z.object({ id: v4_1.z.string(), metadata: v4_1.z.string() }))
432
+ .parse(db
433
+ .prepare('SELECT id, metadata FROM flow_metadata WHERE test_id IS NULL ORDER BY created_at ASC')
434
+ .all());
435
+ if (flows.length === 0) {
436
+ return;
437
+ }
438
+ const groups = new Map();
439
+ for (const flow of flows) {
440
+ try {
441
+ const metadata = FlowMetadata_1.FlowMetadataSchema.parse((0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(flow.metadata)));
442
+ const hash = (0, TestConfigHash_1.hashTestConfig)(metadata);
443
+ const groupedFlow = { id: flow.id, metadata };
444
+ const existing = groups.get(hash);
445
+ if (existing) {
446
+ existing.flows.push(groupedFlow);
447
+ }
448
+ else {
449
+ groups.set(hash, { flows: [groupedFlow] });
450
+ }
451
+ }
452
+ catch (error) {
453
+ // Fail open: skip flows we can't parse/normalize so one bad row
454
+ // doesn't block the rest of the migration.
455
+ Logger_1.appLogger.error(`Migration 12: skipping flow ${flow.id} during grouping:`, error);
456
+ }
457
+ }
458
+ // 4. Create a test for each group.
459
+ const insertTestStmt = db.prepare(`
460
+ INSERT INTO test_metadata (id, name, metadata, created_at, suite_id, next_run_mode)
461
+ VALUES (@id, @name, @metadata, @createdAt, @suiteId, @nextRunMode)
462
+ `);
463
+ const updateFlowStmt = db.prepare('UPDATE flow_metadata SET test_id = ?, metadata = ? WHERE id = ?');
464
+ for (const [hash, group] of groups) {
465
+ try {
466
+ // Flows were inserted oldest-first, so the last entry is newest.
467
+ const newest = group.flows[group.flows.length - 1].metadata;
468
+ // `maxToolCalls` is null on every deterministic flow, so prefer the
469
+ // most recent autonomous flow's value. Fall back to the newest flow's
470
+ // value (likely null) if no autonomous flow exists in the group.
471
+ const newestAutonomous = [...group.flows]
472
+ .reverse()
473
+ .find((f) => f.metadata.runMode === 'AUTONOMOUS')?.metadata;
474
+ const maxToolCalls = newestAutonomous?.maxToolCalls ?? newest.maxToolCalls;
475
+ const testName = (0, displayName_1.getDisplayName)(newest, 'Untitled Test');
476
+ const testMetadata = {
477
+ id: hash,
478
+ name: testName,
479
+ target: newest.target,
480
+ web: newest.web,
481
+ envVars: newest.envVars,
482
+ customTools: newest.customTools,
483
+ overallObjective: newest.overallObjective,
484
+ allowedTools: newest.allowedTools,
485
+ resultJsonSchema: newest.resultJsonSchema,
486
+ callbackUrl: newest.callbackUrl,
487
+ videoDisabled: newest.videoDisabled,
488
+ maxToolCalls,
489
+ metadataVersion: newest.metadataVersion,
490
+ createdWithDonobuVersion: newest.createdWithDonobuVersion,
491
+ suiteId: null,
492
+ nextRunMode: 'DETERMINISTIC',
493
+ };
494
+ insertTestStmt.run({
495
+ id: hash,
496
+ name: testName,
497
+ metadata: JSON.stringify(testMetadata),
498
+ createdAt: Date.now(),
499
+ suiteId: null,
500
+ nextRunMode: 'DETERMINISTIC',
501
+ });
502
+ // 5. Link each flow to its test (both test_id column and testId in JSON).
503
+ for (const groupedFlow of group.flows) {
504
+ try {
505
+ groupedFlow.metadata.testId = hash;
506
+ updateFlowStmt.run(hash, JSON.stringify(groupedFlow.metadata), groupedFlow.id);
507
+ }
508
+ catch (error) {
509
+ Logger_1.appLogger.error(`Migration 12: failed to link flow ${groupedFlow.id} to test ${hash}:`, error);
510
+ }
511
+ }
512
+ }
513
+ catch (error) {
514
+ // Fail open: skip groups we can't materialize as tests.
515
+ Logger_1.appLogger.error(`Migration 12: skipping test group ${hash}:`, error);
516
+ }
517
+ }
518
+ },
519
+ },
418
520
  ];
419
521
  /**
420
522
  * Create the SQL schema migrations table that can be used to manage table
@@ -0,0 +1,11 @@
1
+ import type { FlowMetadata } from '../models/FlowMetadata';
2
+ /**
3
+ * Deterministic MD5 of the RunConfig-derived slice of `metadata`, used to
4
+ * group orphan flows into a single back-filled test during migration v12 and
5
+ * the equivalent cloud ops script. `maxToolCalls` is excluded because it is
6
+ * null on deterministic flows and would otherwise split groups along run-mode
7
+ * boundaries. MD5 is fine here because the hash is used for grouping, not
8
+ * security.
9
+ */
10
+ export declare function hashTestConfig(metadata: FlowMetadata): string;
11
+ //# sourceMappingURL=TestConfigHash.d.ts.map
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hashTestConfig = hashTestConfig;
7
+ const crypto_1 = require("crypto");
8
+ const fast_json_stable_stringify_1 = __importDefault(require("fast-json-stable-stringify"));
9
+ /**
10
+ * Deterministic MD5 of the RunConfig-derived slice of `metadata`, used to
11
+ * group orphan flows into a single back-filled test during migration v12 and
12
+ * the equivalent cloud ops script. `maxToolCalls` is excluded because it is
13
+ * null on deterministic flows and would otherwise split groups along run-mode
14
+ * boundaries. MD5 is fine here because the hash is used for grouping, not
15
+ * security.
16
+ */
17
+ function hashTestConfig(metadata) {
18
+ const config = {
19
+ name: metadata.name,
20
+ target: metadata.target,
21
+ web: metadata.web,
22
+ envVars: metadata.envVars,
23
+ customTools: metadata.customTools,
24
+ overallObjective: metadata.overallObjective,
25
+ allowedTools: metadata.allowedTools,
26
+ resultJsonSchema: metadata.resultJsonSchema,
27
+ callbackUrl: metadata.callbackUrl,
28
+ };
29
+ return (0, crypto_1.createHash)('md5').update((0, fast_json_stable_stringify_1.default)(config)).digest('hex');
30
+ }
31
+ //# sourceMappingURL=TestConfigHash.js.map
@@ -6,15 +6,6 @@ import type { PaginatedResult } from '../../models/PaginatedResult';
6
6
  import type { ToolCall } from '../../models/ToolCall';
7
7
  import type { VideoSegment } from '../../models/VideoSegment';
8
8
  import type { FlowsPersistence } from './FlowsPersistence';
9
- /**
10
- * Normalizes flow metadata written by older SDK versions to the current schema.
11
- *
12
- * Rows written by SDK v5+ carry a `metadataVersion` field. Rows without it
13
- * were written by an older SDK and may use a legacy format (e.g. top-level
14
- * `browser` / `targetWebsite` instead of the `{ target, web }` wrapper
15
- * introduced in migration v9).
16
- */
17
- export declare function normalizeFlowMetadata(raw: Record<string, unknown>): FlowMetadata;
18
9
  /**
19
10
  * A persistence implementation that uses SQLite for all data storage, including binary files.
20
11
  */
@@ -1,40 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlowsPersistenceSqlite = void 0;
4
- exports.normalizeFlowMetadata = normalizeFlowMetadata;
5
4
  const crypto_1 = require("crypto");
6
5
  const FlowNotFoundException_1 = require("../../exceptions/FlowNotFoundException");
7
6
  const BrowserStorageState_1 = require("../../models/BrowserStorageState");
8
7
  const MiscUtils_1 = require("../../utils/MiscUtils");
9
- /**
10
- * Current metadata schema version. Bump this when the metadata JSON structure
11
- * changes in a way that requires read-time normalization.
12
- */
13
- const CURRENT_METADATA_VERSION = 1;
14
- /**
15
- * Normalizes flow metadata written by older SDK versions to the current schema.
16
- *
17
- * Rows written by SDK v5+ carry a `metadataVersion` field. Rows without it
18
- * were written by an older SDK and may use a legacy format (e.g. top-level
19
- * `browser` / `targetWebsite` instead of the `{ target, web }` wrapper
20
- * introduced in migration v9).
21
- */
22
- function normalizeFlowMetadata(raw) {
23
- if (raw.metadataVersion === CURRENT_METADATA_VERSION) {
24
- return raw;
25
- }
26
- // Pre-v5 format: `browser` and `targetWebsite` at the top level, no `target`.
27
- if (!raw.target && raw.browser) {
28
- raw.target = 'web';
29
- raw.web = {
30
- browser: raw.browser,
31
- targetWebsite: raw.targetWebsite ?? '',
32
- };
33
- delete raw.browser;
34
- delete raw.targetWebsite;
35
- }
36
- return raw;
37
- }
8
+ const normalizeFlowMetadata_1 = require("../normalizeFlowMetadata");
38
9
  /**
39
10
  * A persistence implementation that uses SQLite for all data storage, including binary files.
40
11
  */
@@ -111,7 +82,7 @@ class FlowsPersistenceSqlite {
111
82
  if (!row) {
112
83
  throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId);
113
84
  }
114
- return normalizeFlowMetadata(JSON.parse(row.metadata));
85
+ return (0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(row.metadata));
115
86
  }
116
87
  async getFlowMetadataByName(flowName) {
117
88
  const stmt = this.db.prepare('SELECT metadata FROM flow_metadata WHERE name = ? ORDER BY created_at DESC LIMIT 1');
@@ -119,7 +90,7 @@ class FlowsPersistenceSqlite {
119
90
  if (!row) {
120
91
  throw FlowNotFoundException_1.FlowNotFoundException.forName(flowName);
121
92
  }
122
- return normalizeFlowMetadata(JSON.parse(row.metadata));
93
+ return (0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(row.metadata));
123
94
  }
124
95
  async getFlowsMetadata(query) {
125
96
  // Sanitize inputs
@@ -186,7 +157,7 @@ class FlowsPersistenceSqlite {
186
157
  // Generate next token if needed
187
158
  const nextPageToken = hasMore ? `${offset + validLimit}` : undefined;
188
159
  return {
189
- items: results.map((row) => normalizeFlowMetadata(JSON.parse(row.metadata))),
160
+ items: results.map((row) => (0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(row.metadata))),
190
161
  nextPageToken,
191
162
  };
192
163
  }
@@ -0,0 +1,16 @@
1
+ import type { FlowMetadata } from '../models/FlowMetadata';
2
+ /**
3
+ * Current metadata schema version. Bump this when the metadata JSON structure
4
+ * changes in a way that requires read-time normalization.
5
+ */
6
+ export declare const CURRENT_METADATA_VERSION = 1;
7
+ /**
8
+ * Normalizes flow metadata written by older SDK versions to the current schema.
9
+ *
10
+ * Rows written by SDK v5+ carry a `metadataVersion` field. Rows without it
11
+ * were written by an older SDK and may use a legacy format (e.g. top-level
12
+ * `browser` / `targetWebsite` instead of the `{ target, web }` wrapper
13
+ * introduced in migration v9).
14
+ */
15
+ export declare function normalizeFlowMetadata(raw: Record<string, unknown>): FlowMetadata;
16
+ //# sourceMappingURL=normalizeFlowMetadata.d.ts.map
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CURRENT_METADATA_VERSION = void 0;
4
+ exports.normalizeFlowMetadata = normalizeFlowMetadata;
5
+ /**
6
+ * Current metadata schema version. Bump this when the metadata JSON structure
7
+ * changes in a way that requires read-time normalization.
8
+ */
9
+ exports.CURRENT_METADATA_VERSION = 1;
10
+ /**
11
+ * Normalizes flow metadata written by older SDK versions to the current schema.
12
+ *
13
+ * Rows written by SDK v5+ carry a `metadataVersion` field. Rows without it
14
+ * were written by an older SDK and may use a legacy format (e.g. top-level
15
+ * `browser` / `targetWebsite` instead of the `{ target, web }` wrapper
16
+ * introduced in migration v9).
17
+ */
18
+ function normalizeFlowMetadata(raw) {
19
+ if (raw.metadataVersion === exports.CURRENT_METADATA_VERSION) {
20
+ return raw;
21
+ }
22
+ // Pre-v5 format: `browser` and `targetWebsite` at the top level, no `target`.
23
+ if (!raw.target && raw.browser) {
24
+ raw.target = 'web';
25
+ raw.web = {
26
+ browser: raw.browser,
27
+ targetWebsite: raw.targetWebsite ?? '',
28
+ };
29
+ delete raw.browser;
30
+ delete raw.targetWebsite;
31
+ }
32
+ return raw;
33
+ }
34
+ //# sourceMappingURL=normalizeFlowMetadata.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview Reconstructs a `DonobuReport` from live Playwright `Reporter`
3
+ * events. Shared across all Donobu reporter classes (HTML, Markdown, Slack)
4
+ * so they all agree on the canonical shape sent downstream to the renderers
5
+ * and the auto-heal merge step.
6
+ *
7
+ * The output structure mirrors Playwright's native JSON reporter:
8
+ * suites (one per file) → specs (one per test title) → tests (one per project)
9
+ * plus the Donobu-specific `metadata` fields the merge + render steps rely on.
10
+ */
11
+ import type { TestCase, TestResult } from '@playwright/test/reporter';
12
+ import type { DonobuReport } from './model';
13
+ /**
14
+ * Build a canonical `DonobuReport` from the per-test result accumulators that
15
+ * each Donobu reporter maintains during a run.
16
+ *
17
+ * Callers are expected to fill in `metadata` (specifically the `donobuOutputs`
18
+ * entry for their format) before persisting the result — this helper owns the
19
+ * structure walk, not the output-path accounting.
20
+ */
21
+ export declare function buildDonobuReport(resultsByTest: Map<TestCase, TestResult[]>): DonobuReport;
22
+ //# sourceMappingURL=buildReport.d.ts.map
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Reconstructs a `DonobuReport` from live Playwright `Reporter`
4
+ * events. Shared across all Donobu reporter classes (HTML, Markdown, Slack)
5
+ * so they all agree on the canonical shape sent downstream to the renderers
6
+ * and the auto-heal merge step.
7
+ *
8
+ * The output structure mirrors Playwright's native JSON reporter:
9
+ * suites (one per file) → specs (one per test title) → tests (one per project)
10
+ * plus the Donobu-specific `metadata` fields the merge + render steps rely on.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.buildDonobuReport = buildDonobuReport;
14
+ /**
15
+ * Build a canonical `DonobuReport` from the per-test result accumulators that
16
+ * each Donobu reporter maintains during a run.
17
+ *
18
+ * Callers are expected to fill in `metadata` (specifically the `donobuOutputs`
19
+ * entry for their format) before persisting the result — this helper owns the
20
+ * structure walk, not the output-path accounting.
21
+ */
22
+ function buildDonobuReport(resultsByTest) {
23
+ // Group tests by file path, then by test title.
24
+ // Multiple TestCase objects with the same (file, title) represent the same
25
+ // spec running under different Playwright projects.
26
+ const byFile = new Map();
27
+ for (const test of resultsByTest.keys()) {
28
+ const file = test.location.file;
29
+ if (!byFile.has(file)) {
30
+ byFile.set(file, new Map());
31
+ }
32
+ const byTitle = byFile.get(file);
33
+ if (!byTitle.has(test.title)) {
34
+ byTitle.set(test.title, []);
35
+ }
36
+ byTitle.get(test.title).push(test);
37
+ }
38
+ const suites = [];
39
+ for (const [file, titleMap] of byFile) {
40
+ const specs = [];
41
+ for (const [title, tests] of titleMap) {
42
+ const testEntries = tests.map((test) => {
43
+ const results = resultsByTest.get(test) ?? [];
44
+ return {
45
+ annotations: test.annotations,
46
+ projectName: getProjectName(test),
47
+ // Signal "skipped" tests to the renderers the same way the JSON
48
+ // reporter does.
49
+ status: test.expectedStatus === 'skipped' ? 'skipped' : undefined,
50
+ results: results.map((r) => ({
51
+ status: r.status,
52
+ duration: r.duration,
53
+ retry: r.retry,
54
+ startTime: r.startTime?.toISOString() ?? null,
55
+ // Pass errors through; cast to any to preserve runtime-only
56
+ // fields (snippet, actual, expected) that Playwright adds to
57
+ // assertion errors.
58
+ errors: r.errors.map((e) => ({
59
+ message: e.message,
60
+ stack: e.stack,
61
+ snippet: e.snippet,
62
+ actual: e.actual,
63
+ expected: e.expected,
64
+ location: e.location,
65
+ })),
66
+ // The markdown renderer reads `result.error` (singular) in addition
67
+ // to `errors[]`; surface the first error there for back-compat.
68
+ error: r.errors[0]
69
+ ? {
70
+ message: r.errors[0].message,
71
+ stack: r.errors[0].stack,
72
+ snippet: r.errors[0].snippet,
73
+ }
74
+ : undefined,
75
+ attachments: r.attachments.map((a) => ({
76
+ name: a.name,
77
+ contentType: a.contentType,
78
+ path: a.path ?? null,
79
+ // Playwright Reporter API provides body as a Buffer; the HTML
80
+ // generator expects a base64-encoded string (matching JSON format).
81
+ body: a.body ? a.body.toString('base64') : undefined,
82
+ })),
83
+ // TestResult.stderr is already Array<{text?: string; buffer?: string}>,
84
+ // which is the same shape parseStderrSteps expects.
85
+ stderr: r.stderr,
86
+ })),
87
+ };
88
+ });
89
+ specs.push({ title, tests: testEntries });
90
+ }
91
+ suites.push({ file, specs });
92
+ }
93
+ return { suites, metadata: {} };
94
+ }
95
+ /** Walk up the suite chain to find the enclosing project suite's title. */
96
+ function getProjectName(test) {
97
+ let suite = test.parent;
98
+ while (suite) {
99
+ if (suite.type === 'project') {
100
+ return suite.title;
101
+ }
102
+ suite = suite.parent;
103
+ }
104
+ return '';
105
+ }
106
+ //# sourceMappingURL=buildReport.js.map
@@ -24,6 +24,11 @@
24
24
  * }],
25
25
  * ],
26
26
  * ```
27
+ *
28
+ * During an auto-heal rerun (`DONOBU_AUTO_HEAL_ACTIVE=1`) the reporter skips
29
+ * HTML generation. It always serializes its `DonobuReport` to a state file in
30
+ * the Playwright JSON output directory so the orchestrator can pick it up,
31
+ * merge it with the initial run's state, and re-render the HTML once.
27
32
  */
28
33
  import type { FullResult, Reporter, TestCase, TestResult } from '@playwright/test/reporter';
29
34
  export interface DonobuHtmlReporterOptions {
@@ -44,14 +49,5 @@ export default class DonobuHtmlReporter implements Reporter {
44
49
  onTestEnd(test: TestCase, result: TestResult): void;
45
50
  onEnd(_result: FullResult): Promise<void>;
46
51
  printsToStdio(): boolean;
47
- /**
48
- * Reconstruct the JSON structure that `generateHtml` / `extractTests` expects,
49
- * from the TestCase + TestResult objects collected during the run.
50
- *
51
- * Structure: suites (one per file) → specs (one per test title) → tests (one per project).
52
- */
53
- private buildJsonData;
54
- /** Walk up the suite chain to find the enclosing project suite's title. */
55
- private getProjectName;
56
52
  }
57
53
  //# sourceMappingURL=html.d.ts.map
@@ -25,11 +25,18 @@
25
25
  * }],
26
26
  * ],
27
27
  * ```
28
+ *
29
+ * During an auto-heal rerun (`DONOBU_AUTO_HEAL_ACTIVE=1`) the reporter skips
30
+ * HTML generation. It always serializes its `DonobuReport` to a state file in
31
+ * the Playwright JSON output directory so the orchestrator can pick it up,
32
+ * merge it with the initial run's state, and re-render the HTML once.
28
33
  */
29
34
  Object.defineProperty(exports, "__esModule", { value: true });
30
35
  const fs_1 = require("fs");
31
36
  const path_1 = require("path");
32
- const playwright_json_to_html_1 = require("../cli/playwright-json-to-html");
37
+ const buildReport_1 = require("./buildReport");
38
+ const render_1 = require("./render");
39
+ const stateFile_1 = require("./stateFile");
33
40
  class DonobuHtmlReporter {
34
41
  constructor(options = {}) {
35
42
  /** Accumulates all TestResult objects per TestCase (one per retry attempt). */
@@ -46,117 +53,34 @@ class DonobuHtmlReporter {
46
53
  }
47
54
  }
48
55
  async onEnd(_result) {
49
- // During an auto-heal rerun the CLI will regenerate the HTML from the
50
- // merged report (which combines both runs and includes self-heal context).
51
- // Generating here would overwrite the initial run's HTML with incomplete data.
52
- if (process.env.DONOBU_AUTO_HEAL_ACTIVE === '1') {
53
- return;
54
- }
55
56
  const outputFile = (0, path_1.resolve)(this.options.outputFile ?? 'test-results/index.html');
56
57
  const outputDir = (0, path_1.dirname)(outputFile);
58
+ const autoHealActive = process.env.DONOBU_AUTO_HEAL_ACTIVE === '1';
59
+ const report = (0, buildReport_1.buildDonobuReport)(this.resultsByTest);
60
+ // Persist the full report + this reporter's output entry to the shared
61
+ // state file so other reporters and the auto-heal orchestrator can find
62
+ // it. When multiple reporters run in the same process this merges each
63
+ // entry in without clobbering siblings.
64
+ (0, stateFile_1.mergeStateFileEntry)(process.env.PLAYWRIGHT_JSON_OUTPUT_DIR, report, {
65
+ html: { outputFile },
66
+ });
67
+ // During an auto-heal rerun the orchestrator re-renders the HTML from the
68
+ // merged report; writing it here would overwrite the initial HTML with
69
+ // incomplete data.
70
+ if (autoHealActive) {
71
+ return;
72
+ }
57
73
  const triage = this.options.triageDir
58
- ? (0, playwright_json_to_html_1.loadTriageData)((0, path_1.resolve)(this.options.triageDir))
74
+ ? (0, render_1.loadTriageData)((0, path_1.resolve)(this.options.triageDir))
59
75
  : { plans: [], evidence: [] };
60
- const jsonData = this.buildJsonData();
61
- const html = (0, playwright_json_to_html_1.generateHtml)(jsonData, triage, outputDir);
76
+ const html = (0, render_1.renderHtml)(report, triage, outputDir);
62
77
  (0, fs_1.mkdirSync)(outputDir, { recursive: true });
63
78
  (0, fs_1.writeFileSync)(outputFile, html, 'utf8');
64
79
  console.error(`Donobu report written to ${outputFile}`);
65
- // Write a sidecar so the CLI can find and regenerate this report from the
66
- // merged JSON after an auto-heal run completes.
67
- const playwrightOutputDir = process.env.PLAYWRIGHT_JSON_OUTPUT_DIR;
68
- if (playwrightOutputDir) {
69
- const sidecarPath = (0, path_1.join)(playwrightOutputDir, '.donobu-html-reporter.json');
70
- try {
71
- (0, fs_1.writeFileSync)(sidecarPath, JSON.stringify({ outputFile }), 'utf8');
72
- }
73
- catch {
74
- // Non-fatal — worst case the CLI falls back to no HTML regeneration.
75
- }
76
- }
77
80
  }
78
81
  printsToStdio() {
79
82
  return false;
80
83
  }
81
- /**
82
- * Reconstruct the JSON structure that `generateHtml` / `extractTests` expects,
83
- * from the TestCase + TestResult objects collected during the run.
84
- *
85
- * Structure: suites (one per file) → specs (one per test title) → tests (one per project).
86
- */
87
- buildJsonData() {
88
- // Group tests by file path, then by test title.
89
- // Multiple TestCase objects with the same (file, title) represent the same
90
- // spec running under different Playwright projects.
91
- const byFile = new Map();
92
- for (const test of this.resultsByTest.keys()) {
93
- const file = test.location.file;
94
- if (!byFile.has(file)) {
95
- byFile.set(file, new Map());
96
- }
97
- const byTitle = byFile.get(file);
98
- if (!byTitle.has(test.title)) {
99
- byTitle.set(test.title, []);
100
- }
101
- byTitle.get(test.title).push(test);
102
- }
103
- const suites = [];
104
- for (const [file, titleMap] of byFile) {
105
- const specs = [];
106
- for (const [title, tests] of titleMap) {
107
- const testEntries = tests.map((test) => {
108
- const results = this.resultsByTest.get(test) ?? [];
109
- return {
110
- annotations: test.annotations,
111
- projectName: this.getProjectName(test),
112
- // Signal "skipped" tests to extractTests the same way the JSON reporter does.
113
- status: test.expectedStatus === 'skipped' ? 'skipped' : undefined,
114
- results: results.map((r) => ({
115
- status: r.status,
116
- duration: r.duration,
117
- retry: r.retry,
118
- startTime: r.startTime?.toISOString() ?? null,
119
- // Pass errors through; cast to any to preserve runtime-only fields
120
- // (snippet, actual, expected) that Playwright adds to assertion errors.
121
- errors: r.errors.map((e) => ({
122
- message: e.message,
123
- stack: e.stack,
124
- snippet: e.snippet,
125
- actual: e.actual,
126
- expected: e.expected,
127
- location: e.location,
128
- })),
129
- attachments: r.attachments.map((a) => ({
130
- name: a.name,
131
- contentType: a.contentType,
132
- path: a.path ?? null,
133
- // Playwright Reporter API provides body as a Buffer; the HTML
134
- // generator expects a base64-encoded string (matching JSON format).
135
- body: a.body ? a.body.toString('base64') : undefined,
136
- })),
137
- // TestResult.stderr is already Array<{text?: string; buffer?: string}>,
138
- // which is the same shape parseStderrSteps expects.
139
- stderr: r.stderr,
140
- })),
141
- };
142
- });
143
- specs.push({ title, tests: testEntries });
144
- }
145
- suites.push({ file, specs });
146
- }
147
- return { suites, metadata: {} };
148
- }
149
- /** Walk up the suite chain to find the enclosing project suite's title. */
150
- getProjectName(test) {
151
- let suite = test.parent;
152
- while (suite) {
153
- if (suite.type === 'project') {
154
- return suite.title;
155
- }
156
- suite = suite.parent;
157
- }
158
- return '';
159
- }
160
84
  }
161
85
  exports.default = DonobuHtmlReporter;
162
86
  //# sourceMappingURL=html.js.map