donobu 5.28.0 → 5.30.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.d.ts +7 -1
- package/dist/cli/donobu-cli.js +195 -84
- package/dist/esm/cli/donobu-cli.d.ts +7 -1
- package/dist/esm/cli/donobu-cli.js +195 -84
- package/dist/esm/lib/test/testExtension.d.ts +31 -1
- package/dist/esm/lib/test/testExtension.js +273 -0
- package/dist/esm/managers/DonobuStack.js +1 -1
- package/dist/esm/managers/TestsManager.d.ts +2 -2
- package/dist/esm/managers/TestsManager.js +10 -0
- package/dist/esm/models/PaginatedResult.d.ts +15 -0
- package/dist/esm/models/PaginatedResult.js +13 -2
- package/dist/esm/models/TestMetadata.d.ts +630 -0
- package/dist/esm/models/TestMetadata.js +10 -1
- package/dist/esm/persistence/tests/TestsPersistence.d.ts +2 -2
- package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
- package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
- package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
- package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +9 -2
- package/dist/esm/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
- package/dist/esm/persistence/tests/TestsPersistenceSqlite.js +65 -6
- package/dist/esm/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
- package/dist/esm/persistence/tests/TestsPersistenceVolatile.js +69 -4
- package/dist/esm/reporter/renderMarkdown.js +1 -1
- package/dist/lib/test/testExtension.d.ts +31 -1
- package/dist/lib/test/testExtension.js +273 -0
- package/dist/managers/DonobuStack.js +1 -1
- package/dist/managers/TestsManager.d.ts +2 -2
- package/dist/managers/TestsManager.js +10 -0
- package/dist/models/PaginatedResult.d.ts +15 -0
- package/dist/models/PaginatedResult.js +13 -2
- package/dist/models/TestMetadata.d.ts +630 -0
- package/dist/models/TestMetadata.js +10 -1
- package/dist/persistence/tests/TestsPersistence.d.ts +2 -2
- package/dist/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
- package/dist/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
- package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
- package/dist/persistence/tests/TestsPersistenceRegistry.js +9 -2
- package/dist/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
- package/dist/persistence/tests/TestsPersistenceSqlite.js +65 -6
- package/dist/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
- package/dist/persistence/tests/TestsPersistenceVolatile.js +69 -4
- package/dist/reporter/renderMarkdown.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
2
|
-
import type { TestMetadata, TestsQuery } from '../../models/TestMetadata';
|
|
2
|
+
import type { TestListItem, TestMetadata, TestsQuery } from '../../models/TestMetadata';
|
|
3
3
|
/**
|
|
4
4
|
* Persistence interface for test metadata.
|
|
5
5
|
*
|
|
@@ -15,7 +15,7 @@ export interface TestsPersistence {
|
|
|
15
15
|
/** Get a test by ID. Throws TestNotFoundException if not found. */
|
|
16
16
|
getTestById(testId: string): Promise<TestMetadata>;
|
|
17
17
|
/** List tests with optional filtering and pagination. */
|
|
18
|
-
getTests(query: TestsQuery): Promise<PaginatedResult<
|
|
18
|
+
getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
|
|
19
19
|
/** Delete a test. */
|
|
20
20
|
deleteTest(testId: string): Promise<void>;
|
|
21
21
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type TestMetadata, type TestsQuery } from '../../models/TestMetadata';
|
|
1
|
+
import { type PaginatedResult } from '../../models/PaginatedResult';
|
|
2
|
+
import { type TestListItem, type TestMetadata, type TestsQuery } from '../../models/TestMetadata';
|
|
3
3
|
import { DonobuApiClient } from '../DonobuApiClient';
|
|
4
4
|
import type { TestsPersistence } from './TestsPersistence';
|
|
5
5
|
export declare class TestsPersistenceDonobuApi extends DonobuApiClient implements TestsPersistence {
|
|
@@ -7,7 +7,7 @@ export declare class TestsPersistenceDonobuApi extends DonobuApiClient implement
|
|
|
7
7
|
createTest(testMetadata: TestMetadata): Promise<void>;
|
|
8
8
|
updateTest(testMetadata: TestMetadata): Promise<void>;
|
|
9
9
|
getTestById(testId: string): Promise<TestMetadata>;
|
|
10
|
-
getTests(query: TestsQuery): Promise<PaginatedResult<
|
|
10
|
+
getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
|
|
11
11
|
deleteTest(testId: string): Promise<void>;
|
|
12
12
|
}
|
|
13
13
|
//# sourceMappingURL=TestsPersistenceDonobuApi.d.ts.map
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TestsPersistenceDonobuApi = void 0;
|
|
4
4
|
const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
|
|
5
|
+
const PaginatedResult_1 = require("../../models/PaginatedResult");
|
|
5
6
|
const TestMetadata_1 = require("../../models/TestMetadata");
|
|
6
7
|
const DonobuApiClient_1 = require("../DonobuApiClient");
|
|
8
|
+
const normalizeFlowMetadata_1 = require("../normalizeFlowMetadata");
|
|
7
9
|
class TestsPersistenceDonobuApi extends DonobuApiClient_1.DonobuApiClient {
|
|
8
10
|
constructor(baseUrl, apiKey) {
|
|
9
11
|
super(baseUrl, apiKey);
|
|
@@ -76,9 +78,23 @@ class TestsPersistenceDonobuApi extends DonobuApiClient_1.DonobuApiClient {
|
|
|
76
78
|
if (!response.ok) {
|
|
77
79
|
throw new Error(`Failed to list tests: ${response.status} ${response.statusText}`);
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
// Validate the basic pagination shape of the API response, but keep the
|
|
82
|
+
// items as `unknown` for now.
|
|
83
|
+
const json = PaginatedResult_1.PaginatedResultSchema.parse(await response.json());
|
|
80
84
|
return {
|
|
81
|
-
items: json.items.map((item) =>
|
|
85
|
+
items: json.items.map((item) => {
|
|
86
|
+
// Cast this enough to be able to easily pull out the `latestFlow`
|
|
87
|
+
// field. It'll be properly validated by the schema in the `return`
|
|
88
|
+
// statement below, after normalizing the flow metadata.
|
|
89
|
+
const testListItem = item;
|
|
90
|
+
// Run full validation after normalizing the flow metadata.
|
|
91
|
+
return TestMetadata_1.TestListItemSchema.parse({
|
|
92
|
+
...testListItem,
|
|
93
|
+
latestFlow: testListItem.latestFlow
|
|
94
|
+
? (0, normalizeFlowMetadata_1.normalizeFlowMetadata)(testListItem.latestFlow)
|
|
95
|
+
: null,
|
|
96
|
+
});
|
|
97
|
+
}),
|
|
82
98
|
nextPageToken: json.nextPageToken,
|
|
83
99
|
};
|
|
84
100
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EnvPick } from 'env-struct';
|
|
2
2
|
import type { env } from '../../envVars';
|
|
3
|
+
import type { FlowsPersistenceRegistry } from '../flows/FlowsPersistenceRegistry';
|
|
3
4
|
import { PersistencePluginRegistry } from '../PersistencePlugin';
|
|
4
5
|
import type { TestsPersistence } from './TestsPersistence';
|
|
5
6
|
export interface TestsPersistenceRegistry {
|
|
@@ -11,7 +12,14 @@ export interface TestsPersistenceRegistry {
|
|
|
11
12
|
export declare class TestsPersistenceRegistryImpl implements TestsPersistenceRegistry {
|
|
12
13
|
private readonly layers;
|
|
13
14
|
constructor(layers: TestsPersistence[]);
|
|
14
|
-
static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>,
|
|
15
|
+
static fromEnvironment(environ: EnvPick<typeof env, 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'PERSISTENCE_PRIORITY'>,
|
|
16
|
+
/**
|
|
17
|
+
* Flows registry — used by the volatile tests layer to compute
|
|
18
|
+
* flow-derived sort keys (flow_count, latest_flow_created_at).
|
|
19
|
+
* Optional: when omitted, the volatile layer falls back to
|
|
20
|
+
* best-effort no-op sorting on those keys.
|
|
21
|
+
*/
|
|
22
|
+
flowsRegistry?: FlowsPersistenceRegistry, persistencePlugins?: PersistencePluginRegistry): Promise<TestsPersistenceRegistryImpl>;
|
|
15
23
|
get(): Promise<TestsPersistence>;
|
|
16
24
|
getAll(): Promise<TestsPersistence[]>;
|
|
17
25
|
}
|
|
@@ -13,7 +13,14 @@ class TestsPersistenceRegistryImpl {
|
|
|
13
13
|
throw new Error('No valid test persistence implementation available!');
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
static async fromEnvironment(environ,
|
|
16
|
+
static async fromEnvironment(environ,
|
|
17
|
+
/**
|
|
18
|
+
* Flows registry — used by the volatile tests layer to compute
|
|
19
|
+
* flow-derived sort keys (flow_count, latest_flow_created_at).
|
|
20
|
+
* Optional: when omitted, the volatile layer falls back to
|
|
21
|
+
* best-effort no-op sorting on those keys.
|
|
22
|
+
*/
|
|
23
|
+
flowsRegistry, persistencePlugins = new PersistencePlugin_1.PersistencePluginRegistry()) {
|
|
17
24
|
const donobuApiBaseUrl = environ.data.DONOBU_API_BASE_URL;
|
|
18
25
|
const donobuApiKey = environ.data.DONOBU_API_KEY;
|
|
19
26
|
const layers = [];
|
|
@@ -28,7 +35,7 @@ class TestsPersistenceRegistryImpl {
|
|
|
28
35
|
layers.push(await TestsPersistenceSqlite_1.TestsPersistenceSqlite.create(await (0, DonobuSqliteDb_1.getDonobuSqliteDatabase)()));
|
|
29
36
|
break;
|
|
30
37
|
case 'RAM':
|
|
31
|
-
layers.push(new TestsPersistenceVolatile_1.TestsPersistenceVolatile());
|
|
38
|
+
layers.push(new TestsPersistenceVolatile_1.TestsPersistenceVolatile(undefined, flowsRegistry ? () => flowsRegistry.get() : undefined));
|
|
32
39
|
break;
|
|
33
40
|
default: {
|
|
34
41
|
const plugin = persistencePlugins.get(key);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type Database from 'better-sqlite3';
|
|
2
2
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
3
|
+
import type { TestListItem } from '../../models/TestMetadata';
|
|
3
4
|
import { type TestMetadata, type TestsQuery } from '../../models/TestMetadata';
|
|
4
5
|
import type { TestsPersistence } from './TestsPersistence';
|
|
5
6
|
export declare class TestsPersistenceSqlite implements TestsPersistence {
|
|
@@ -9,7 +10,7 @@ export declare class TestsPersistenceSqlite implements TestsPersistence {
|
|
|
9
10
|
createTest(testMetadata: TestMetadata): Promise<void>;
|
|
10
11
|
updateTest(testMetadata: TestMetadata): Promise<void>;
|
|
11
12
|
getTestById(testId: string): Promise<TestMetadata>;
|
|
12
|
-
getTests(query: TestsQuery): Promise<PaginatedResult<
|
|
13
|
+
getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
|
|
13
14
|
deleteTest(testId: string): Promise<void>;
|
|
14
15
|
}
|
|
15
16
|
//# sourceMappingURL=TestsPersistenceSqlite.d.ts.map
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TestsPersistenceSqlite = void 0;
|
|
4
4
|
const TestNotFoundException_1 = require("../../exceptions/TestNotFoundException");
|
|
5
|
+
const FlowMetadata_1 = require("../../models/FlowMetadata");
|
|
5
6
|
const TestMetadata_1 = require("../../models/TestMetadata");
|
|
7
|
+
const normalizeFlowMetadata_1 = require("../normalizeFlowMetadata");
|
|
6
8
|
class TestsPersistenceSqlite {
|
|
7
9
|
constructor(db) {
|
|
8
10
|
this.db = db;
|
|
@@ -70,13 +72,60 @@ class TestsPersistenceSqlite {
|
|
|
70
72
|
whereConditions.push('suite_id = ?');
|
|
71
73
|
params.push(query.suiteId);
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// LEFT JOIN against a per-test aggregate of flow_metadata so the
|
|
76
|
+
// `flow_count` and `latest_flow_created_at` columns are queryable for
|
|
77
|
+
// ORDER BY without a separate round-trip per test. A second LEFT JOIN
|
|
78
|
+
// uses ROW_NUMBER() OVER (PARTITION BY test_id …) to pick the single
|
|
79
|
+
// most-recent flow per test, so we can return its full metadata blob
|
|
80
|
+
// alongside the count without an N+1 fetch.
|
|
81
|
+
let sql = `
|
|
82
|
+
SELECT
|
|
83
|
+
t.metadata AS metadata,
|
|
84
|
+
COALESCE(fc.flow_count, 0) AS flow_count,
|
|
85
|
+
fc.latest_flow_created_at AS latest_flow_created_at,
|
|
86
|
+
lf.metadata AS latest_flow_metadata
|
|
87
|
+
FROM test_metadata t
|
|
88
|
+
LEFT JOIN (
|
|
89
|
+
SELECT test_id,
|
|
90
|
+
COUNT(*) AS flow_count,
|
|
91
|
+
MAX(created_at) AS latest_flow_created_at
|
|
92
|
+
FROM flow_metadata
|
|
93
|
+
WHERE test_id IS NOT NULL
|
|
94
|
+
GROUP BY test_id
|
|
95
|
+
) fc ON fc.test_id = t.id
|
|
96
|
+
LEFT JOIN (
|
|
97
|
+
SELECT test_id, metadata,
|
|
98
|
+
ROW_NUMBER() OVER (
|
|
99
|
+
PARTITION BY test_id
|
|
100
|
+
ORDER BY created_at DESC, id DESC
|
|
101
|
+
) AS rn
|
|
102
|
+
FROM flow_metadata
|
|
103
|
+
WHERE test_id IS NOT NULL
|
|
104
|
+
) lf ON lf.test_id = t.id AND lf.rn = 1
|
|
105
|
+
`;
|
|
106
|
+
// The WHERE clauses still target test_metadata columns; rewrite them to
|
|
107
|
+
// the aliased table.
|
|
108
|
+
const aliasedWhere = whereConditions.map((c) => `t.${c}`);
|
|
109
|
+
if (aliasedWhere.length > 0) {
|
|
110
|
+
sql += ' WHERE ' + aliasedWhere.join(' AND ');
|
|
76
111
|
}
|
|
77
|
-
|
|
112
|
+
// Map the API-level sortBy to the actual SQL column. test_metadata
|
|
113
|
+
// columns are referenced through the `t.` alias; the join-derived
|
|
114
|
+
// columns are referenced by the alias defined in the SELECT list.
|
|
115
|
+
const sortColMap = {
|
|
116
|
+
created_at: 't.created_at',
|
|
117
|
+
name: 't.name',
|
|
118
|
+
suite_id: 't.suite_id',
|
|
119
|
+
next_run_mode: 't.next_run_mode',
|
|
120
|
+
flow_count: 'flow_count',
|
|
121
|
+
latest_flow_created_at: 'latest_flow_created_at',
|
|
122
|
+
};
|
|
123
|
+
const sortCol = sortColMap[query.sortBy ?? 'created_at'] ?? 't.created_at';
|
|
78
124
|
const sortDir = query.sortOrder === 'asc' ? 'ASC' : 'DESC';
|
|
79
|
-
|
|
125
|
+
// Stable secondary sort by created_at so flows with identical sort keys
|
|
126
|
+
// (e.g. flow_count = 0 across many tests) come back in a deterministic
|
|
127
|
+
// order across requests.
|
|
128
|
+
sql += ` ORDER BY ${sortCol} ${sortDir}, t.created_at DESC LIMIT ? OFFSET ?`;
|
|
80
129
|
params.push(validLimit + 1);
|
|
81
130
|
params.push(offset);
|
|
82
131
|
const stmt = this.db.prepare(sql);
|
|
@@ -85,7 +134,17 @@ class TestsPersistenceSqlite {
|
|
|
85
134
|
const results = hasMore ? rows.slice(0, validLimit) : rows;
|
|
86
135
|
const nextPageToken = hasMore ? `${offset + validLimit}` : undefined;
|
|
87
136
|
return {
|
|
88
|
-
items: results.map((row) =>
|
|
137
|
+
items: results.map((row) => {
|
|
138
|
+
const test = TestMetadata_1.TestMetadataSchema.parse(JSON.parse(row.metadata));
|
|
139
|
+
const latestFlow = row.latest_flow_metadata
|
|
140
|
+
? FlowMetadata_1.FlowMetadataSchema.parse((0, normalizeFlowMetadata_1.normalizeFlowMetadata)(JSON.parse(row.latest_flow_metadata)))
|
|
141
|
+
: null;
|
|
142
|
+
return {
|
|
143
|
+
...test,
|
|
144
|
+
flowCount: row.flow_count,
|
|
145
|
+
latestFlow,
|
|
146
|
+
};
|
|
147
|
+
}),
|
|
89
148
|
nextPageToken,
|
|
90
149
|
};
|
|
91
150
|
}
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import type { PaginatedResult } from '../../models/PaginatedResult';
|
|
2
|
-
import type { TestMetadata, TestsQuery } from '../../models/TestMetadata';
|
|
2
|
+
import type { TestListItem, TestMetadata, TestsQuery } from '../../models/TestMetadata';
|
|
3
|
+
import type { FlowsPersistence } from '../flows/FlowsPersistence';
|
|
3
4
|
import type { TestsPersistence } from './TestsPersistence';
|
|
4
5
|
/**
|
|
5
6
|
* A volatile (in-memory) implementation of TestsPersistence.
|
|
6
7
|
*/
|
|
7
8
|
export declare class TestsPersistenceVolatile implements TestsPersistence {
|
|
8
9
|
private readonly tests;
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Optional flows-source callback used to compute flow-derived sort
|
|
12
|
+
* keys (`flow_count`, `latest_flow_created_at`). When supplied, the
|
|
13
|
+
* volatile layer can natively sort by those keys; otherwise they
|
|
14
|
+
* fall back to the existing best-effort string-index lookup. The
|
|
15
|
+
* callback is async so the registry can resolve the flows
|
|
16
|
+
* persistence layer lazily.
|
|
17
|
+
*/
|
|
18
|
+
private readonly flowsSource?;
|
|
19
|
+
constructor(tests?: Map<string, TestMetadata>,
|
|
20
|
+
/**
|
|
21
|
+
* Optional flows-source callback used to compute flow-derived sort
|
|
22
|
+
* keys (`flow_count`, `latest_flow_created_at`). When supplied, the
|
|
23
|
+
* volatile layer can natively sort by those keys; otherwise they
|
|
24
|
+
* fall back to the existing best-effort string-index lookup. The
|
|
25
|
+
* callback is async so the registry can resolve the flows
|
|
26
|
+
* persistence layer lazily.
|
|
27
|
+
*/
|
|
28
|
+
flowsSource?: (() => Promise<FlowsPersistence>) | undefined);
|
|
10
29
|
createTest(testMetadata: TestMetadata): Promise<void>;
|
|
11
30
|
updateTest(testMetadata: TestMetadata): Promise<void>;
|
|
12
31
|
getTestById(testId: string): Promise<TestMetadata>;
|
|
13
|
-
getTests(query: TestsQuery): Promise<PaginatedResult<
|
|
32
|
+
getTests(query: TestsQuery): Promise<PaginatedResult<TestListItem>>;
|
|
14
33
|
deleteTest(testId: string): Promise<void>;
|
|
15
34
|
}
|
|
16
35
|
//# sourceMappingURL=TestsPersistenceVolatile.d.ts.map
|
|
@@ -9,8 +9,18 @@ function deepCopy(test) {
|
|
|
9
9
|
* A volatile (in-memory) implementation of TestsPersistence.
|
|
10
10
|
*/
|
|
11
11
|
class TestsPersistenceVolatile {
|
|
12
|
-
constructor(tests = new Map()
|
|
12
|
+
constructor(tests = new Map(),
|
|
13
|
+
/**
|
|
14
|
+
* Optional flows-source callback used to compute flow-derived sort
|
|
15
|
+
* keys (`flow_count`, `latest_flow_created_at`). When supplied, the
|
|
16
|
+
* volatile layer can natively sort by those keys; otherwise they
|
|
17
|
+
* fall back to the existing best-effort string-index lookup. The
|
|
18
|
+
* callback is async so the registry can resolve the flows
|
|
19
|
+
* persistence layer lazily.
|
|
20
|
+
*/
|
|
21
|
+
flowsSource) {
|
|
13
22
|
this.tests = tests;
|
|
23
|
+
this.flowsSource = flowsSource;
|
|
14
24
|
}
|
|
15
25
|
async createTest(testMetadata) {
|
|
16
26
|
this.tests.set(testMetadata.id, deepCopy(testMetadata));
|
|
@@ -44,9 +54,57 @@ class TestsPersistenceVolatile {
|
|
|
44
54
|
}
|
|
45
55
|
const sortCol = query.sortBy ?? 'created_at';
|
|
46
56
|
const sortAsc = query.sortOrder === 'asc';
|
|
57
|
+
// Pre-compute a per-test summary (count + latest flow) from the flows
|
|
58
|
+
// source (volatile flows layer in the typical RAM setup; can be any
|
|
59
|
+
// FlowsPersistence). Done in parallel; results populate `summaries`
|
|
60
|
+
// before the synchronous sort comparator and the final TestListItem
|
|
61
|
+
// construction below. Without a flows source we still serve the list
|
|
62
|
+
// — flowCount defaults to 0 and latestFlow to null.
|
|
63
|
+
const summaries = new Map();
|
|
64
|
+
if (this.flowsSource) {
|
|
65
|
+
const flowsLayer = await this.flowsSource();
|
|
66
|
+
await Promise.all(filtered.map(async (test) => {
|
|
67
|
+
let count = 0;
|
|
68
|
+
let latestCreatedAt = 0;
|
|
69
|
+
let latestFlow = null;
|
|
70
|
+
let pageToken = undefined;
|
|
71
|
+
do {
|
|
72
|
+
const page = await flowsLayer.getFlowsMetadata({
|
|
73
|
+
testId: test.id,
|
|
74
|
+
limit: 100,
|
|
75
|
+
pageToken,
|
|
76
|
+
});
|
|
77
|
+
count += page.items.length;
|
|
78
|
+
for (const flow of page.items) {
|
|
79
|
+
// Volatile flows don't track a separate created_at column;
|
|
80
|
+
// startedAt is the closest proxy and the same value the
|
|
81
|
+
// SQLite migration uses for backfilled rows.
|
|
82
|
+
const ts = flow.startedAt ?? 0;
|
|
83
|
+
if (ts > latestCreatedAt) {
|
|
84
|
+
latestCreatedAt = ts;
|
|
85
|
+
latestFlow = flow;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
pageToken = page.nextPageToken;
|
|
89
|
+
} while (pageToken);
|
|
90
|
+
summaries.set(test.id, { count, latestCreatedAt, latestFlow });
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
47
93
|
filtered.sort((a, b) => {
|
|
48
|
-
|
|
49
|
-
|
|
94
|
+
let aVal;
|
|
95
|
+
let bVal;
|
|
96
|
+
if (sortCol === 'flow_count') {
|
|
97
|
+
aVal = summaries.get(a.id)?.count ?? 0;
|
|
98
|
+
bVal = summaries.get(b.id)?.count ?? 0;
|
|
99
|
+
}
|
|
100
|
+
else if (sortCol === 'latest_flow_created_at') {
|
|
101
|
+
aVal = summaries.get(a.id)?.latestCreatedAt ?? 0;
|
|
102
|
+
bVal = summaries.get(b.id)?.latestCreatedAt ?? 0;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
aVal = String(a[sortCol] ?? '');
|
|
106
|
+
bVal = String(b[sortCol] ?? '');
|
|
107
|
+
}
|
|
50
108
|
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
51
109
|
return sortAsc ? cmp : -cmp;
|
|
52
110
|
});
|
|
@@ -54,7 +112,14 @@ class TestsPersistenceVolatile {
|
|
|
54
112
|
const paged = filtered.slice(offset, offset + validLimit);
|
|
55
113
|
const hasMore = offset + validLimit < filtered.length;
|
|
56
114
|
return {
|
|
57
|
-
items: paged.map(
|
|
115
|
+
items: paged.map((test) => {
|
|
116
|
+
const summary = summaries.get(test.id);
|
|
117
|
+
return {
|
|
118
|
+
...deepCopy(test),
|
|
119
|
+
flowCount: summary?.count ?? 0,
|
|
120
|
+
latestFlow: summary?.latestFlow ?? null,
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
58
123
|
nextPageToken: hasMore ? (offset + validLimit).toString() : undefined,
|
|
59
124
|
};
|
|
60
125
|
}
|
|
@@ -152,7 +152,7 @@ function renderMarkdown(report) {
|
|
|
152
152
|
}
|
|
153
153
|
if (result.status === 'failed' && result.error) {
|
|
154
154
|
markdown += `\n<details>\n<summary>⚠️ Error Details</summary>\n\n`;
|
|
155
|
-
markdown += `\`\`\`\n${result.error.message || 'No error message available'}\n\`\`\`\n\n`;
|
|
155
|
+
markdown += `\`\`\`\n${(0, ansi_1.stripAnsi)(result.error.message || '') || 'No error message available'}\n\`\`\`\n\n`;
|
|
156
156
|
if (result.error.snippet) {
|
|
157
157
|
markdown += `**Code Snippet**:\n\`\`\`\n${(0, ansi_1.stripAnsi)(result.error.snippet)}\n\`\`\`\n\n`;
|
|
158
158
|
}
|