@letsrunit/store 0.10.0 → 0.11.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/README.md +18 -14
- package/dist/index.d.ts +55 -10
- package/dist/index.js +249 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/db.ts +34 -22
- package/src/ids.ts +69 -6
- package/src/index.ts +4 -4
- package/src/read.ts +160 -21
- package/src/write.ts +70 -13
package/README.md
CHANGED
|
@@ -10,20 +10,21 @@ npm install @letsrunit/store
|
|
|
10
10
|
yarn add @letsrunit/store
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
SQLite-backed persistence layer for letsrunit run history and artifacts. It manages the database schema and provides write functions for recording
|
|
13
|
+
SQLite-backed persistence layer for letsrunit run history and artifacts. It manages the database schema and provides write functions for recording runs, features, scenarios, rules/outline metadata, tests, steps, and file artifacts.
|
|
14
14
|
|
|
15
15
|
## Schema
|
|
16
16
|
|
|
17
|
-
The store maintains
|
|
17
|
+
The store maintains seven tables:
|
|
18
18
|
|
|
19
19
|
| Table | Purpose |
|
|
20
20
|
|---|---|
|
|
21
|
-
| `
|
|
22
|
-
| `features` | Gherkin feature files, keyed by
|
|
23
|
-
| `scenarios` |
|
|
24
|
-
| `steps` |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
21
|
+
| `runs` | One row per top-level invocation, keyed by a hash ID with optional git commit |
|
|
22
|
+
| `features` | Gherkin feature files, keyed by hash of ordered scenario IDs |
|
|
23
|
+
| `scenarios` | Executable scenarios (including outline rows), with nullable `rule`, `outline`, and `example_row` grouping fields |
|
|
24
|
+
| `steps` | Canonical normalized step definitions, keyed by step-text hash |
|
|
25
|
+
| `scenario_steps` | Ordered step placement for each scenario |
|
|
26
|
+
| `tests` | One row per scenario execution within a run; status is `running`, `passed`, or `failed` |
|
|
27
|
+
| `artifacts` | Files (screenshots, HTML snapshots) produced at a scenario step index in a test |
|
|
27
28
|
|
|
28
29
|
## API
|
|
29
30
|
|
|
@@ -43,13 +44,16 @@ All write functions take a `db` instance as their first argument.
|
|
|
43
44
|
|
|
44
45
|
| Function | Description |
|
|
45
46
|
|---|---|
|
|
46
|
-
| `
|
|
47
|
+
| `insertRun(db, id, gitCommit, startedAt)` | Records a new run |
|
|
47
48
|
| `upsertFeature(db, id, path, name)` | Inserts or replaces a feature file record |
|
|
48
|
-
| `upsertScenario(db, id, featureId, name)` | Inserts or replaces a scenario record |
|
|
49
|
-
| `upsertStep(db, id,
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
49
|
+
| `upsertScenario(db, id, featureId, index, name, refs?)` | Inserts or replaces a scenario record |
|
|
50
|
+
| `upsertStep(db, id, text)` | Inserts or replaces a canonical step record |
|
|
51
|
+
| `upsertScenarioStep(db, scenarioId, index, stepId)` | Inserts or replaces a scenario-step mapping |
|
|
52
|
+
| `insertTest(db, id, runId, scenarioId, startedAt)` | Starts a new test with status `running` |
|
|
53
|
+
| `finaliseTest(db, id, status, failedStepIndex?, error?)` | Updates a test's final status and optional failure details |
|
|
54
|
+
| `insertArtifact(db, id, testId, stepIndex, filename)` | Links a saved file to a specific step index in a test |
|
|
55
|
+
| `findLastPassingBaseline(db, scenarioId, allowedCommits?)` | Returns latest passing test id + run commit for a scenario |
|
|
56
|
+
| `findLastRun(db)` | Returns latest run metadata and all tests (with ordered scenario steps) in that run |
|
|
53
57
|
|
|
54
58
|
## Testing
|
|
55
59
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,26 +3,71 @@ import nodeWasm from 'node-sqlite3-wasm';
|
|
|
3
3
|
type Database = InstanceType<typeof nodeWasm.Database>;
|
|
4
4
|
declare function openStore(path?: string): Database;
|
|
5
5
|
|
|
6
|
-
declare function
|
|
6
|
+
declare function insertRun(db: Database, id: string, gitCommit: string | null, startedAt: number): void;
|
|
7
7
|
declare function upsertFeature(db: Database, id: string, path: string, name: string): void;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
interface ScenarioRefs {
|
|
9
|
+
rule?: string;
|
|
10
|
+
outline?: string;
|
|
11
|
+
exampleRow?: string;
|
|
12
|
+
exampleIndex?: number;
|
|
13
|
+
}
|
|
14
|
+
declare function upsertScenario(db: Database, id: string, featureId: string, index: number, name: string, refs?: ScenarioRefs): void;
|
|
15
|
+
declare function upsertStep(db: Database, id: string, text: string): void;
|
|
16
|
+
declare function upsertScenarioStep(db: Database, scenarioId: string, index: number, stepId: string): void;
|
|
17
|
+
declare function insertTest(db: Database, id: string, runId: string, scenarioId: string, startedAt: number): void;
|
|
18
|
+
declare function finaliseTest(db: Database, id: string, status: string, failedStepIndex?: number, error?: string): void;
|
|
19
|
+
declare function insertArtifact(db: Database, id: string, testId: string, stepIndex: number, filename: string): void;
|
|
13
20
|
|
|
14
21
|
declare function computeStepId(normalizedText: string): string;
|
|
15
22
|
declare function computeScenarioId(stepIds: string[]): string;
|
|
16
|
-
declare function computeFeatureId(
|
|
23
|
+
declare function computeFeatureId(scenarioIds: string[]): string;
|
|
24
|
+
declare function computeRuleId(scenarioIds: string[]): string;
|
|
25
|
+
declare function computeOutlineId(stepIds: string[]): string;
|
|
26
|
+
declare function computeExampleRowId(values: string[]): string;
|
|
27
|
+
declare function toIdBlob(id: string): Buffer;
|
|
28
|
+
declare function fromIdBlob(id: Uint8Array): string;
|
|
17
29
|
|
|
18
|
-
declare function
|
|
30
|
+
declare function findLastTest(db: Database, scenarioId: string, status?: string, allowedCommits?: string[]): {
|
|
19
31
|
id: string;
|
|
20
32
|
gitCommit: string | null;
|
|
21
33
|
} | null;
|
|
22
|
-
declare function
|
|
34
|
+
declare function findLastPassingBaseline(db: Database, scenarioId: string, allowedCommits?: string[]): {
|
|
35
|
+
testId: string;
|
|
36
|
+
gitCommit: string | null;
|
|
37
|
+
} | null;
|
|
38
|
+
declare function findArtifacts(db: Database, testId: string, stepId?: string): Array<{
|
|
23
39
|
filename: string;
|
|
24
40
|
stepId: string;
|
|
25
41
|
stepIdx: number;
|
|
26
42
|
}>;
|
|
43
|
+
interface LastRunStep {
|
|
44
|
+
id: string;
|
|
45
|
+
index: number;
|
|
46
|
+
text: string;
|
|
47
|
+
}
|
|
48
|
+
interface LastRunTest {
|
|
49
|
+
id: string;
|
|
50
|
+
scenarioId: string;
|
|
51
|
+
scenarioName: string;
|
|
52
|
+
featureId: string;
|
|
53
|
+
featurePath: string;
|
|
54
|
+
featureName: string;
|
|
55
|
+
status: string;
|
|
56
|
+
startedAt: number;
|
|
57
|
+
failedStepIndex: number | null;
|
|
58
|
+
error: string | null;
|
|
59
|
+
ruleId: string | null;
|
|
60
|
+
outlineId: string | null;
|
|
61
|
+
exampleRowId: string | null;
|
|
62
|
+
exampleIndex: number | null;
|
|
63
|
+
steps: LastRunStep[];
|
|
64
|
+
}
|
|
65
|
+
interface LastRun {
|
|
66
|
+
id: string;
|
|
67
|
+
startedAt: number;
|
|
68
|
+
gitCommit: string | null;
|
|
69
|
+
tests: LastRunTest[];
|
|
70
|
+
}
|
|
71
|
+
declare function findLastRun(db: Database): LastRun | null;
|
|
27
72
|
|
|
28
|
-
export { computeFeatureId, computeScenarioId, computeStepId,
|
|
73
|
+
export { type Database, type LastRun, type LastRunStep, type LastRunTest, computeExampleRowId, computeFeatureId, computeOutlineId, computeRuleId, computeScenarioId, computeStepId, finaliseTest, findArtifacts, findLastPassingBaseline, findLastRun, findLastTest, fromIdBlob, insertArtifact, insertRun, insertTest, openStore, toIdBlob, upsertFeature, upsertScenario, upsertScenarioStep, upsertStep };
|
package/dist/index.js
CHANGED
|
@@ -1,44 +1,56 @@
|
|
|
1
1
|
import nodeWasm from 'node-sqlite3-wasm';
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
3
|
|
|
4
4
|
// src/db.ts
|
|
5
5
|
var { Database } = nodeWasm;
|
|
6
6
|
var SCHEMA = `
|
|
7
|
-
CREATE TABLE IF NOT EXISTS
|
|
8
|
-
id
|
|
7
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
8
|
+
id BLOB PRIMARY KEY,
|
|
9
9
|
started_at INTEGER NOT NULL,
|
|
10
10
|
git_commit TEXT
|
|
11
11
|
);
|
|
12
12
|
CREATE TABLE IF NOT EXISTS features (
|
|
13
|
-
id
|
|
13
|
+
id BLOB PRIMARY KEY,
|
|
14
14
|
path TEXT NOT NULL,
|
|
15
15
|
name TEXT NOT NULL
|
|
16
16
|
);
|
|
17
17
|
CREATE TABLE IF NOT EXISTS scenarios (
|
|
18
|
-
id
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
id BLOB PRIMARY KEY,
|
|
19
|
+
feature BLOB NOT NULL REFERENCES features(id),
|
|
20
|
+
"index" INTEGER NOT NULL,
|
|
21
|
+
name TEXT NOT NULL,
|
|
22
|
+
rule BLOB,
|
|
23
|
+
outline BLOB,
|
|
24
|
+
example_row BLOB,
|
|
25
|
+
example_index INTEGER,
|
|
26
|
+
UNIQUE(feature, "index"),
|
|
27
|
+
CHECK (outline IS NULL OR example_row IS NOT NULL),
|
|
28
|
+
CHECK (example_row IS NULL OR outline IS NOT NULL)
|
|
21
29
|
);
|
|
22
30
|
CREATE TABLE IF NOT EXISTS steps (
|
|
23
|
-
id
|
|
24
|
-
|
|
25
|
-
idx INTEGER NOT NULL,
|
|
26
|
-
text TEXT NOT NULL
|
|
31
|
+
id BLOB PRIMARY KEY,
|
|
32
|
+
text TEXT NOT NULL
|
|
27
33
|
);
|
|
28
|
-
CREATE TABLE IF NOT EXISTS
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
CREATE TABLE IF NOT EXISTS scenario_steps (
|
|
35
|
+
scenario BLOB NOT NULL REFERENCES scenarios(id),
|
|
36
|
+
"index" INTEGER NOT NULL,
|
|
37
|
+
step BLOB NOT NULL REFERENCES steps(id),
|
|
38
|
+
PRIMARY KEY (scenario, "index")
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS tests (
|
|
41
|
+
id BLOB PRIMARY KEY,
|
|
42
|
+
run BLOB NOT NULL REFERENCES runs(id),
|
|
43
|
+
scenario BLOB NOT NULL REFERENCES scenarios(id),
|
|
44
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
45
|
+
failed_step_index INTEGER,
|
|
46
|
+
error TEXT,
|
|
47
|
+
started_at INTEGER NOT NULL
|
|
36
48
|
);
|
|
37
49
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
38
|
-
id
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
filename
|
|
50
|
+
id BLOB PRIMARY KEY,
|
|
51
|
+
test BLOB NOT NULL REFERENCES tests(id),
|
|
52
|
+
step_index INTEGER NOT NULL,
|
|
53
|
+
filename TEXT NOT NULL
|
|
42
54
|
);
|
|
43
55
|
`;
|
|
44
56
|
function openStore(path = ".letsrunit/letsrunit.db") {
|
|
@@ -48,82 +60,254 @@ function openStore(path = ".letsrunit/letsrunit.db") {
|
|
|
48
60
|
db.exec(SCHEMA);
|
|
49
61
|
return db;
|
|
50
62
|
}
|
|
63
|
+
var TAGS = {
|
|
64
|
+
step: 1,
|
|
65
|
+
scenario: 2,
|
|
66
|
+
feature: 3,
|
|
67
|
+
rule: 4,
|
|
68
|
+
outline: 5,
|
|
69
|
+
exampleRow: 6
|
|
70
|
+
};
|
|
71
|
+
function encodeTag(tag) {
|
|
72
|
+
const buf = Buffer.allocUnsafe(2);
|
|
73
|
+
buf.writeUInt16BE(tag, 0);
|
|
74
|
+
return buf;
|
|
75
|
+
}
|
|
76
|
+
function hashBytes(tag, payload) {
|
|
77
|
+
return createHash("sha256").update(encodeTag(tag)).update(payload).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
function concatHexIds(ids) {
|
|
80
|
+
if (ids.length === 0) return Buffer.alloc(0);
|
|
81
|
+
return Buffer.concat(ids.map((id) => Buffer.from(id, "hex")));
|
|
82
|
+
}
|
|
83
|
+
function encodeStrings(parts) {
|
|
84
|
+
const encoded = [];
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
const chunk = Buffer.from(part, "utf8");
|
|
87
|
+
const len = Buffer.allocUnsafe(4);
|
|
88
|
+
len.writeUInt32BE(chunk.length, 0);
|
|
89
|
+
encoded.push(len, chunk);
|
|
90
|
+
}
|
|
91
|
+
return Buffer.concat(encoded);
|
|
92
|
+
}
|
|
93
|
+
function computeStepId(normalizedText) {
|
|
94
|
+
return hashBytes(TAGS.step, Buffer.from(normalizedText, "utf8"));
|
|
95
|
+
}
|
|
96
|
+
function computeScenarioId(stepIds) {
|
|
97
|
+
return hashBytes(TAGS.scenario, concatHexIds(stepIds));
|
|
98
|
+
}
|
|
99
|
+
function computeFeatureId(scenarioIds) {
|
|
100
|
+
return hashBytes(TAGS.feature, concatHexIds(scenarioIds));
|
|
101
|
+
}
|
|
102
|
+
function computeRuleId(scenarioIds) {
|
|
103
|
+
return hashBytes(TAGS.rule, concatHexIds(scenarioIds));
|
|
104
|
+
}
|
|
105
|
+
function computeOutlineId(stepIds) {
|
|
106
|
+
return hashBytes(TAGS.outline, concatHexIds(stepIds));
|
|
107
|
+
}
|
|
108
|
+
function computeExampleRowId(values) {
|
|
109
|
+
return hashBytes(TAGS.exampleRow, encodeStrings(values));
|
|
110
|
+
}
|
|
111
|
+
function hashId(input) {
|
|
112
|
+
return createHash("sha256").update(input, "utf8").digest();
|
|
113
|
+
}
|
|
114
|
+
function toIdBlob(id) {
|
|
115
|
+
const normalized = id.toLowerCase();
|
|
116
|
+
if (/^[0-9a-f]{64}$/.test(normalized)) {
|
|
117
|
+
return Buffer.from(normalized, "hex");
|
|
118
|
+
}
|
|
119
|
+
return hashId(id);
|
|
120
|
+
}
|
|
121
|
+
function fromIdBlob(id) {
|
|
122
|
+
return Buffer.from(id).toString("hex");
|
|
123
|
+
}
|
|
51
124
|
|
|
52
125
|
// src/write.ts
|
|
53
|
-
function
|
|
54
|
-
db.run("INSERT OR IGNORE INTO
|
|
126
|
+
function insertRun(db, id, gitCommit, startedAt) {
|
|
127
|
+
db.run("INSERT OR IGNORE INTO runs (id, started_at, git_commit) VALUES (?, ?, ?)", [
|
|
128
|
+
toIdBlob(id),
|
|
129
|
+
startedAt,
|
|
130
|
+
gitCommit
|
|
131
|
+
]);
|
|
55
132
|
}
|
|
56
133
|
function upsertFeature(db, id, path, name) {
|
|
57
|
-
db.run("INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)", [id, path, name]);
|
|
134
|
+
db.run("INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)", [toIdBlob(id), path, name]);
|
|
58
135
|
}
|
|
59
|
-
function upsertScenario(db, id, featureId, name) {
|
|
60
|
-
db.run(
|
|
136
|
+
function upsertScenario(db, id, featureId, index, name, refs = {}) {
|
|
137
|
+
db.run(
|
|
138
|
+
`INSERT OR REPLACE INTO scenarios (
|
|
139
|
+
id, feature, "index", name, rule, outline, example_row, example_index
|
|
140
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
141
|
+
[
|
|
142
|
+
toIdBlob(id),
|
|
143
|
+
toIdBlob(featureId),
|
|
144
|
+
index,
|
|
145
|
+
name,
|
|
146
|
+
refs.rule ? toIdBlob(refs.rule) : null,
|
|
147
|
+
refs.outline ? toIdBlob(refs.outline) : null,
|
|
148
|
+
refs.exampleRow ? toIdBlob(refs.exampleRow) : null,
|
|
149
|
+
refs.exampleIndex ?? null
|
|
150
|
+
]
|
|
151
|
+
);
|
|
61
152
|
}
|
|
62
|
-
function upsertStep(db, id,
|
|
63
|
-
db.run("INSERT OR REPLACE INTO steps (id,
|
|
153
|
+
function upsertStep(db, id, text) {
|
|
154
|
+
db.run("INSERT OR REPLACE INTO steps (id, text) VALUES (?, ?)", [toIdBlob(id), text]);
|
|
64
155
|
}
|
|
65
|
-
function
|
|
66
|
-
db.run(
|
|
156
|
+
function upsertScenarioStep(db, scenarioId, index, stepId) {
|
|
157
|
+
db.run('INSERT OR REPLACE INTO scenario_steps (scenario, "index", step) VALUES (?, ?, ?)', [
|
|
158
|
+
toIdBlob(scenarioId),
|
|
159
|
+
index,
|
|
160
|
+
toIdBlob(stepId)
|
|
161
|
+
]);
|
|
67
162
|
}
|
|
68
|
-
function
|
|
69
|
-
db.run("
|
|
163
|
+
function insertTest(db, id, runId, scenarioId, startedAt) {
|
|
164
|
+
db.run("INSERT INTO tests (id, run, scenario, started_at) VALUES (?, ?, ?, ?)", [
|
|
165
|
+
toIdBlob(id),
|
|
166
|
+
toIdBlob(runId),
|
|
167
|
+
toIdBlob(scenarioId),
|
|
168
|
+
startedAt
|
|
169
|
+
]);
|
|
70
170
|
}
|
|
71
|
-
function
|
|
72
|
-
db.run("
|
|
171
|
+
function finaliseTest(db, id, status, failedStepIndex, error) {
|
|
172
|
+
db.run("UPDATE tests SET status = ?, failed_step_index = ?, error = ? WHERE id = ?", [
|
|
173
|
+
status,
|
|
174
|
+
failedStepIndex ?? null,
|
|
175
|
+
error ?? null,
|
|
176
|
+
toIdBlob(id)
|
|
177
|
+
]);
|
|
73
178
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function computeFeatureId(uri) {
|
|
82
|
-
return v5(uri, UUID_NAMESPACE);
|
|
179
|
+
function insertArtifact(db, id, testId, stepIndex, filename) {
|
|
180
|
+
db.run("INSERT INTO artifacts (id, test, step_index, filename) VALUES (?, ?, ?, ?)", [
|
|
181
|
+
toIdBlob(id),
|
|
182
|
+
toIdBlob(testId),
|
|
183
|
+
stepIndex,
|
|
184
|
+
filename
|
|
185
|
+
]);
|
|
83
186
|
}
|
|
84
187
|
|
|
85
188
|
// src/read.ts
|
|
86
|
-
function
|
|
87
|
-
const conditions = ["
|
|
88
|
-
const params = [scenarioId];
|
|
189
|
+
function findLastTest(db, scenarioId, status, allowedCommits) {
|
|
190
|
+
const conditions = ["t.scenario = ?"];
|
|
191
|
+
const params = [toIdBlob(scenarioId)];
|
|
89
192
|
if (status !== void 0) {
|
|
90
|
-
conditions.push("
|
|
193
|
+
conditions.push("t.status = ?");
|
|
91
194
|
params.push(status);
|
|
92
195
|
}
|
|
93
196
|
if (allowedCommits !== void 0) {
|
|
94
|
-
conditions.push("
|
|
197
|
+
conditions.push("r.git_commit IN (SELECT value FROM json_each(?))");
|
|
95
198
|
params.push(JSON.stringify(allowedCommits));
|
|
96
199
|
}
|
|
97
200
|
const sql = `
|
|
98
|
-
SELECT
|
|
99
|
-
FROM
|
|
100
|
-
JOIN
|
|
201
|
+
SELECT t.id, r.git_commit
|
|
202
|
+
FROM tests t
|
|
203
|
+
JOIN runs r ON t.run = r.id
|
|
101
204
|
WHERE ${conditions.join(" AND ")}
|
|
102
|
-
ORDER BY
|
|
205
|
+
ORDER BY t.started_at DESC
|
|
103
206
|
LIMIT 1
|
|
104
207
|
`;
|
|
105
208
|
const row = db.get(sql, params);
|
|
106
209
|
if (!row) return null;
|
|
107
|
-
return { id: row.id, gitCommit: row.git_commit };
|
|
210
|
+
return { id: fromIdBlob(row.id), gitCommit: row.git_commit };
|
|
211
|
+
}
|
|
212
|
+
function findLastPassingBaseline(db, scenarioId, allowedCommits) {
|
|
213
|
+
const test = findLastTest(db, scenarioId, "passed", allowedCommits);
|
|
214
|
+
if (!test) return null;
|
|
215
|
+
return { testId: test.id, gitCommit: test.gitCommit };
|
|
108
216
|
}
|
|
109
|
-
function findArtifacts(db,
|
|
110
|
-
const conditions = ["a.
|
|
111
|
-
const params = [
|
|
217
|
+
function findArtifacts(db, testId, stepId) {
|
|
218
|
+
const conditions = ["a.test = ?"];
|
|
219
|
+
const params = [toIdBlob(testId)];
|
|
112
220
|
if (stepId !== void 0) {
|
|
113
|
-
conditions.push("
|
|
114
|
-
params.push(stepId);
|
|
221
|
+
conditions.push("ss.step = ?");
|
|
222
|
+
params.push(toIdBlob(stepId));
|
|
115
223
|
}
|
|
116
224
|
const sql = `
|
|
117
|
-
SELECT a.filename,
|
|
225
|
+
SELECT a.filename, ss.step, a.step_index
|
|
118
226
|
FROM artifacts a
|
|
119
|
-
JOIN
|
|
227
|
+
JOIN tests t ON a.test = t.id
|
|
228
|
+
JOIN scenario_steps ss ON ss.scenario = t.scenario AND ss."index" = a.step_index
|
|
120
229
|
WHERE ${conditions.join(" AND ")}
|
|
121
|
-
ORDER BY
|
|
230
|
+
ORDER BY a.step_index ASC
|
|
122
231
|
`;
|
|
123
232
|
const rows = db.all(sql, params);
|
|
124
|
-
return rows.map((r) => ({ filename: r.filename, stepId: r.
|
|
233
|
+
return rows.map((r) => ({ filename: r.filename, stepId: fromIdBlob(r.step), stepIdx: r.step_index }));
|
|
234
|
+
}
|
|
235
|
+
function findLastRun(db) {
|
|
236
|
+
const run = db.get(
|
|
237
|
+
`
|
|
238
|
+
SELECT id, started_at, git_commit
|
|
239
|
+
FROM runs
|
|
240
|
+
ORDER BY started_at DESC
|
|
241
|
+
LIMIT 1
|
|
242
|
+
`
|
|
243
|
+
);
|
|
244
|
+
if (!run) return null;
|
|
245
|
+
const tests = db.all(
|
|
246
|
+
`
|
|
247
|
+
SELECT
|
|
248
|
+
t.id,
|
|
249
|
+
t.scenario,
|
|
250
|
+
s.name AS scenario_name,
|
|
251
|
+
s.rule,
|
|
252
|
+
s.outline,
|
|
253
|
+
s.example_row,
|
|
254
|
+
s.example_index,
|
|
255
|
+
f.id AS feature_id,
|
|
256
|
+
f.path AS feature_path,
|
|
257
|
+
f.name AS feature_name,
|
|
258
|
+
t.status,
|
|
259
|
+
t.started_at,
|
|
260
|
+
t.failed_step_index,
|
|
261
|
+
t.error
|
|
262
|
+
FROM tests t
|
|
263
|
+
JOIN scenarios s ON s.id = t.scenario
|
|
264
|
+
JOIN features f ON f.id = s.feature
|
|
265
|
+
WHERE t.run = ?
|
|
266
|
+
ORDER BY t.started_at ASC
|
|
267
|
+
`,
|
|
268
|
+
[run.id]
|
|
269
|
+
);
|
|
270
|
+
const withSteps = tests.map((test) => {
|
|
271
|
+
const steps = db.all(
|
|
272
|
+
`
|
|
273
|
+
SELECT ss.step, ss."index", st.text
|
|
274
|
+
FROM scenario_steps ss
|
|
275
|
+
JOIN steps st ON st.id = ss.step
|
|
276
|
+
WHERE ss.scenario = ?
|
|
277
|
+
ORDER BY ss."index" ASC
|
|
278
|
+
`,
|
|
279
|
+
[test.scenario]
|
|
280
|
+
);
|
|
281
|
+
return {
|
|
282
|
+
id: fromIdBlob(test.id),
|
|
283
|
+
scenarioId: fromIdBlob(test.scenario),
|
|
284
|
+
scenarioName: test.scenario_name,
|
|
285
|
+
featureId: fromIdBlob(test.feature_id),
|
|
286
|
+
featurePath: test.feature_path,
|
|
287
|
+
featureName: test.feature_name,
|
|
288
|
+
status: test.status,
|
|
289
|
+
startedAt: test.started_at,
|
|
290
|
+
failedStepIndex: test.failed_step_index,
|
|
291
|
+
error: test.error,
|
|
292
|
+
ruleId: test.rule ? fromIdBlob(test.rule) : null,
|
|
293
|
+
outlineId: test.outline ? fromIdBlob(test.outline) : null,
|
|
294
|
+
exampleRowId: test.example_row ? fromIdBlob(test.example_row) : null,
|
|
295
|
+
exampleIndex: test.example_index,
|
|
296
|
+
steps: steps.map((step) => ({
|
|
297
|
+
id: fromIdBlob(step.step),
|
|
298
|
+
index: step.index,
|
|
299
|
+
text: step.text
|
|
300
|
+
}))
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
id: fromIdBlob(run.id),
|
|
305
|
+
startedAt: run.started_at,
|
|
306
|
+
gitCommit: run.git_commit,
|
|
307
|
+
tests: withSteps
|
|
308
|
+
};
|
|
125
309
|
}
|
|
126
310
|
|
|
127
|
-
export { computeFeatureId, computeScenarioId, computeStepId,
|
|
311
|
+
export { computeExampleRowId, computeFeatureId, computeOutlineId, computeRuleId, computeScenarioId, computeStepId, finaliseTest, findArtifacts, findLastPassingBaseline, findLastRun, findLastTest, fromIdBlob, insertArtifact, insertRun, insertTest, openStore, toIdBlob, upsertFeature, upsertScenario, upsertScenarioStep, upsertStep };
|
|
128
312
|
//# sourceMappingURL=index.js.map
|
|
129
313
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db.ts","../src/write.ts","../src/ids.ts","../src/read.ts"],"names":["uuidv5"],"mappings":"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","file":"index.js","sourcesContent":["import nodeWasm from 'node-sqlite3-wasm';\nconst { Database } = nodeWasm;\nexport type Database = InstanceType<typeof nodeWasm.Database>;\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n started_at INTEGER NOT NULL,\n git_commit TEXT\n);\nCREATE TABLE IF NOT EXISTS features (\n id TEXT PRIMARY KEY,\n path TEXT NOT NULL,\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS scenarios (\n id TEXT PRIMARY KEY,\n feature_id TEXT NOT NULL REFERENCES features(id),\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS steps (\n id TEXT PRIMARY KEY,\n scenario_id TEXT NOT NULL REFERENCES scenarios(id),\n idx INTEGER NOT NULL,\n text TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS runs (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n scenario_id TEXT NOT NULL REFERENCES scenarios(id),\n status TEXT NOT NULL DEFAULT 'running',\n failed_step_id TEXT REFERENCES steps(id),\n error TEXT,\n started_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS artifacts (\n id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL REFERENCES runs(id),\n step_id TEXT NOT NULL REFERENCES steps(id),\n filename TEXT NOT NULL\n);\n`;\n\nexport function openStore(path = '.letsrunit/letsrunit.db'): Database {\n const db = new Database(path);\n db.run('PRAGMA journal_mode = WAL');\n db.run('PRAGMA foreign_keys = ON');\n db.exec(SCHEMA);\n return db;\n}\n","import type { Database } from './db';\n\nexport function insertSession(db: Database, id: string, gitCommit: string | null, startedAt: number): void {\n db.run('INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)', [id, startedAt, gitCommit]);\n}\n\nexport function upsertFeature(db: Database, id: string, path: string, name: string): void {\n db.run('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)', [id, path, name]);\n}\n\nexport function upsertScenario(db: Database, id: string, featureId: string, name: string): void {\n db.run('INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)', [id, featureId, name]);\n}\n\nexport function upsertStep(db: Database, id: string, scenarioId: string, idx: number, text: string): void {\n db.run('INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)', [id, scenarioId, idx, text]);\n}\n\nexport function insertRun(db: Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void {\n db.run('INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)', [id, sessionId, scenarioId, startedAt]);\n}\n\nexport function finaliseRun(db: Database, id: string, status: string, failedStepId?: string, error?: string): void {\n db.run('UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?', [status, failedStepId ?? null, error ?? null, id]);\n}\n\nexport function insertArtifact(db: Database, id: string, runId: string, stepId: string, filename: string): void {\n db.run('INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)', [id, runId, stepId, filename]);\n}\n","import { v5 as uuidv5 } from 'uuid';\n\nconst UUID_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';\n\nexport function computeStepId(normalizedText: string): string {\n return uuidv5(normalizedText, UUID_NAMESPACE);\n}\n\nexport function computeScenarioId(stepIds: string[]): string {\n return uuidv5(stepIds.join(':'), UUID_NAMESPACE);\n}\n\nexport function computeFeatureId(uri: string): string {\n return uuidv5(uri, UUID_NAMESPACE);\n}\n","import type { Database } from './db';\n\nexport function findLastRun(\n db: Database,\n scenarioId: string,\n status?: string,\n allowedCommits?: string[],\n): { id: string; gitCommit: string | null } | null {\n const conditions: string[] = ['r.scenario_id = ?'];\n const params: (string | number | null)[] = [scenarioId];\n\n if (status !== undefined) {\n conditions.push('r.status = ?');\n params.push(status);\n }\n\n if (allowedCommits !== undefined) {\n conditions.push('s.git_commit IN (SELECT value FROM json_each(?))');\n params.push(JSON.stringify(allowedCommits));\n }\n\n const sql = `\n SELECT r.id, s.git_commit\n FROM runs r\n JOIN sessions s ON r.session_id = s.id\n WHERE ${conditions.join(' AND ')}\n ORDER BY r.started_at DESC\n LIMIT 1\n `;\n\n const row = db.get(sql, params) as { id: string; git_commit: string | null } | undefined;\n if (!row) return null;\n return { id: row.id, gitCommit: row.git_commit };\n}\n\nexport function findArtifacts(\n db: Database,\n runId: string,\n stepId?: string,\n): Array<{ filename: string; stepId: string; stepIdx: number }> {\n const conditions: string[] = ['a.run_id = ?'];\n const params: (string | number | null)[] = [runId];\n\n if (stepId !== undefined) {\n conditions.push('a.step_id = ?');\n params.push(stepId);\n }\n\n const sql = `\n SELECT a.filename, a.step_id, st.idx\n FROM artifacts a\n JOIN steps st ON a.step_id = st.id\n WHERE ${conditions.join(' AND ')}\n ORDER BY st.idx ASC\n `;\n\n const rows = db.all(sql, params) as Array<{ filename: string; step_id: string; idx: number }>;\n return rows.map((r) => ({ filename: r.filename, stepId: r.step_id, stepIdx: r.idx }));\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/db.ts","../src/ids.ts","../src/write.ts","../src/read.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import nodeWasm from 'node-sqlite3-wasm';\nconst { Database } = nodeWasm;\nexport type Database = InstanceType<typeof nodeWasm.Database>;\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS runs (\n id BLOB PRIMARY KEY,\n started_at INTEGER NOT NULL,\n git_commit TEXT\n);\nCREATE TABLE IF NOT EXISTS features (\n id BLOB PRIMARY KEY,\n path TEXT NOT NULL,\n name TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS scenarios (\n id BLOB PRIMARY KEY,\n feature BLOB NOT NULL REFERENCES features(id),\n \"index\" INTEGER NOT NULL,\n name TEXT NOT NULL,\n rule BLOB,\n outline BLOB,\n example_row BLOB,\n example_index INTEGER,\n UNIQUE(feature, \"index\"),\n CHECK (outline IS NULL OR example_row IS NOT NULL),\n CHECK (example_row IS NULL OR outline IS NOT NULL)\n);\nCREATE TABLE IF NOT EXISTS steps (\n id BLOB PRIMARY KEY,\n text TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS scenario_steps (\n scenario BLOB NOT NULL REFERENCES scenarios(id),\n \"index\" INTEGER NOT NULL,\n step BLOB NOT NULL REFERENCES steps(id),\n PRIMARY KEY (scenario, \"index\")\n);\nCREATE TABLE IF NOT EXISTS tests (\n id BLOB PRIMARY KEY,\n run BLOB NOT NULL REFERENCES runs(id),\n scenario BLOB NOT NULL REFERENCES scenarios(id),\n status TEXT NOT NULL DEFAULT 'running',\n failed_step_index INTEGER,\n error TEXT,\n started_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS artifacts (\n id BLOB PRIMARY KEY,\n test BLOB NOT NULL REFERENCES tests(id),\n step_index INTEGER NOT NULL,\n filename TEXT NOT NULL\n);\n`;\n\nexport function openStore(path = '.letsrunit/letsrunit.db'): Database {\n const db = new Database(path);\n db.run('PRAGMA journal_mode = WAL');\n db.run('PRAGMA foreign_keys = ON');\n db.exec(SCHEMA);\n return db;\n}\n","import { createHash } from 'node:crypto';\n\nconst TAGS = {\n step: 0x0001,\n scenario: 0x0002,\n feature: 0x0003,\n rule: 0x0004,\n outline: 0x0005,\n exampleRow: 0x0006,\n} as const;\n\nfunction encodeTag(tag: number): Buffer {\n const buf = Buffer.allocUnsafe(2);\n buf.writeUInt16BE(tag, 0);\n return buf;\n}\n\nfunction hashBytes(tag: number, payload: Buffer): string {\n return createHash('sha256').update(encodeTag(tag)).update(payload).digest('hex');\n}\n\nfunction concatHexIds(ids: string[]): Buffer {\n if (ids.length === 0) return Buffer.alloc(0);\n return Buffer.concat(ids.map((id) => Buffer.from(id, 'hex')));\n}\n\nfunction encodeStrings(parts: string[]): Buffer {\n const encoded: Buffer[] = [];\n for (const part of parts) {\n const chunk = Buffer.from(part, 'utf8');\n const len = Buffer.allocUnsafe(4);\n len.writeUInt32BE(chunk.length, 0);\n encoded.push(len, chunk);\n }\n\n return Buffer.concat(encoded);\n}\n\nexport function computeStepId(normalizedText: string): string {\n return hashBytes(TAGS.step, Buffer.from(normalizedText, 'utf8'));\n}\n\nexport function computeScenarioId(stepIds: string[]): string {\n return hashBytes(TAGS.scenario, concatHexIds(stepIds));\n}\n\nexport function computeFeatureId(scenarioIds: string[]): string {\n return hashBytes(TAGS.feature, concatHexIds(scenarioIds));\n}\n\nexport function computeRuleId(scenarioIds: string[]): string {\n return hashBytes(TAGS.rule, concatHexIds(scenarioIds));\n}\n\nexport function computeOutlineId(stepIds: string[]): string {\n return hashBytes(TAGS.outline, concatHexIds(stepIds));\n}\n\nexport function computeExampleRowId(values: string[]): string {\n return hashBytes(TAGS.exampleRow, encodeStrings(values));\n}\n\nfunction hashId(input: string): Buffer {\n return createHash('sha256').update(input, 'utf8').digest();\n}\n\nexport function toIdBlob(id: string): Buffer {\n const normalized = id.toLowerCase();\n if (/^[0-9a-f]{64}$/.test(normalized)) {\n return Buffer.from(normalized, 'hex');\n }\n\n return hashId(id);\n}\n\nexport function fromIdBlob(id: Uint8Array): string {\n return Buffer.from(id).toString('hex');\n}\n","import type { Database } from './db';\n\nimport { toIdBlob } from './ids';\n\nexport function insertRun(db: Database, id: string, gitCommit: string | null, startedAt: number): void {\n db.run('INSERT OR IGNORE INTO runs (id, started_at, git_commit) VALUES (?, ?, ?)', [\n toIdBlob(id),\n startedAt,\n gitCommit,\n ]);\n}\n\nexport function upsertFeature(db: Database, id: string, path: string, name: string): void {\n db.run('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)', [toIdBlob(id), path, name]);\n}\n\ninterface ScenarioRefs {\n rule?: string;\n outline?: string;\n exampleRow?: string;\n exampleIndex?: number;\n}\n\nexport function upsertScenario(\n db: Database,\n id: string,\n featureId: string,\n index: number,\n name: string,\n refs: ScenarioRefs = {},\n): void {\n db.run(\n `INSERT OR REPLACE INTO scenarios (\n id, feature, \"index\", name, rule, outline, example_row, example_index\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n toIdBlob(id),\n toIdBlob(featureId),\n index,\n name,\n refs.rule ? toIdBlob(refs.rule) : null,\n refs.outline ? toIdBlob(refs.outline) : null,\n refs.exampleRow ? toIdBlob(refs.exampleRow) : null,\n refs.exampleIndex ?? null,\n ],\n );\n}\n\nexport function upsertStep(db: Database, id: string, text: string): void {\n db.run('INSERT OR REPLACE INTO steps (id, text) VALUES (?, ?)', [toIdBlob(id), text]);\n}\n\nexport function upsertScenarioStep(db: Database, scenarioId: string, index: number, stepId: string): void {\n db.run('INSERT OR REPLACE INTO scenario_steps (scenario, \"index\", step) VALUES (?, ?, ?)', [\n toIdBlob(scenarioId),\n index,\n toIdBlob(stepId),\n ]);\n}\n\nexport function insertTest(db: Database, id: string, runId: string, scenarioId: string, startedAt: number): void {\n db.run('INSERT INTO tests (id, run, scenario, started_at) VALUES (?, ?, ?, ?)', [\n toIdBlob(id),\n toIdBlob(runId),\n toIdBlob(scenarioId),\n startedAt,\n ]);\n}\n\nexport function finaliseTest(db: Database, id: string, status: string, failedStepIndex?: number, error?: string): void {\n db.run('UPDATE tests SET status = ?, failed_step_index = ?, error = ? WHERE id = ?', [\n status,\n failedStepIndex ?? null,\n error ?? null,\n toIdBlob(id),\n ]);\n}\n\nexport function insertArtifact(db: Database, id: string, testId: string, stepIndex: number, filename: string): void {\n db.run('INSERT INTO artifacts (id, test, step_index, filename) VALUES (?, ?, ?, ?)', [\n toIdBlob(id),\n toIdBlob(testId),\n stepIndex,\n filename,\n ]);\n}\n","import type { Database } from './db';\nimport { fromIdBlob, toIdBlob } from './ids';\n\nexport function findLastTest(\n db: Database,\n scenarioId: string,\n status?: string,\n allowedCommits?: string[],\n): { id: string; gitCommit: string | null } | null {\n const conditions: string[] = ['t.scenario = ?'];\n const params: (string | number | null | Uint8Array)[] = [toIdBlob(scenarioId)];\n\n if (status !== undefined) {\n conditions.push('t.status = ?');\n params.push(status);\n }\n\n if (allowedCommits !== undefined) {\n conditions.push('r.git_commit IN (SELECT value FROM json_each(?))');\n params.push(JSON.stringify(allowedCommits));\n }\n\n const sql = `\n SELECT t.id, r.git_commit\n FROM tests t\n JOIN runs r ON t.run = r.id\n WHERE ${conditions.join(' AND ')}\n ORDER BY t.started_at DESC\n LIMIT 1\n `;\n\n const row = db.get(sql, params) as { id: Uint8Array; git_commit: string | null } | undefined;\n if (!row) return null;\n return { id: fromIdBlob(row.id), gitCommit: row.git_commit };\n}\n\nexport function findLastPassingBaseline(\n db: Database,\n scenarioId: string,\n allowedCommits?: string[],\n): { testId: string; gitCommit: string | null } | null {\n const test = findLastTest(db, scenarioId, 'passed', allowedCommits);\n if (!test) return null;\n return { testId: test.id, gitCommit: test.gitCommit };\n}\n\nexport function findArtifacts(\n db: Database,\n testId: string,\n stepId?: string,\n): Array<{ filename: string; stepId: string; stepIdx: number }> {\n const conditions: string[] = ['a.test = ?'];\n const params: (string | number | null | Uint8Array)[] = [toIdBlob(testId)];\n\n if (stepId !== undefined) {\n conditions.push('ss.step = ?');\n params.push(toIdBlob(stepId));\n }\n\n const sql = `\n SELECT a.filename, ss.step, a.step_index\n FROM artifacts a\n JOIN tests t ON a.test = t.id\n JOIN scenario_steps ss ON ss.scenario = t.scenario AND ss.\"index\" = a.step_index\n WHERE ${conditions.join(' AND ')}\n ORDER BY a.step_index ASC\n `;\n\n const rows = db.all(sql, params) as Array<{ filename: string; step: Uint8Array; step_index: number }>;\n return rows.map((r) => ({ filename: r.filename, stepId: fromIdBlob(r.step), stepIdx: r.step_index }));\n}\n\nexport interface LastRunStep {\n id: string;\n index: number;\n text: string;\n}\n\nexport interface LastRunTest {\n id: string;\n scenarioId: string;\n scenarioName: string;\n featureId: string;\n featurePath: string;\n featureName: string;\n status: string;\n startedAt: number;\n failedStepIndex: number | null;\n error: string | null;\n ruleId: string | null;\n outlineId: string | null;\n exampleRowId: string | null;\n exampleIndex: number | null;\n steps: LastRunStep[];\n}\n\nexport interface LastRun {\n id: string;\n startedAt: number;\n gitCommit: string | null;\n tests: LastRunTest[];\n}\n\nexport function findLastRun(db: Database): LastRun | null {\n const run = db.get(\n `\n SELECT id, started_at, git_commit\n FROM runs\n ORDER BY started_at DESC\n LIMIT 1\n `,\n ) as { id: Uint8Array; started_at: number; git_commit: string | null } | undefined;\n\n if (!run) return null;\n\n const tests = db.all(\n `\n SELECT\n t.id,\n t.scenario,\n s.name AS scenario_name,\n s.rule,\n s.outline,\n s.example_row,\n s.example_index,\n f.id AS feature_id,\n f.path AS feature_path,\n f.name AS feature_name,\n t.status,\n t.started_at,\n t.failed_step_index,\n t.error\n FROM tests t\n JOIN scenarios s ON s.id = t.scenario\n JOIN features f ON f.id = s.feature\n WHERE t.run = ?\n ORDER BY t.started_at ASC\n `,\n [run.id],\n ) as Array<{\n id: Uint8Array;\n scenario: Uint8Array;\n scenario_name: string;\n rule: Uint8Array | null;\n outline: Uint8Array | null;\n example_row: Uint8Array | null;\n example_index: number | null;\n feature_id: Uint8Array;\n feature_path: string;\n feature_name: string;\n status: string;\n started_at: number;\n failed_step_index: number | null;\n error: string | null;\n }>;\n\n const withSteps = tests.map((test) => {\n const steps = db.all(\n `\n SELECT ss.step, ss.\"index\", st.text\n FROM scenario_steps ss\n JOIN steps st ON st.id = ss.step\n WHERE ss.scenario = ?\n ORDER BY ss.\"index\" ASC\n `,\n [test.scenario],\n ) as Array<{ step: Uint8Array; index: number; text: string }>;\n\n return {\n id: fromIdBlob(test.id),\n scenarioId: fromIdBlob(test.scenario),\n scenarioName: test.scenario_name,\n featureId: fromIdBlob(test.feature_id),\n featurePath: test.feature_path,\n featureName: test.feature_name,\n status: test.status,\n startedAt: test.started_at,\n failedStepIndex: test.failed_step_index,\n error: test.error,\n ruleId: test.rule ? fromIdBlob(test.rule) : null,\n outlineId: test.outline ? fromIdBlob(test.outline) : null,\n exampleRowId: test.example_row ? fromIdBlob(test.example_row) : null,\n exampleIndex: test.example_index,\n steps: steps.map((step) => ({\n id: fromIdBlob(step.step),\n index: step.index,\n text: step.text,\n })),\n } satisfies LastRunTest;\n });\n\n return {\n id: fromIdBlob(run.id),\n startedAt: run.started_at,\n gitCommit: run.git_commit,\n tests: withSteps,\n };\n}\n"]}
|
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -3,41 +3,53 @@ const { Database } = nodeWasm;
|
|
|
3
3
|
export type Database = InstanceType<typeof nodeWasm.Database>;
|
|
4
4
|
|
|
5
5
|
const SCHEMA = `
|
|
6
|
-
CREATE TABLE IF NOT EXISTS
|
|
7
|
-
id
|
|
6
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
7
|
+
id BLOB PRIMARY KEY,
|
|
8
8
|
started_at INTEGER NOT NULL,
|
|
9
9
|
git_commit TEXT
|
|
10
10
|
);
|
|
11
11
|
CREATE TABLE IF NOT EXISTS features (
|
|
12
|
-
id
|
|
12
|
+
id BLOB PRIMARY KEY,
|
|
13
13
|
path TEXT NOT NULL,
|
|
14
14
|
name TEXT NOT NULL
|
|
15
15
|
);
|
|
16
16
|
CREATE TABLE IF NOT EXISTS scenarios (
|
|
17
|
-
id
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
id BLOB PRIMARY KEY,
|
|
18
|
+
feature BLOB NOT NULL REFERENCES features(id),
|
|
19
|
+
"index" INTEGER NOT NULL,
|
|
20
|
+
name TEXT NOT NULL,
|
|
21
|
+
rule BLOB,
|
|
22
|
+
outline BLOB,
|
|
23
|
+
example_row BLOB,
|
|
24
|
+
example_index INTEGER,
|
|
25
|
+
UNIQUE(feature, "index"),
|
|
26
|
+
CHECK (outline IS NULL OR example_row IS NOT NULL),
|
|
27
|
+
CHECK (example_row IS NULL OR outline IS NOT NULL)
|
|
20
28
|
);
|
|
21
29
|
CREATE TABLE IF NOT EXISTS steps (
|
|
22
|
-
id
|
|
23
|
-
|
|
24
|
-
idx INTEGER NOT NULL,
|
|
25
|
-
text TEXT NOT NULL
|
|
30
|
+
id BLOB PRIMARY KEY,
|
|
31
|
+
text TEXT NOT NULL
|
|
26
32
|
);
|
|
27
|
-
CREATE TABLE IF NOT EXISTS
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
CREATE TABLE IF NOT EXISTS scenario_steps (
|
|
34
|
+
scenario BLOB NOT NULL REFERENCES scenarios(id),
|
|
35
|
+
"index" INTEGER NOT NULL,
|
|
36
|
+
step BLOB NOT NULL REFERENCES steps(id),
|
|
37
|
+
PRIMARY KEY (scenario, "index")
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS tests (
|
|
40
|
+
id BLOB PRIMARY KEY,
|
|
41
|
+
run BLOB NOT NULL REFERENCES runs(id),
|
|
42
|
+
scenario BLOB NOT NULL REFERENCES scenarios(id),
|
|
43
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
44
|
+
failed_step_index INTEGER,
|
|
45
|
+
error TEXT,
|
|
46
|
+
started_at INTEGER NOT NULL
|
|
35
47
|
);
|
|
36
48
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
37
|
-
id
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
filename
|
|
49
|
+
id BLOB PRIMARY KEY,
|
|
50
|
+
test BLOB NOT NULL REFERENCES tests(id),
|
|
51
|
+
step_index INTEGER NOT NULL,
|
|
52
|
+
filename TEXT NOT NULL
|
|
41
53
|
);
|
|
42
54
|
`;
|
|
43
55
|
|
package/src/ids.ts
CHANGED
|
@@ -1,15 +1,78 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const TAGS = {
|
|
4
|
+
step: 0x0001,
|
|
5
|
+
scenario: 0x0002,
|
|
6
|
+
feature: 0x0003,
|
|
7
|
+
rule: 0x0004,
|
|
8
|
+
outline: 0x0005,
|
|
9
|
+
exampleRow: 0x0006,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
function encodeTag(tag: number): Buffer {
|
|
13
|
+
const buf = Buffer.allocUnsafe(2);
|
|
14
|
+
buf.writeUInt16BE(tag, 0);
|
|
15
|
+
return buf;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hashBytes(tag: number, payload: Buffer): string {
|
|
19
|
+
return createHash('sha256').update(encodeTag(tag)).update(payload).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function concatHexIds(ids: string[]): Buffer {
|
|
23
|
+
if (ids.length === 0) return Buffer.alloc(0);
|
|
24
|
+
return Buffer.concat(ids.map((id) => Buffer.from(id, 'hex')));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function encodeStrings(parts: string[]): Buffer {
|
|
28
|
+
const encoded: Buffer[] = [];
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const chunk = Buffer.from(part, 'utf8');
|
|
31
|
+
const len = Buffer.allocUnsafe(4);
|
|
32
|
+
len.writeUInt32BE(chunk.length, 0);
|
|
33
|
+
encoded.push(len, chunk);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Buffer.concat(encoded);
|
|
37
|
+
}
|
|
4
38
|
|
|
5
39
|
export function computeStepId(normalizedText: string): string {
|
|
6
|
-
return
|
|
40
|
+
return hashBytes(TAGS.step, Buffer.from(normalizedText, 'utf8'));
|
|
7
41
|
}
|
|
8
42
|
|
|
9
43
|
export function computeScenarioId(stepIds: string[]): string {
|
|
10
|
-
return
|
|
44
|
+
return hashBytes(TAGS.scenario, concatHexIds(stepIds));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function computeFeatureId(scenarioIds: string[]): string {
|
|
48
|
+
return hashBytes(TAGS.feature, concatHexIds(scenarioIds));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function computeRuleId(scenarioIds: string[]): string {
|
|
52
|
+
return hashBytes(TAGS.rule, concatHexIds(scenarioIds));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function computeOutlineId(stepIds: string[]): string {
|
|
56
|
+
return hashBytes(TAGS.outline, concatHexIds(stepIds));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function computeExampleRowId(values: string[]): string {
|
|
60
|
+
return hashBytes(TAGS.exampleRow, encodeStrings(values));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hashId(input: string): Buffer {
|
|
64
|
+
return createHash('sha256').update(input, 'utf8').digest();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toIdBlob(id: string): Buffer {
|
|
68
|
+
const normalized = id.toLowerCase();
|
|
69
|
+
if (/^[0-9a-f]{64}$/.test(normalized)) {
|
|
70
|
+
return Buffer.from(normalized, 'hex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return hashId(id);
|
|
11
74
|
}
|
|
12
75
|
|
|
13
|
-
export function
|
|
14
|
-
return
|
|
76
|
+
export function fromIdBlob(id: Uint8Array): string {
|
|
77
|
+
return Buffer.from(id).toString('hex');
|
|
15
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export * from './db';
|
|
2
|
+
export * from './write';
|
|
3
|
+
export * from './ids';
|
|
4
|
+
export * from './read';
|
package/src/read.ts
CHANGED
|
@@ -1,59 +1,198 @@
|
|
|
1
1
|
import type { Database } from './db';
|
|
2
|
+
import { fromIdBlob, toIdBlob } from './ids';
|
|
2
3
|
|
|
3
|
-
export function
|
|
4
|
+
export function findLastTest(
|
|
4
5
|
db: Database,
|
|
5
6
|
scenarioId: string,
|
|
6
7
|
status?: string,
|
|
7
8
|
allowedCommits?: string[],
|
|
8
9
|
): { id: string; gitCommit: string | null } | null {
|
|
9
|
-
const conditions: string[] = ['
|
|
10
|
-
const params: (string | number | null)[] = [scenarioId];
|
|
10
|
+
const conditions: string[] = ['t.scenario = ?'];
|
|
11
|
+
const params: (string | number | null | Uint8Array)[] = [toIdBlob(scenarioId)];
|
|
11
12
|
|
|
12
13
|
if (status !== undefined) {
|
|
13
|
-
conditions.push('
|
|
14
|
+
conditions.push('t.status = ?');
|
|
14
15
|
params.push(status);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
if (allowedCommits !== undefined) {
|
|
18
|
-
conditions.push('
|
|
19
|
+
conditions.push('r.git_commit IN (SELECT value FROM json_each(?))');
|
|
19
20
|
params.push(JSON.stringify(allowedCommits));
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const sql = `
|
|
23
|
-
SELECT
|
|
24
|
-
FROM
|
|
25
|
-
JOIN
|
|
24
|
+
SELECT t.id, r.git_commit
|
|
25
|
+
FROM tests t
|
|
26
|
+
JOIN runs r ON t.run = r.id
|
|
26
27
|
WHERE ${conditions.join(' AND ')}
|
|
27
|
-
ORDER BY
|
|
28
|
+
ORDER BY t.started_at DESC
|
|
28
29
|
LIMIT 1
|
|
29
30
|
`;
|
|
30
31
|
|
|
31
|
-
const row = db.get(sql, params) as { id:
|
|
32
|
+
const row = db.get(sql, params) as { id: Uint8Array; git_commit: string | null } | undefined;
|
|
32
33
|
if (!row) return null;
|
|
33
|
-
return { id: row.id, gitCommit: row.git_commit };
|
|
34
|
+
return { id: fromIdBlob(row.id), gitCommit: row.git_commit };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function findLastPassingBaseline(
|
|
38
|
+
db: Database,
|
|
39
|
+
scenarioId: string,
|
|
40
|
+
allowedCommits?: string[],
|
|
41
|
+
): { testId: string; gitCommit: string | null } | null {
|
|
42
|
+
const test = findLastTest(db, scenarioId, 'passed', allowedCommits);
|
|
43
|
+
if (!test) return null;
|
|
44
|
+
return { testId: test.id, gitCommit: test.gitCommit };
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export function findArtifacts(
|
|
37
48
|
db: Database,
|
|
38
|
-
|
|
49
|
+
testId: string,
|
|
39
50
|
stepId?: string,
|
|
40
51
|
): Array<{ filename: string; stepId: string; stepIdx: number }> {
|
|
41
|
-
const conditions: string[] = ['a.
|
|
42
|
-
const params: (string | number | null)[] = [
|
|
52
|
+
const conditions: string[] = ['a.test = ?'];
|
|
53
|
+
const params: (string | number | null | Uint8Array)[] = [toIdBlob(testId)];
|
|
43
54
|
|
|
44
55
|
if (stepId !== undefined) {
|
|
45
|
-
conditions.push('
|
|
46
|
-
params.push(stepId);
|
|
56
|
+
conditions.push('ss.step = ?');
|
|
57
|
+
params.push(toIdBlob(stepId));
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
const sql = `
|
|
50
|
-
SELECT a.filename,
|
|
61
|
+
SELECT a.filename, ss.step, a.step_index
|
|
51
62
|
FROM artifacts a
|
|
52
|
-
JOIN
|
|
63
|
+
JOIN tests t ON a.test = t.id
|
|
64
|
+
JOIN scenario_steps ss ON ss.scenario = t.scenario AND ss."index" = a.step_index
|
|
53
65
|
WHERE ${conditions.join(' AND ')}
|
|
54
|
-
ORDER BY
|
|
66
|
+
ORDER BY a.step_index ASC
|
|
55
67
|
`;
|
|
56
68
|
|
|
57
|
-
const rows = db.all(sql, params) as Array<{ filename: string;
|
|
58
|
-
return rows.map((r) => ({ filename: r.filename, stepId: r.
|
|
69
|
+
const rows = db.all(sql, params) as Array<{ filename: string; step: Uint8Array; step_index: number }>;
|
|
70
|
+
return rows.map((r) => ({ filename: r.filename, stepId: fromIdBlob(r.step), stepIdx: r.step_index }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LastRunStep {
|
|
74
|
+
id: string;
|
|
75
|
+
index: number;
|
|
76
|
+
text: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface LastRunTest {
|
|
80
|
+
id: string;
|
|
81
|
+
scenarioId: string;
|
|
82
|
+
scenarioName: string;
|
|
83
|
+
featureId: string;
|
|
84
|
+
featurePath: string;
|
|
85
|
+
featureName: string;
|
|
86
|
+
status: string;
|
|
87
|
+
startedAt: number;
|
|
88
|
+
failedStepIndex: number | null;
|
|
89
|
+
error: string | null;
|
|
90
|
+
ruleId: string | null;
|
|
91
|
+
outlineId: string | null;
|
|
92
|
+
exampleRowId: string | null;
|
|
93
|
+
exampleIndex: number | null;
|
|
94
|
+
steps: LastRunStep[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface LastRun {
|
|
98
|
+
id: string;
|
|
99
|
+
startedAt: number;
|
|
100
|
+
gitCommit: string | null;
|
|
101
|
+
tests: LastRunTest[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function findLastRun(db: Database): LastRun | null {
|
|
105
|
+
const run = db.get(
|
|
106
|
+
`
|
|
107
|
+
SELECT id, started_at, git_commit
|
|
108
|
+
FROM runs
|
|
109
|
+
ORDER BY started_at DESC
|
|
110
|
+
LIMIT 1
|
|
111
|
+
`,
|
|
112
|
+
) as { id: Uint8Array; started_at: number; git_commit: string | null } | undefined;
|
|
113
|
+
|
|
114
|
+
if (!run) return null;
|
|
115
|
+
|
|
116
|
+
const tests = db.all(
|
|
117
|
+
`
|
|
118
|
+
SELECT
|
|
119
|
+
t.id,
|
|
120
|
+
t.scenario,
|
|
121
|
+
s.name AS scenario_name,
|
|
122
|
+
s.rule,
|
|
123
|
+
s.outline,
|
|
124
|
+
s.example_row,
|
|
125
|
+
s.example_index,
|
|
126
|
+
f.id AS feature_id,
|
|
127
|
+
f.path AS feature_path,
|
|
128
|
+
f.name AS feature_name,
|
|
129
|
+
t.status,
|
|
130
|
+
t.started_at,
|
|
131
|
+
t.failed_step_index,
|
|
132
|
+
t.error
|
|
133
|
+
FROM tests t
|
|
134
|
+
JOIN scenarios s ON s.id = t.scenario
|
|
135
|
+
JOIN features f ON f.id = s.feature
|
|
136
|
+
WHERE t.run = ?
|
|
137
|
+
ORDER BY t.started_at ASC
|
|
138
|
+
`,
|
|
139
|
+
[run.id],
|
|
140
|
+
) as Array<{
|
|
141
|
+
id: Uint8Array;
|
|
142
|
+
scenario: Uint8Array;
|
|
143
|
+
scenario_name: string;
|
|
144
|
+
rule: Uint8Array | null;
|
|
145
|
+
outline: Uint8Array | null;
|
|
146
|
+
example_row: Uint8Array | null;
|
|
147
|
+
example_index: number | null;
|
|
148
|
+
feature_id: Uint8Array;
|
|
149
|
+
feature_path: string;
|
|
150
|
+
feature_name: string;
|
|
151
|
+
status: string;
|
|
152
|
+
started_at: number;
|
|
153
|
+
failed_step_index: number | null;
|
|
154
|
+
error: string | null;
|
|
155
|
+
}>;
|
|
156
|
+
|
|
157
|
+
const withSteps = tests.map((test) => {
|
|
158
|
+
const steps = db.all(
|
|
159
|
+
`
|
|
160
|
+
SELECT ss.step, ss."index", st.text
|
|
161
|
+
FROM scenario_steps ss
|
|
162
|
+
JOIN steps st ON st.id = ss.step
|
|
163
|
+
WHERE ss.scenario = ?
|
|
164
|
+
ORDER BY ss."index" ASC
|
|
165
|
+
`,
|
|
166
|
+
[test.scenario],
|
|
167
|
+
) as Array<{ step: Uint8Array; index: number; text: string }>;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
id: fromIdBlob(test.id),
|
|
171
|
+
scenarioId: fromIdBlob(test.scenario),
|
|
172
|
+
scenarioName: test.scenario_name,
|
|
173
|
+
featureId: fromIdBlob(test.feature_id),
|
|
174
|
+
featurePath: test.feature_path,
|
|
175
|
+
featureName: test.feature_name,
|
|
176
|
+
status: test.status,
|
|
177
|
+
startedAt: test.started_at,
|
|
178
|
+
failedStepIndex: test.failed_step_index,
|
|
179
|
+
error: test.error,
|
|
180
|
+
ruleId: test.rule ? fromIdBlob(test.rule) : null,
|
|
181
|
+
outlineId: test.outline ? fromIdBlob(test.outline) : null,
|
|
182
|
+
exampleRowId: test.example_row ? fromIdBlob(test.example_row) : null,
|
|
183
|
+
exampleIndex: test.example_index,
|
|
184
|
+
steps: steps.map((step) => ({
|
|
185
|
+
id: fromIdBlob(step.step),
|
|
186
|
+
index: step.index,
|
|
187
|
+
text: step.text,
|
|
188
|
+
})),
|
|
189
|
+
} satisfies LastRunTest;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
id: fromIdBlob(run.id),
|
|
194
|
+
startedAt: run.started_at,
|
|
195
|
+
gitCommit: run.git_commit,
|
|
196
|
+
tests: withSteps,
|
|
197
|
+
};
|
|
59
198
|
}
|
package/src/write.ts
CHANGED
|
@@ -1,29 +1,86 @@
|
|
|
1
1
|
import type { Database } from './db';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { toIdBlob } from './ids';
|
|
4
|
+
|
|
5
|
+
export function insertRun(db: Database, id: string, gitCommit: string | null, startedAt: number): void {
|
|
6
|
+
db.run('INSERT OR IGNORE INTO runs (id, started_at, git_commit) VALUES (?, ?, ?)', [
|
|
7
|
+
toIdBlob(id),
|
|
8
|
+
startedAt,
|
|
9
|
+
gitCommit,
|
|
10
|
+
]);
|
|
5
11
|
}
|
|
6
12
|
|
|
7
13
|
export function upsertFeature(db: Database, id: string, path: string, name: string): void {
|
|
8
|
-
db.run('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)', [id, path, name]);
|
|
14
|
+
db.run('INSERT OR REPLACE INTO features (id, path, name) VALUES (?, ?, ?)', [toIdBlob(id), path, name]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ScenarioRefs {
|
|
18
|
+
rule?: string;
|
|
19
|
+
outline?: string;
|
|
20
|
+
exampleRow?: string;
|
|
21
|
+
exampleIndex?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function upsertScenario(
|
|
25
|
+
db: Database,
|
|
26
|
+
id: string,
|
|
27
|
+
featureId: string,
|
|
28
|
+
index: number,
|
|
29
|
+
name: string,
|
|
30
|
+
refs: ScenarioRefs = {},
|
|
31
|
+
): void {
|
|
32
|
+
db.run(
|
|
33
|
+
`INSERT OR REPLACE INTO scenarios (
|
|
34
|
+
id, feature, "index", name, rule, outline, example_row, example_index
|
|
35
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36
|
+
[
|
|
37
|
+
toIdBlob(id),
|
|
38
|
+
toIdBlob(featureId),
|
|
39
|
+
index,
|
|
40
|
+
name,
|
|
41
|
+
refs.rule ? toIdBlob(refs.rule) : null,
|
|
42
|
+
refs.outline ? toIdBlob(refs.outline) : null,
|
|
43
|
+
refs.exampleRow ? toIdBlob(refs.exampleRow) : null,
|
|
44
|
+
refs.exampleIndex ?? null,
|
|
45
|
+
],
|
|
46
|
+
);
|
|
9
47
|
}
|
|
10
48
|
|
|
11
|
-
export function
|
|
12
|
-
db.run('INSERT OR REPLACE INTO
|
|
49
|
+
export function upsertStep(db: Database, id: string, text: string): void {
|
|
50
|
+
db.run('INSERT OR REPLACE INTO steps (id, text) VALUES (?, ?)', [toIdBlob(id), text]);
|
|
13
51
|
}
|
|
14
52
|
|
|
15
|
-
export function
|
|
16
|
-
db.run('INSERT OR REPLACE INTO
|
|
53
|
+
export function upsertScenarioStep(db: Database, scenarioId: string, index: number, stepId: string): void {
|
|
54
|
+
db.run('INSERT OR REPLACE INTO scenario_steps (scenario, "index", step) VALUES (?, ?, ?)', [
|
|
55
|
+
toIdBlob(scenarioId),
|
|
56
|
+
index,
|
|
57
|
+
toIdBlob(stepId),
|
|
58
|
+
]);
|
|
17
59
|
}
|
|
18
60
|
|
|
19
|
-
export function
|
|
20
|
-
db.run('INSERT INTO
|
|
61
|
+
export function insertTest(db: Database, id: string, runId: string, scenarioId: string, startedAt: number): void {
|
|
62
|
+
db.run('INSERT INTO tests (id, run, scenario, started_at) VALUES (?, ?, ?, ?)', [
|
|
63
|
+
toIdBlob(id),
|
|
64
|
+
toIdBlob(runId),
|
|
65
|
+
toIdBlob(scenarioId),
|
|
66
|
+
startedAt,
|
|
67
|
+
]);
|
|
21
68
|
}
|
|
22
69
|
|
|
23
|
-
export function
|
|
24
|
-
db.run('UPDATE
|
|
70
|
+
export function finaliseTest(db: Database, id: string, status: string, failedStepIndex?: number, error?: string): void {
|
|
71
|
+
db.run('UPDATE tests SET status = ?, failed_step_index = ?, error = ? WHERE id = ?', [
|
|
72
|
+
status,
|
|
73
|
+
failedStepIndex ?? null,
|
|
74
|
+
error ?? null,
|
|
75
|
+
toIdBlob(id),
|
|
76
|
+
]);
|
|
25
77
|
}
|
|
26
78
|
|
|
27
|
-
export function insertArtifact(db: Database, id: string,
|
|
28
|
-
db.run('INSERT INTO artifacts (id,
|
|
79
|
+
export function insertArtifact(db: Database, id: string, testId: string, stepIndex: number, filename: string): void {
|
|
80
|
+
db.run('INSERT INTO artifacts (id, test, step_index, filename) VALUES (?, ?, ?, ?)', [
|
|
81
|
+
toIdBlob(id),
|
|
82
|
+
toIdBlob(testId),
|
|
83
|
+
stepIndex,
|
|
84
|
+
filename,
|
|
85
|
+
]);
|
|
29
86
|
}
|