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.
- package/dist/cli/donobu-cli.js +203 -251
- package/dist/codegen/CodeGenerator.js +12 -16
- package/dist/esm/cli/donobu-cli.js +203 -251
- package/dist/esm/codegen/CodeGenerator.js +12 -16
- package/dist/esm/managers/DonobuFlowsManager.js +2 -1
- package/dist/esm/managers/TestsManager.js +2 -2
- package/dist/esm/models/CreateTest.d.ts +1 -1
- package/dist/esm/models/CreateTest.js +6 -0
- package/dist/esm/persistence/DonobuSqliteDb.js +102 -0
- package/dist/esm/persistence/TestConfigHash.d.ts +11 -0
- package/dist/esm/persistence/TestConfigHash.js +31 -0
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts +0 -9
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +4 -33
- package/dist/esm/persistence/normalizeFlowMetadata.d.ts +16 -0
- package/dist/esm/persistence/normalizeFlowMetadata.js +34 -0
- package/dist/esm/reporter/buildReport.d.ts +22 -0
- package/dist/esm/reporter/buildReport.js +106 -0
- package/dist/esm/reporter/html.d.ts +5 -9
- package/dist/esm/reporter/html.js +25 -101
- package/dist/esm/reporter/markdown.d.ts +33 -0
- package/dist/esm/reporter/markdown.js +62 -0
- package/dist/esm/reporter/merge.d.ts +33 -0
- package/dist/esm/reporter/merge.js +229 -0
- package/dist/esm/reporter/model.d.ts +101 -0
- package/dist/esm/reporter/model.js +27 -0
- package/dist/{cli/playwright-json-to-html.d.ts → esm/reporter/render.d.ts} +9 -14
- package/dist/esm/{cli/playwright-json-to-html.js → reporter/render.js} +52 -152
- package/dist/esm/reporter/renderMarkdown.d.ts +11 -0
- package/dist/esm/{cli/playwright-json-to-markdown.js → reporter/renderMarkdown.js} +31 -110
- package/dist/esm/reporter/renderSlack.d.ts +17 -0
- package/dist/esm/reporter/renderSlack.js +100 -0
- package/dist/esm/reporter/reportWalk.d.ts +28 -0
- package/dist/esm/reporter/reportWalk.js +61 -0
- package/dist/esm/reporter/slack.d.ts +93 -0
- package/dist/esm/reporter/slack.js +150 -0
- package/dist/esm/reporter/stateFile.d.ts +31 -0
- package/dist/esm/reporter/stateFile.js +70 -0
- package/dist/esm/tools/AssertPageTool.d.ts +2 -2
- package/dist/esm/utils/MiscUtils.d.ts +0 -13
- package/dist/esm/utils/MiscUtils.js +0 -21
- package/dist/esm/utils/displayName.d.ts +16 -0
- package/dist/esm/utils/displayName.js +28 -0
- package/dist/managers/DonobuFlowsManager.js +2 -1
- package/dist/managers/TestsManager.js +2 -2
- package/dist/models/CreateTest.d.ts +1 -1
- package/dist/models/CreateTest.js +6 -0
- package/dist/persistence/DonobuSqliteDb.js +102 -0
- package/dist/persistence/TestConfigHash.d.ts +11 -0
- package/dist/persistence/TestConfigHash.js +31 -0
- package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts +0 -9
- package/dist/persistence/flows/FlowsPersistenceSqlite.js +4 -33
- package/dist/persistence/normalizeFlowMetadata.d.ts +16 -0
- package/dist/persistence/normalizeFlowMetadata.js +34 -0
- package/dist/reporter/buildReport.d.ts +22 -0
- package/dist/reporter/buildReport.js +106 -0
- package/dist/reporter/html.d.ts +5 -9
- package/dist/reporter/html.js +25 -101
- package/dist/reporter/markdown.d.ts +33 -0
- package/dist/reporter/markdown.js +62 -0
- package/dist/reporter/merge.d.ts +33 -0
- package/dist/reporter/merge.js +229 -0
- package/dist/reporter/model.d.ts +101 -0
- package/dist/reporter/model.js +27 -0
- package/dist/{esm/cli/playwright-json-to-html.d.ts → reporter/render.d.ts} +9 -14
- package/dist/{cli/playwright-json-to-html.js → reporter/render.js} +52 -152
- package/dist/reporter/renderMarkdown.d.ts +11 -0
- package/dist/{cli/playwright-json-to-markdown.js → reporter/renderMarkdown.js} +31 -110
- package/dist/reporter/renderSlack.d.ts +17 -0
- package/dist/reporter/renderSlack.js +100 -0
- package/dist/reporter/reportWalk.d.ts +28 -0
- package/dist/reporter/reportWalk.js +61 -0
- package/dist/reporter/slack.d.ts +93 -0
- package/dist/reporter/slack.js +150 -0
- package/dist/reporter/stateFile.d.ts +31 -0
- package/dist/reporter/stateFile.js +70 -0
- package/dist/tools/AssertPageTool.d.ts +2 -2
- package/dist/utils/MiscUtils.d.ts +0 -13
- package/dist/utils/MiscUtils.js +0 -21
- package/dist/utils/displayName.d.ts +16 -0
- package/dist/utils/displayName.js +28 -0
- package/package.json +11 -5
- package/dist/cli/playwright-json-to-markdown.d.ts +0 -43
- package/dist/cli/playwright-json-to-slack-json.d.ts +0 -3
- package/dist/cli/playwright-json-to-slack-json.js +0 -214
- package/dist/esm/cli/playwright-json-to-markdown.d.ts +0 -43
- package/dist/esm/cli/playwright-json-to-slack-json.d.ts +0 -3
- 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
|
|
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:
|
|
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
|
|
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,
|
|
74
|
+
? (0, render_1.loadTriageData)((0, path_1.resolve)(this.options.triageDir))
|
|
59
75
|
: { plans: [], evidence: [] };
|
|
60
|
-
const
|
|
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
|