@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 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 test sessions, features, scenarios, steps, runs, and file artifacts.
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 six tables:
17
+ The store maintains seven tables:
18
18
 
19
19
  | Table | Purpose |
20
20
  |---|---|
21
- | `sessions` | One row per test run invocation, keyed by UUID with optional git commit |
22
- | `features` | Gherkin feature files, keyed by a content hash of the file |
23
- | `scenarios` | Gherkin scenarios, linked to their feature |
24
- | `steps` | Individual steps within a scenario, ordered by index |
25
- | `runs` | One row per scenario execution within a session; status is `running`, `passed`, or `failed` |
26
- | `artifacts` | Files (screenshots, HTML snapshots) produced during a step |
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
- | `insertSession(db, id, gitCommit, startedAt)` | Records a new test session |
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, scenarioId, idx, text)` | Inserts or replaces a step record |
50
- | `insertRun(db, id, sessionId, scenarioId, startedAt)` | Starts a new run with status `running` |
51
- | `finaliseRun(db, id, status, failedStepId?, error?)` | Updates a run's final status and optional failure details |
52
- | `insertArtifact(db, id, runId, stepId, filename)` | Links a saved file to a specific step in a run |
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 insertSession(db: Database, id: string, gitCommit: string | null, startedAt: number): void;
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
- declare function upsertScenario(db: Database, id: string, featureId: string, name: string): void;
9
- declare function upsertStep(db: Database, id: string, scenarioId: string, idx: number, text: string): void;
10
- declare function insertRun(db: Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void;
11
- declare function finaliseRun(db: Database, id: string, status: string, failedStepId?: string, error?: string): void;
12
- declare function insertArtifact(db: Database, id: string, runId: string, stepId: string, filename: string): void;
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(uri: string): string;
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 findLastRun(db: Database, scenarioId: string, status?: string, allowedCommits?: string[]): {
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 findArtifacts(db: Database, runId: string, stepId?: string): Array<{
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, finaliseRun, findArtifacts, findLastRun, insertArtifact, insertRun, insertSession, openStore, upsertFeature, upsertScenario, upsertStep };
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 { v5 } from 'uuid';
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 sessions (
8
- id TEXT PRIMARY KEY,
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 TEXT PRIMARY KEY,
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 TEXT PRIMARY KEY,
19
- feature_id TEXT NOT NULL REFERENCES features(id),
20
- name TEXT NOT NULL
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 TEXT PRIMARY KEY,
24
- scenario_id TEXT NOT NULL REFERENCES scenarios(id),
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 runs (
29
- id TEXT PRIMARY KEY,
30
- session_id TEXT NOT NULL REFERENCES sessions(id),
31
- scenario_id TEXT NOT NULL REFERENCES scenarios(id),
32
- status TEXT NOT NULL DEFAULT 'running',
33
- failed_step_id TEXT REFERENCES steps(id),
34
- error TEXT,
35
- started_at INTEGER NOT NULL
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 TEXT PRIMARY KEY,
39
- run_id TEXT NOT NULL REFERENCES runs(id),
40
- step_id TEXT NOT NULL REFERENCES steps(id),
41
- filename TEXT NOT NULL
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 insertSession(db, id, gitCommit, startedAt) {
54
- db.run("INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)", [id, startedAt, gitCommit]);
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("INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)", [id, featureId, name]);
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, scenarioId, idx, text) {
63
- db.run("INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)", [id, scenarioId, idx, text]);
153
+ function upsertStep(db, id, text) {
154
+ db.run("INSERT OR REPLACE INTO steps (id, text) VALUES (?, ?)", [toIdBlob(id), text]);
64
155
  }
65
- function insertRun(db, id, sessionId, scenarioId, startedAt) {
66
- db.run("INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)", [id, sessionId, scenarioId, startedAt]);
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 finaliseRun(db, id, status, failedStepId, error) {
69
- db.run("UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?", [status, failedStepId ?? null, error ?? null, id]);
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 insertArtifact(db, id, runId, stepId, filename) {
72
- db.run("INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)", [id, runId, stepId, filename]);
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
- var UUID_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
75
- function computeStepId(normalizedText) {
76
- return v5(normalizedText, UUID_NAMESPACE);
77
- }
78
- function computeScenarioId(stepIds) {
79
- return v5(stepIds.join(":"), UUID_NAMESPACE);
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 findLastRun(db, scenarioId, status, allowedCommits) {
87
- const conditions = ["r.scenario_id = ?"];
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("r.status = ?");
193
+ conditions.push("t.status = ?");
91
194
  params.push(status);
92
195
  }
93
196
  if (allowedCommits !== void 0) {
94
- conditions.push("s.git_commit IN (SELECT value FROM json_each(?))");
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 r.id, s.git_commit
99
- FROM runs r
100
- JOIN sessions s ON r.session_id = s.id
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 r.started_at DESC
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, runId, stepId) {
110
- const conditions = ["a.run_id = ?"];
111
- const params = [runId];
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("a.step_id = ?");
114
- params.push(stepId);
221
+ conditions.push("ss.step = ?");
222
+ params.push(toIdBlob(stepId));
115
223
  }
116
224
  const sql = `
117
- SELECT a.filename, a.step_id, st.idx
225
+ SELECT a.filename, ss.step, a.step_index
118
226
  FROM artifacts a
119
- JOIN steps st ON a.step_id = st.id
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 st.idx ASC
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.step_id, stepIdx: r.idx }));
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, finaliseRun, findArtifacts, findLastRun, insertArtifact, insertRun, insertSession, openStore, upsertFeature, upsertScenario, upsertStep };
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":";;;;AACA,IAAM,EAAE,UAAS,GAAI,QAAA;AAGrB,IAAM,MAAA,GAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAmDR,SAAS,SAAA,CAAU,OAAO,yBAAA,EAAqC;AACpE,EAAA,MAAM,EAAA,GAAK,IAAI,QAAA,CAAS,IAAI,CAAA;AAC5B,EAAA,EAAA,CAAG,IAAI,2BAA2B,CAAA;AAClC,EAAA,EAAA,CAAG,IAAI,0BAA0B,CAAA;AACjC,EAAA,EAAA,CAAG,KAAK,MAAM,CAAA;AACd,EAAA,OAAO,EAAA;AACT;AC3DA,IAAM,IAAA,GAAO;AAAA,EACX,IAAA,EAAM,CAAA;AAAA,EACN,QAAA,EAAU,CAAA;AAAA,EACV,OAAA,EAAS,CAAA;AAAA,EACT,IAAA,EAAM,CAAA;AAAA,EACN,OAAA,EAAS,CAAA;AAAA,EACT,UAAA,EAAY;AACd,CAAA;AAEA,SAAS,UAAU,GAAA,EAAqB;AACtC,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,WAAA,CAAY,CAAC,CAAA;AAChC,EAAA,GAAA,CAAI,aAAA,CAAc,KAAK,CAAC,CAAA;AACxB,EAAA,OAAO,GAAA;AACT;AAEA,SAAS,SAAA,CAAU,KAAa,OAAA,EAAyB;AACvD,EAAA,OAAO,UAAA,CAAW,QAAQ,CAAA,CAAE,MAAA,CAAO,SAAA,CAAU,GAAG,CAAC,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AACjF;AAEA,SAAS,aAAa,GAAA,EAAuB;AAC3C,EAAA,IAAI,IAAI,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC3C,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,GAAA,CAAI,CAAC,EAAA,KAAO,MAAA,CAAO,IAAA,CAAK,EAAA,EAAI,KAAK,CAAC,CAAC,CAAA;AAC9D;AAEA,SAAS,cAAc,KAAA,EAAyB;AAC9C,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,IAAA,EAAM,MAAM,CAAA;AACtC,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,WAAA,CAAY,CAAC,CAAA;AAChC,IAAA,GAAA,CAAI,aAAA,CAAc,KAAA,CAAM,MAAA,EAAQ,CAAC,CAAA;AACjC,IAAA,OAAA,CAAQ,IAAA,CAAK,KAAK,KAAK,CAAA;AAAA,EACzB;AAEA,EAAA,OAAO,MAAA,CAAO,OAAO,OAAO,CAAA;AAC9B;AAEO,SAAS,cAAc,cAAA,EAAgC;AAC5D,EAAA,OAAO,UAAU,IAAA,CAAK,IAAA,EAAM,OAAO,IAAA,CAAK,cAAA,EAAgB,MAAM,CAAC,CAAA;AACjE;AAEO,SAAS,kBAAkB,OAAA,EAA2B;AAC3D,EAAA,OAAO,SAAA,CAAU,IAAA,CAAK,QAAA,EAAU,YAAA,CAAa,OAAO,CAAC,CAAA;AACvD;AAEO,SAAS,iBAAiB,WAAA,EAA+B;AAC9D,EAAA,OAAO,SAAA,CAAU,IAAA,CAAK,OAAA,EAAS,YAAA,CAAa,WAAW,CAAC,CAAA;AAC1D;AAEO,SAAS,cAAc,WAAA,EAA+B;AAC3D,EAAA,OAAO,SAAA,CAAU,IAAA,CAAK,IAAA,EAAM,YAAA,CAAa,WAAW,CAAC,CAAA;AACvD;AAEO,SAAS,iBAAiB,OAAA,EAA2B;AAC1D,EAAA,OAAO,SAAA,CAAU,IAAA,CAAK,OAAA,EAAS,YAAA,CAAa,OAAO,CAAC,CAAA;AACtD;AAEO,SAAS,oBAAoB,MAAA,EAA0B;AAC5D,EAAA,OAAO,SAAA,CAAU,IAAA,CAAK,UAAA,EAAY,aAAA,CAAc,MAAM,CAAC,CAAA;AACzD;AAEA,SAAS,OAAO,KAAA,EAAuB;AACrC,EAAA,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,KAAA,EAAO,MAAM,EAAE,MAAA,EAAO;AAC3D;AAEO,SAAS,SAAS,EAAA,EAAoB;AAC3C,EAAA,MAAM,UAAA,GAAa,GAAG,WAAA,EAAY;AAClC,EAAA,IAAI,gBAAA,CAAiB,IAAA,CAAK,UAAU,CAAA,EAAG;AACrC,IAAA,OAAO,MAAA,CAAO,IAAA,CAAK,UAAA,EAAY,KAAK,CAAA;AAAA,EACtC;AAEA,EAAA,OAAO,OAAO,EAAE,CAAA;AAClB;AAEO,SAAS,WAAW,EAAA,EAAwB;AACjD,EAAA,OAAO,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,CAAE,SAAS,KAAK,CAAA;AACvC;;;ACzEO,SAAS,SAAA,CAAU,EAAA,EAAc,EAAA,EAAY,SAAA,EAA0B,SAAA,EAAyB;AACrG,EAAA,EAAA,CAAG,IAAI,0EAAA,EAA4E;AAAA,IACjF,SAAS,EAAE,CAAA;AAAA,IACX,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;AAEO,SAAS,aAAA,CAAc,EAAA,EAAc,EAAA,EAAY,IAAA,EAAc,IAAA,EAAoB;AACxF,EAAA,EAAA,CAAG,GAAA,CAAI,qEAAqE,CAAC,QAAA,CAAS,EAAE,CAAA,EAAG,IAAA,EAAM,IAAI,CAAC,CAAA;AACxG;AASO,SAAS,cAAA,CACd,IACA,EAAA,EACA,SAAA,EACA,OACA,IAAA,EACA,IAAA,GAAqB,EAAC,EAChB;AACN,EAAA,EAAA,CAAG,GAAA;AAAA,IACD,CAAA;AAAA;AAAA,qCAAA,CAAA;AAAA,IAGA;AAAA,MACE,SAAS,EAAE,CAAA;AAAA,MACX,SAAS,SAAS,CAAA;AAAA,MAClB,KAAA;AAAA,MACA,IAAA;AAAA,MACA,IAAA,CAAK,IAAA,GAAO,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAAA,MAClC,IAAA,CAAK,OAAA,GAAU,QAAA,CAAS,IAAA,CAAK,OAAO,CAAA,GAAI,IAAA;AAAA,MACxC,IAAA,CAAK,UAAA,GAAa,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA,GAAI,IAAA;AAAA,MAC9C,KAAK,YAAA,IAAgB;AAAA;AACvB,GACF;AACF;AAEO,SAAS,UAAA,CAAW,EAAA,EAAc,EAAA,EAAY,IAAA,EAAoB;AACvE,EAAA,EAAA,CAAG,IAAI,uDAAA,EAAyD,CAAC,SAAS,EAAE,CAAA,EAAG,IAAI,CAAC,CAAA;AACtF;AAEO,SAAS,kBAAA,CAAmB,EAAA,EAAc,UAAA,EAAoB,KAAA,EAAe,MAAA,EAAsB;AACxG,EAAA,EAAA,CAAG,IAAI,kFAAA,EAAoF;AAAA,IACzF,SAAS,UAAU,CAAA;AAAA,IACnB,KAAA;AAAA,IACA,SAAS,MAAM;AAAA,GAChB,CAAA;AACH;AAEO,SAAS,UAAA,CAAW,EAAA,EAAc,EAAA,EAAY,KAAA,EAAe,YAAoB,SAAA,EAAyB;AAC/G,EAAA,EAAA,CAAG,IAAI,uEAAA,EAAyE;AAAA,IAC9E,SAAS,EAAE,CAAA;AAAA,IACX,SAAS,KAAK,CAAA;AAAA,IACd,SAAS,UAAU,CAAA;AAAA,IACnB;AAAA,GACD,CAAA;AACH;AAEO,SAAS,YAAA,CAAa,EAAA,EAAc,EAAA,EAAY,MAAA,EAAgB,iBAA0B,KAAA,EAAsB;AACrH,EAAA,EAAA,CAAG,IAAI,4EAAA,EAA8E;AAAA,IACnF,MAAA;AAAA,IACA,eAAA,IAAmB,IAAA;AAAA,IACnB,KAAA,IAAS,IAAA;AAAA,IACT,SAAS,EAAE;AAAA,GACZ,CAAA;AACH;AAEO,SAAS,cAAA,CAAe,EAAA,EAAc,EAAA,EAAY,MAAA,EAAgB,WAAmB,QAAA,EAAwB;AAClH,EAAA,EAAA,CAAG,IAAI,4EAAA,EAA8E;AAAA,IACnF,SAAS,EAAE,CAAA;AAAA,IACX,SAAS,MAAM,CAAA;AAAA,IACf,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH;;;AClFO,SAAS,YAAA,CACd,EAAA,EACA,UAAA,EACA,MAAA,EACA,cAAA,EACiD;AACjD,EAAA,MAAM,UAAA,GAAuB,CAAC,gBAAgB,CAAA;AAC9C,EAAA,MAAM,MAAA,GAAkD,CAAC,QAAA,CAAS,UAAU,CAAC,CAAA;AAE7E,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,UAAA,CAAW,KAAK,cAAc,CAAA;AAC9B,IAAA,MAAA,CAAO,KAAK,MAAM,CAAA;AAAA,EACpB;AAEA,EAAA,IAAI,mBAAmB,MAAA,EAAW;AAChC,IAAA,UAAA,CAAW,KAAK,kDAAkD,CAAA;AAClE,IAAA,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,cAAc,CAAC,CAAA;AAAA,EAC5C;AAEA,EAAA,MAAM,GAAA,GAAM;AAAA;AAAA;AAAA;AAAA,UAAA,EAIF,UAAA,CAAW,IAAA,CAAK,OAAO,CAAC;AAAA;AAAA;AAAA,EAAA,CAAA;AAKlC,EAAA,MAAM,GAAA,GAAM,EAAA,CAAG,GAAA,CAAI,GAAA,EAAK,MAAM,CAAA;AAC9B,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,OAAO,EAAE,IAAI,UAAA,CAAW,GAAA,CAAI,EAAE,CAAA,EAAG,SAAA,EAAW,IAAI,UAAA,EAAW;AAC7D;AAEO,SAAS,uBAAA,CACd,EAAA,EACA,UAAA,EACA,cAAA,EACqD;AACrD,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,EAAA,EAAI,UAAA,EAAY,UAAU,cAAc,CAAA;AAClE,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,CAAK,EAAA,EAAI,SAAA,EAAW,KAAK,SAAA,EAAU;AACtD;AAEO,SAAS,aAAA,CACd,EAAA,EACA,MAAA,EACA,MAAA,EAC8D;AAC9D,EAAA,MAAM,UAAA,GAAuB,CAAC,YAAY,CAAA;AAC1C,EAAA,MAAM,MAAA,GAAkD,CAAC,QAAA,CAAS,MAAM,CAAC,CAAA;AAEzE,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,UAAA,CAAW,KAAK,aAAa,CAAA;AAC7B,IAAA,MAAA,CAAO,IAAA,CAAK,QAAA,CAAS,MAAM,CAAC,CAAA;AAAA,EAC9B;AAEA,EAAA,MAAM,GAAA,GAAM;AAAA;AAAA;AAAA;AAAA;AAAA,UAAA,EAKF,UAAA,CAAW,IAAA,CAAK,OAAO,CAAC;AAAA;AAAA,EAAA,CAAA;AAIlC,EAAA,MAAM,IAAA,GAAO,EAAA,CAAG,GAAA,CAAI,GAAA,EAAK,MAAM,CAAA;AAC/B,EAAA,OAAO,KAAK,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,UAAU,CAAA,CAAE,QAAA,EAAU,MAAA,EAAQ,UAAA,CAAW,EAAE,IAAI,CAAA,EAAG,OAAA,EAAS,CAAA,CAAE,YAAW,CAAE,CAAA;AACtG;AAiCO,SAAS,YAAY,EAAA,EAA8B;AACxD,EAAA,MAAM,MAAM,EAAA,CAAG,GAAA;AAAA,IACb;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA;AAAA,GAMF;AAEA,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,EAAA,MAAM,QAAQ,EAAA,CAAG,GAAA;AAAA,IACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,CAAA;AAAA,IAsBA,CAAC,IAAI,EAAE;AAAA,GACT;AAiBA,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,KAAS;AACpC,IAAA,MAAM,QAAQ,EAAA,CAAG,GAAA;AAAA,MACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,CAAA;AAAA,MAOA,CAAC,KAAK,QAAQ;AAAA,KAChB;AAEA,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,UAAA,CAAW,IAAA,CAAK,EAAE,CAAA;AAAA,MACtB,UAAA,EAAY,UAAA,CAAW,IAAA,CAAK,QAAQ,CAAA;AAAA,MACpC,cAAc,IAAA,CAAK,aAAA;AAAA,MACnB,SAAA,EAAW,UAAA,CAAW,IAAA,CAAK,UAAU,CAAA;AAAA,MACrC,aAAa,IAAA,CAAK,YAAA;AAAA,MAClB,aAAa,IAAA,CAAK,YAAA;AAAA,MAClB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,WAAW,IAAA,CAAK,UAAA;AAAA,MAChB,iBAAiB,IAAA,CAAK,iBAAA;AAAA,MACtB,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,QAAQ,IAAA,CAAK,IAAA,GAAO,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA;AAAA,MAC5C,WAAW,IAAA,CAAK,OAAA,GAAU,UAAA,CAAW,IAAA,CAAK,OAAO,CAAA,GAAI,IAAA;AAAA,MACrD,cAAc,IAAA,CAAK,WAAA,GAAc,UAAA,CAAW,IAAA,CAAK,WAAW,CAAA,GAAI,IAAA;AAAA,MAChE,cAAc,IAAA,CAAK,aAAA;AAAA,MACnB,KAAA,EAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,MAAU;AAAA,QAC1B,EAAA,EAAI,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA;AAAA,QACxB,OAAO,IAAA,CAAK,KAAA;AAAA,QACZ,MAAM,IAAA,CAAK;AAAA,OACb,CAAE;AAAA,KACJ;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,UAAA,CAAW,GAAA,CAAI,EAAE,CAAA;AAAA,IACrB,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,KAAA,EAAO;AAAA,GACT;AACF","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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letsrunit/store",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "SQLite-backed artifact store for letsrunit",
5
5
  "keywords": [
6
6
  "testing",
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 sessions (
7
- id TEXT PRIMARY KEY,
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 TEXT PRIMARY KEY,
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 TEXT PRIMARY KEY,
18
- feature_id TEXT NOT NULL REFERENCES features(id),
19
- name TEXT NOT NULL
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 TEXT PRIMARY KEY,
23
- scenario_id TEXT NOT NULL REFERENCES scenarios(id),
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 runs (
28
- id TEXT PRIMARY KEY,
29
- session_id TEXT NOT NULL REFERENCES sessions(id),
30
- scenario_id TEXT NOT NULL REFERENCES scenarios(id),
31
- status TEXT NOT NULL DEFAULT 'running',
32
- failed_step_id TEXT REFERENCES steps(id),
33
- error TEXT,
34
- started_at INTEGER NOT NULL
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 TEXT PRIMARY KEY,
38
- run_id TEXT NOT NULL REFERENCES runs(id),
39
- step_id TEXT NOT NULL REFERENCES steps(id),
40
- filename TEXT NOT NULL
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 { v5 as uuidv5 } from 'uuid';
1
+ import { createHash } from 'node:crypto';
2
2
 
3
- const UUID_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
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 uuidv5(normalizedText, UUID_NAMESPACE);
40
+ return hashBytes(TAGS.step, Buffer.from(normalizedText, 'utf8'));
7
41
  }
8
42
 
9
43
  export function computeScenarioId(stepIds: string[]): string {
10
- return uuidv5(stepIds.join(':'), UUID_NAMESPACE);
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 computeFeatureId(uri: string): string {
14
- return uuidv5(uri, UUID_NAMESPACE);
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 { openStore } from './db.js';
2
- export { insertSession, upsertFeature, upsertScenario, upsertStep, insertRun, finaliseRun, insertArtifact } from './write.js';
3
- export { computeStepId, computeScenarioId, computeFeatureId } from './ids.js';
4
- export { findLastRun, findArtifacts } from './read.js';
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 findLastRun(
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[] = ['r.scenario_id = ?'];
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('r.status = ?');
14
+ conditions.push('t.status = ?');
14
15
  params.push(status);
15
16
  }
16
17
 
17
18
  if (allowedCommits !== undefined) {
18
- conditions.push('s.git_commit IN (SELECT value FROM json_each(?))');
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 r.id, s.git_commit
24
- FROM runs r
25
- JOIN sessions s ON r.session_id = s.id
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 r.started_at DESC
28
+ ORDER BY t.started_at DESC
28
29
  LIMIT 1
29
30
  `;
30
31
 
31
- const row = db.get(sql, params) as { id: string; git_commit: string | null } | undefined;
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
- runId: string,
49
+ testId: string,
39
50
  stepId?: string,
40
51
  ): Array<{ filename: string; stepId: string; stepIdx: number }> {
41
- const conditions: string[] = ['a.run_id = ?'];
42
- const params: (string | number | null)[] = [runId];
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('a.step_id = ?');
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, a.step_id, st.idx
61
+ SELECT a.filename, ss.step, a.step_index
51
62
  FROM artifacts a
52
- JOIN steps st ON a.step_id = st.id
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 st.idx ASC
66
+ ORDER BY a.step_index ASC
55
67
  `;
56
68
 
57
- const rows = db.all(sql, params) as Array<{ filename: string; step_id: string; idx: number }>;
58
- return rows.map((r) => ({ filename: r.filename, stepId: r.step_id, stepIdx: r.idx }));
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
- export function insertSession(db: Database, id: string, gitCommit: string | null, startedAt: number): void {
4
- db.run('INSERT OR IGNORE INTO sessions (id, started_at, git_commit) VALUES (?, ?, ?)', [id, startedAt, gitCommit]);
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 upsertScenario(db: Database, id: string, featureId: string, name: string): void {
12
- db.run('INSERT OR REPLACE INTO scenarios (id, feature_id, name) VALUES (?, ?, ?)', [id, featureId, name]);
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 upsertStep(db: Database, id: string, scenarioId: string, idx: number, text: string): void {
16
- db.run('INSERT OR REPLACE INTO steps (id, scenario_id, idx, text) VALUES (?, ?, ?, ?)', [id, scenarioId, idx, text]);
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 insertRun(db: Database, id: string, sessionId: string, scenarioId: string, startedAt: number): void {
20
- db.run('INSERT INTO runs (id, session_id, scenario_id, started_at) VALUES (?, ?, ?, ?)', [id, sessionId, scenarioId, startedAt]);
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 finaliseRun(db: Database, id: string, status: string, failedStepId?: string, error?: string): void {
24
- db.run('UPDATE runs SET status = ?, failed_step_id = ?, error = ? WHERE id = ?', [status, failedStepId ?? null, error ?? null, id]);
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, runId: string, stepId: string, filename: string): void {
28
- db.run('INSERT INTO artifacts (id, run_id, step_id, filename) VALUES (?, ?, ?, ?)', [id, runId, stepId, filename]);
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
  }