@databricks-solutions/lakebase-scm-utils 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/scripts/index.cjs +2 -2
  2. package/dist/scripts/index.js +2 -2
  3. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2 -2
  4. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +2 -2
  5. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2 -2
  6. package/dist/scripts/lakebase/collapse-heads.cli.js +2 -2
  7. package/dist/scripts/lakebase/detect-language.cli.cjs +2 -2
  8. package/dist/scripts/lakebase/detect-language.cli.js +2 -2
  9. package/dist/scripts/lakebase/doctor.cli.cjs +2 -2
  10. package/dist/scripts/lakebase/doctor.cli.js +2 -2
  11. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  12. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  13. package/dist/scripts/lakebase/index.cjs +2 -2
  14. package/dist/scripts/lakebase/index.js +2 -2
  15. package/dist/scripts/lakebase/infra-runner.cli.cjs +2 -2
  16. package/dist/scripts/lakebase/infra-runner.cli.js +2 -2
  17. package/dist/scripts/lakebase/new-migration.cli.cjs +2 -2
  18. package/dist/scripts/lakebase/new-migration.cli.js +2 -2
  19. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  20. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  21. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2 -2
  22. package/dist/scripts/lakebase/schema-migrate.cli.js +2 -2
  23. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2 -2
  24. package/dist/scripts/lakebase/scm-claim-feature.cli.js +2 -2
  25. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2 -2
  26. package/dist/scripts/lakebase/scm-doctor.cli.js +2 -2
  27. package/dist/scripts/lakebase/scm-merge.cli.cjs +2 -2
  28. package/dist/scripts/lakebase/scm-merge.cli.js +2 -2
  29. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2 -2
  30. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +2 -2
  31. package/dist/scripts/util/index.cjs +2 -2
  32. package/dist/scripts/util/index.js +2 -2
  33. package/package.json +1 -1
  34. package/templates/project/common/consort-capture.ts +121 -3
  35. package/templates/project/python/tests/__pycache__/conftest.cpython-314.pyc +0 -0
  36. package/templates/project/python/tests/conftest.py +90 -1
@@ -88959,8 +88959,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88959
88959
  var cached;
88960
88960
  function substrateSelfVersion() {
88961
88961
  if (cached !== void 0) return cached;
88962
- if ("0.3.7".length > 0) {
88963
- cached = "0.3.7";
88962
+ if ("0.3.8".length > 0) {
88963
+ cached = "0.3.8";
88964
88964
  return cached;
88965
88965
  }
88966
88966
  cached = "unknown";
@@ -9737,8 +9737,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9737
9737
  var cached;
9738
9738
  function substrateSelfVersion() {
9739
9739
  if (cached !== void 0) return cached;
9740
- if ("0.3.7".length > 0) {
9741
- cached = "0.3.7";
9740
+ if ("0.3.8".length > 0) {
9741
+ cached = "0.3.8";
9742
9742
  return cached;
9743
9743
  }
9744
9744
  cached = "unknown";
@@ -79629,8 +79629,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79629
79629
  var cached;
79630
79630
  function substrateSelfVersion() {
79631
79631
  if (cached !== void 0) return cached;
79632
- if ("0.3.7".length > 0) {
79633
- cached = "0.3.7";
79632
+ if ("0.3.8".length > 0) {
79633
+ cached = "0.3.8";
79634
79634
  return cached;
79635
79635
  }
79636
79636
  cached = "unknown";
@@ -721,8 +721,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
721
721
  var cached;
722
722
  function substrateSelfVersion() {
723
723
  if (cached !== void 0) return cached;
724
- if ("0.3.7".length > 0) {
725
- cached = "0.3.7";
724
+ if ("0.3.8".length > 0) {
725
+ cached = "0.3.8";
726
726
  return cached;
727
727
  }
728
728
  cached = "unknown";
@@ -79290,8 +79290,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79290
79290
  var cached;
79291
79291
  function substrateSelfVersion() {
79292
79292
  if (cached !== void 0) return cached;
79293
- if ("0.3.7".length > 0) {
79294
- cached = "0.3.7";
79293
+ if ("0.3.8".length > 0) {
79294
+ cached = "0.3.8";
79295
79295
  return cached;
79296
79296
  }
79297
79297
  cached = "unknown";
@@ -393,8 +393,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
393
393
  var cached;
394
394
  function substrateSelfVersion() {
395
395
  if (cached !== void 0) return cached;
396
- if ("0.3.7".length > 0) {
397
- cached = "0.3.7";
396
+ if ("0.3.8".length > 0) {
397
+ cached = "0.3.8";
398
398
  return cached;
399
399
  }
400
400
  cached = "unknown";
@@ -79290,8 +79290,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79290
79290
  var cached;
79291
79291
  function substrateSelfVersion() {
79292
79292
  if (cached !== void 0) return cached;
79293
- if ("0.3.7".length > 0) {
79294
- cached = "0.3.7";
79293
+ if ("0.3.8".length > 0) {
79294
+ cached = "0.3.8";
79295
79295
  return cached;
79296
79296
  }
79297
79297
  cached = "unknown";
@@ -393,8 +393,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
393
393
  var cached;
394
394
  function substrateSelfVersion() {
395
395
  if (cached !== void 0) return cached;
396
- if ("0.3.7".length > 0) {
397
- cached = "0.3.7";
396
+ if ("0.3.8".length > 0) {
397
+ cached = "0.3.8";
398
398
  return cached;
399
399
  }
400
400
  cached = "unknown";
@@ -80800,8 +80800,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
80800
80800
  var cached;
80801
80801
  function substrateSelfVersion() {
80802
80802
  if (cached !== void 0) return cached;
80803
- if ("0.3.7".length > 0) {
80804
- cached = "0.3.7";
80803
+ if ("0.3.8".length > 0) {
80804
+ cached = "0.3.8";
80805
80805
  return cached;
80806
80806
  }
80807
80807
  cached = "unknown";
@@ -1959,8 +1959,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1959
1959
  var cached;
1960
1960
  function substrateSelfVersion() {
1961
1961
  if (cached !== void 0) return cached;
1962
- if ("0.3.7".length > 0) {
1963
- cached = "0.3.7";
1962
+ if ("0.3.8".length > 0) {
1963
+ cached = "0.3.8";
1964
1964
  return cached;
1965
1965
  }
1966
1966
  cached = "unknown";
@@ -79285,8 +79285,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79285
79285
  var cached;
79286
79286
  function substrateSelfVersion() {
79287
79287
  if (cached !== void 0) return cached;
79288
- if ("0.3.7".length > 0) {
79289
- cached = "0.3.7";
79288
+ if ("0.3.8".length > 0) {
79289
+ cached = "0.3.8";
79290
79290
  return cached;
79291
79291
  }
79292
79292
  cached = "unknown";
@@ -389,8 +389,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
389
389
  var cached;
390
390
  function substrateSelfVersion() {
391
391
  if (cached !== void 0) return cached;
392
- if ("0.3.7".length > 0) {
393
- cached = "0.3.7";
392
+ if ("0.3.8".length > 0) {
393
+ cached = "0.3.8";
394
394
  return cached;
395
395
  }
396
396
  cached = "unknown";
@@ -81607,8 +81607,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
81607
81607
  var cached;
81608
81608
  function substrateSelfVersion() {
81609
81609
  if (cached !== void 0) return cached;
81610
- if ("0.3.7".length > 0) {
81611
- cached = "0.3.7";
81610
+ if ("0.3.8".length > 0) {
81611
+ cached = "0.3.8";
81612
81612
  return cached;
81613
81613
  }
81614
81614
  cached = "unknown";
@@ -2521,8 +2521,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
2521
2521
  var cached;
2522
2522
  function substrateSelfVersion() {
2523
2523
  if (cached !== void 0) return cached;
2524
- if ("0.3.7".length > 0) {
2525
- cached = "0.3.7";
2524
+ if ("0.3.8".length > 0) {
2525
+ cached = "0.3.8";
2526
2526
  return cached;
2527
2527
  }
2528
2528
  cached = "unknown";
@@ -79290,8 +79290,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79290
79290
  var cached;
79291
79291
  function substrateSelfVersion() {
79292
79292
  if (cached !== void 0) return cached;
79293
- if ("0.3.7".length > 0) {
79294
- cached = "0.3.7";
79293
+ if ("0.3.8".length > 0) {
79294
+ cached = "0.3.8";
79295
79295
  return cached;
79296
79296
  }
79297
79297
  cached = "unknown";
@@ -393,8 +393,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
393
393
  var cached;
394
394
  function substrateSelfVersion() {
395
395
  if (cached !== void 0) return cached;
396
- if ("0.3.7".length > 0) {
397
- cached = "0.3.7";
396
+ if ("0.3.8".length > 0) {
397
+ cached = "0.3.8";
398
398
  return cached;
399
399
  }
400
400
  cached = "unknown";
@@ -79290,8 +79290,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79290
79290
  var cached;
79291
79291
  function substrateSelfVersion() {
79292
79292
  if (cached !== void 0) return cached;
79293
- if ("0.3.7".length > 0) {
79294
- cached = "0.3.7";
79293
+ if ("0.3.8".length > 0) {
79294
+ cached = "0.3.8";
79295
79295
  return cached;
79296
79296
  }
79297
79297
  cached = "unknown";
@@ -393,8 +393,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
393
393
  var cached;
394
394
  function substrateSelfVersion() {
395
395
  if (cached !== void 0) return cached;
396
- if ("0.3.7".length > 0) {
397
- cached = "0.3.7";
396
+ if ("0.3.8".length > 0) {
397
+ cached = "0.3.8";
398
398
  return cached;
399
399
  }
400
400
  cached = "unknown";
@@ -79288,8 +79288,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79288
79288
  var cached;
79289
79289
  function substrateSelfVersion() {
79290
79290
  if (cached !== void 0) return cached;
79291
- if ("0.3.7".length > 0) {
79292
- cached = "0.3.7";
79291
+ if ("0.3.8".length > 0) {
79292
+ cached = "0.3.8";
79293
79293
  return cached;
79294
79294
  }
79295
79295
  cached = "unknown";
@@ -389,8 +389,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
389
389
  var cached;
390
390
  function substrateSelfVersion() {
391
391
  if (cached !== void 0) return cached;
392
- if ("0.3.7".length > 0) {
393
- cached = "0.3.7";
392
+ if ("0.3.8".length > 0) {
393
+ cached = "0.3.8";
394
394
  return cached;
395
395
  }
396
396
  cached = "unknown";
@@ -79324,8 +79324,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79324
79324
  var cached;
79325
79325
  function substrateSelfVersion() {
79326
79326
  if (cached !== void 0) return cached;
79327
- if ("0.3.7".length > 0) {
79328
- cached = "0.3.7";
79327
+ if ("0.3.8".length > 0) {
79328
+ cached = "0.3.8";
79329
79329
  return cached;
79330
79330
  }
79331
79331
  cached = "unknown";
@@ -427,8 +427,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
427
427
  var cached;
428
428
  function substrateSelfVersion() {
429
429
  if (cached !== void 0) return cached;
430
- if ("0.3.7".length > 0) {
431
- cached = "0.3.7";
430
+ if ("0.3.8".length > 0) {
431
+ cached = "0.3.8";
432
432
  return cached;
433
433
  }
434
434
  cached = "unknown";
@@ -79783,8 +79783,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79783
79783
  var cached;
79784
79784
  function substrateSelfVersion() {
79785
79785
  if (cached !== void 0) return cached;
79786
- if ("0.3.7".length > 0) {
79787
- cached = "0.3.7";
79786
+ if ("0.3.8".length > 0) {
79787
+ cached = "0.3.8";
79788
79788
  return cached;
79789
79789
  }
79790
79790
  cached = "unknown";
@@ -864,8 +864,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
864
864
  var cached;
865
865
  function substrateSelfVersion() {
866
866
  if (cached !== void 0) return cached;
867
- if ("0.3.7".length > 0) {
868
- cached = "0.3.7";
867
+ if ("0.3.8".length > 0) {
868
+ cached = "0.3.8";
869
869
  return cached;
870
870
  }
871
871
  cached = "unknown";
@@ -88240,8 +88240,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88240
88240
  var cached;
88241
88241
  function substrateSelfVersion() {
88242
88242
  if (cached !== void 0) return cached;
88243
- if ("0.3.7".length > 0) {
88244
- cached = "0.3.7";
88243
+ if ("0.3.8".length > 0) {
88244
+ cached = "0.3.8";
88245
88245
  return cached;
88246
88246
  }
88247
88247
  cached = "unknown";
@@ -9393,8 +9393,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9393
9393
  var cached;
9394
9394
  function substrateSelfVersion() {
9395
9395
  if (cached !== void 0) return cached;
9396
- if ("0.3.7".length > 0) {
9397
- cached = "0.3.7";
9396
+ if ("0.3.8".length > 0) {
9397
+ cached = "0.3.8";
9398
9398
  return cached;
9399
9399
  }
9400
9400
  cached = "unknown";
@@ -88825,8 +88825,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88825
88825
  var cached;
88826
88826
  function substrateSelfVersion() {
88827
88827
  if (cached !== void 0) return cached;
88828
- if ("0.3.7".length > 0) {
88829
- cached = "0.3.7";
88828
+ if ("0.3.8".length > 0) {
88829
+ cached = "0.3.8";
88830
88830
  return cached;
88831
88831
  }
88832
88832
  cached = "unknown";
@@ -9978,8 +9978,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9978
9978
  var cached;
9979
9979
  function substrateSelfVersion() {
9980
9980
  if (cached !== void 0) return cached;
9981
- if ("0.3.7".length > 0) {
9982
- cached = "0.3.7";
9981
+ if ("0.3.8".length > 0) {
9982
+ cached = "0.3.8";
9983
9983
  return cached;
9984
9984
  }
9985
9985
  cached = "unknown";
@@ -79288,8 +79288,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
79288
79288
  var cached;
79289
79289
  function substrateSelfVersion() {
79290
79290
  if (cached !== void 0) return cached;
79291
- if ("0.3.7".length > 0) {
79292
- cached = "0.3.7";
79291
+ if ("0.3.8".length > 0) {
79292
+ cached = "0.3.8";
79293
79293
  return cached;
79294
79294
  }
79295
79295
  cached = "unknown";
@@ -389,8 +389,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
389
389
  var cached;
390
390
  function substrateSelfVersion() {
391
391
  if (cached !== void 0) return cached;
392
- if ("0.3.7".length > 0) {
393
- cached = "0.3.7";
392
+ if ("0.3.8".length > 0) {
393
+ cached = "0.3.8";
394
394
  return cached;
395
395
  }
396
396
  cached = "unknown";
@@ -1667,8 +1667,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1667
1667
  var cached;
1668
1668
  function substrateSelfVersion() {
1669
1669
  if (cached !== void 0) return cached;
1670
- if ("0.3.7".length > 0) {
1671
- cached = "0.3.7";
1670
+ if ("0.3.8".length > 0) {
1671
+ cached = "0.3.8";
1672
1672
  return cached;
1673
1673
  }
1674
1674
  cached = "unknown";
@@ -1630,8 +1630,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1630
1630
  var cached;
1631
1631
  function substrateSelfVersion() {
1632
1632
  if (cached !== void 0) return cached;
1633
- if ("0.3.7".length > 0) {
1634
- cached = "0.3.7";
1633
+ if ("0.3.8".length > 0) {
1634
+ cached = "0.3.8";
1635
1635
  return cached;
1636
1636
  }
1637
1637
  cached = "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks-solutions/lakebase-scm-utils",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Lakebase SCM + substrate engine: database branching, paired-branch SCM workflows, connection/credential minting, schema migration, project scaffold + deploy primitives, and the git/github utility layer. Extracted from consort for portable, standalone consumption by lakebase-scm-extension and the SFTDD orchestration.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -9,8 +9,8 @@
9
9
  // the client's bundler-resolution tsconfig. Importing these from here keeps the client Playwright-free.
10
10
  //
11
11
  // Pure observability: everything is wrapped so a write can NEVER fail a test/suite.
12
- import { appendFileSync, existsSync, mkdirSync } from "node:fs";
13
- import { dirname, join } from "node:path";
12
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
13
+ import { basename, dirname, join } from "node:path";
14
14
 
15
15
  /** Walk up from `start` to the project's `.consort` dir (tests run from root OR client/, so cwd
16
16
  * varies); fall back to `<cwd>/.consort` (created on write) if none is found. */
@@ -33,15 +33,133 @@ export function emitTestRun(id: string, status: TestRunStatus, absFile: string,
33
33
  mkdirSync(dir, { recursive: true });
34
34
  const root = dirname(dir);
35
35
  const file = (absFile || "").replace(`${root}/`, "");
36
+ const ts = new Date().toISOString();
36
37
  appendFileSync(
37
38
  join(dir, "test-run.jsonl"),
38
- JSON.stringify({ ts: new Date().toISOString(), test_id: id, status, file, kind }) + "\n",
39
+ JSON.stringify({ ts, test_id: id, status, file, kind }) + "\n",
39
40
  );
41
+ // RECORD a durable per-test `test.result` into the agent-log AT RUN TIME (not a cycle-end batch),
42
+ // so the dashboard lights each bubble the moment its test runs (live), scrubbing lights each test
43
+ // AT the point it actually ran (the event sits at its real position in the stream), and a REPLAY of
44
+ // the recording does the same — the per-test trail is part of the durable record, not a live-only
45
+ // overlay. Attributed to its story via the on-disk test-lists (the agent-log fold keys by
46
+ // feature|story|test). Best-effort: a reporter write must never fail the suite.
47
+ recordTestResultEvent(dir, id, status, file, ts);
40
48
  } catch {
41
49
  /* best-effort: a per-test reporter write must never fail the suite */
42
50
  }
43
51
  }
44
52
 
53
+ /** Owner of a test id, resolved from the project's per-story test-lists (the attribution the reporter
54
+ * lacks — it only sees id + file). Cached per process (a reporter process is one suite run). */
55
+ interface TestOwner { feature_id: string; story_id: string; ac_id: string; scenario_file?: string }
56
+ const ownerIndexCache = new Map<string, Map<string, TestOwner[]>>();
57
+
58
+ function buildOwnerIndex(consortDir: string): Map<string, TestOwner[]> {
59
+ const cached = ownerIndexCache.get(consortDir);
60
+ if (cached) return cached;
61
+ const index = new Map<string, TestOwner[]>();
62
+ try {
63
+ const featuresDir = join(consortDir, "features");
64
+ for (const f of readdirSync(featuresDir)) {
65
+ const storiesDir = join(featuresDir, f, "stories");
66
+ let stories: string[];
67
+ try { stories = readdirSync(storiesDir); } catch { continue; }
68
+ for (const s of stories) {
69
+ const listFile = join(storiesDir, s, "test-list-per-story.json");
70
+ if (!existsSync(listFile)) continue;
71
+ let list: { feature_id?: string; story_id?: string; items?: unknown[] };
72
+ try { list = JSON.parse(readFileSync(listFile, "utf8")); } catch { continue; }
73
+ const featureId = typeof list.feature_id === "string" ? list.feature_id : f;
74
+ const storyId = typeof list.story_id === "string" ? list.story_id : s;
75
+ for (const raw of list.items ?? []) {
76
+ const it = raw as { id?: unknown; ac_id?: unknown; scenario_file?: unknown };
77
+ if (typeof it.id !== "string") continue;
78
+ const owners = index.get(it.id) ?? [];
79
+ owners.push({
80
+ feature_id: featureId,
81
+ story_id: storyId,
82
+ ac_id: typeof it.ac_id === "string" ? it.ac_id : "unknown",
83
+ ...(typeof it.scenario_file === "string" ? { scenario_file: it.scenario_file } : {}),
84
+ });
85
+ index.set(it.id, owners);
86
+ }
87
+ }
88
+ }
89
+ } catch {
90
+ /* no test-lists (pre-design project) — leave the index empty; the kit's cycle-scoped emit covers it */
91
+ }
92
+ ownerIndexCache.set(consortDir, index);
93
+ return index;
94
+ }
95
+
96
+ /** The feature currently on the build lane, from `.consort/next.json` — the disambiguator of last
97
+ * resort when a reused T-id's file does not pin a single owner (e.g. a fitness/behavior test whose
98
+ * test-list item carries no scenario_file). Null when unreadable. */
99
+ function activeFeature(consortDir: string): string | null {
100
+ try {
101
+ const next = JSON.parse(readFileSync(join(consortDir, "next.json"), "utf8")) as { feature?: unknown };
102
+ return typeof next.feature === "string" ? next.feature : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /** Resolve a (test id, file) to its single owning story. One owner -> it. Several (a T-id reused across
109
+ * features — ids are unique only WITHIN a story) -> (1) the owner whose scenario_file basename matches
110
+ * the running file (a client/e2e spec pins ITSELF, so a sibling feature's same-id spec resolves to its
111
+ * own story); then (2) the owner in the feature currently on the build lane (next.json) — a
112
+ * fitness/behavior test carries no scenario_file, so during THIS feature's build its reused id belongs
113
+ * to the active feature's story, while another feature's same-id test already resolved via (1). Else
114
+ * null (skip, never mis-attribute). */
115
+ function resolveOwner(consortDir: string, id: string, file: string): TestOwner | null {
116
+ const base = /^(T\d+)/.exec(id)?.[1] ?? id;
117
+ const index = buildOwnerIndex(consortDir);
118
+ const owners = index.get(id) ?? index.get(base) ?? [];
119
+ if (owners.length === 0) return null;
120
+ if (owners.length === 1) return owners[0];
121
+ // (1) The owner whose scenario_file matches the running file (unique match).
122
+ const fileBase = basename(file || "");
123
+ if (fileBase) {
124
+ const byFile = owners.filter((o) => o.scenario_file && basename(o.scenario_file) === fileBase);
125
+ if (byFile.length === 1) return byFile[0];
126
+ }
127
+ // (2) Fall back to the owner in the currently-building feature (one match only).
128
+ const active = activeFeature(consortDir);
129
+ if (active) {
130
+ const byFeature = owners.filter((o) => o.feature_id === active);
131
+ if (byFeature.length === 1) return byFeature[0];
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /** Append a durable, schema-valid `test.result` to the agent-log (+ the corpus mirror when a recording
137
+ * is in flight), shaped exactly like the kit's emitAgentLogEvent output so the dashboard fold reads it
138
+ * identically. Only emitted for an attributable test; best-effort. */
139
+ export function recordTestResultEvent(consortDir: string, id: string, status: TestRunStatus, file: string, ts: string): void {
140
+ try {
141
+ const owner = resolveOwner(consortDir, id, file);
142
+ if (!owner) return; // cannot attribute (ambiguous reused id) — skip rather than mis-key a bubble
143
+ const line =
144
+ JSON.stringify({
145
+ timestamp: ts,
146
+ level: "info",
147
+ role: "driver",
148
+ event: "test.result",
149
+ message: `TEST ${id} ${status}`,
150
+ metadata: { feature_id: owner.feature_id, test_id: id, status, story_id: owner.story_id, ac_id: owner.ac_id },
151
+ }) + "\n";
152
+ appendFileSync(join(consortDir, "agent-log.jsonl"), line);
153
+ // Mirror into the corpus so a RECORDED run replays the per-test trail (same env the kit uses).
154
+ const recordDir = process.env.LAKEBASE_CONSORT_RECORD_DIR?.trim();
155
+ if (recordDir) {
156
+ try { mkdirSync(recordDir, { recursive: true }); appendFileSync(join(recordDir, "agent-log.jsonl"), line); } catch { /* mirror is best-effort */ }
157
+ }
158
+ } catch {
159
+ /* best-effort: recording a per-test result must never fail the suite */
160
+ }
161
+ }
162
+
45
163
  /** Append a per-test TABLE snapshot (one line per table) to `.consort/table-snapshots.jsonl`, keyed
46
164
  * by test id — parallel to `emitTestRun`. Best-effort. */
47
165
  export function emitTableSnapshot(id: string, absFile: string, table: string, rows: unknown[], rowCount: number | null): void {
@@ -95,11 +95,100 @@ def _consort_emit_test_run(test_id, status, item):
95
95
  rel = str(Path(str(item.fspath))).replace(f"{root}/", "")
96
96
  out = root / ".consort"
97
97
  out.mkdir(parents=True, exist_ok=True)
98
+ ts = datetime.now(timezone.utc).isoformat()
98
99
  line = json.dumps(
99
- {"ts": datetime.now(timezone.utc).isoformat(), "test_id": test_id, "status": status, "file": rel, "kind": "behavior"}
100
+ {"ts": ts, "test_id": test_id, "status": status, "file": rel, "kind": "behavior"}
100
101
  )
101
102
  with open(out / "test-run.jsonl", "a", encoding="utf-8") as fh:
102
103
  fh.write(line + "\n")
104
+ # RECORD a durable per-test test.result into the agent-log AT RUN TIME (mirrors the TS reporter
105
+ # in consort-capture.ts) so the dashboard lights each behavior test the moment it runs, scrubbing
106
+ # lights it at its real run position, and a replay of the recording does the same.
107
+ _consort_record_test_result(out, test_id, status, rel, ts)
108
+ except (OSError, ValueError, TypeError):
109
+ pass
110
+
111
+
112
+ def _consort_test_owner(consort_dir, test_id, file_rel):
113
+ """Resolve (feature_id, story_id, ac_id) for a test id from the per-story test-lists — the
114
+ attribution the reporter lacks (it sees only id + file). Ids are unique only WITHIN a story, so a
115
+ reused id has several owners: (1) the owner whose scenario_file basename matches ``file_rel`` (a
116
+ client/e2e spec pins ITSELF); then (2) the owner in the feature currently on the build lane
117
+ (``next.json``) — a fitness/behavior test carries no scenario_file, so during THIS feature's build
118
+ its reused id belongs to the active feature (a sibling feature's same-id test already resolved via
119
+ (1)). Else None (skip, never mis-key). Mirrors consort-capture.ts resolveOwner. Scoped, best-effort."""
120
+ try:
121
+ import glob
122
+ import json
123
+ import os
124
+ import re
125
+
126
+ m = re.match(r"^(T\d+)", test_id or "")
127
+ base = m.group(1) if m else test_id
128
+ owners = [] # (feature, story, ac, scenario_file)
129
+ for p in glob.glob(os.path.join(str(consort_dir), "features", "*", "stories", "*", "test-list-per-story.json")):
130
+ try:
131
+ with open(p, encoding="utf-8") as fh:
132
+ o = json.load(fh)
133
+ except (OSError, ValueError):
134
+ continue
135
+ feat, story = o.get("feature_id"), o.get("story_id")
136
+ for it in o.get("items", []):
137
+ if it.get("id") in (test_id, base):
138
+ owners.append((feat, story, it.get("ac_id") or "unknown", it.get("scenario_file")))
139
+ if not owners:
140
+ return None
141
+ if len(owners) == 1:
142
+ return owners[0][:3]
143
+ # (1) the owner whose scenario_file matches the running file (unique match)
144
+ fb = os.path.basename(file_rel or "")
145
+ by_file = [o for o in owners if o[3] and os.path.basename(o[3]) == fb]
146
+ if len(by_file) == 1:
147
+ return by_file[0][:3]
148
+ # (2) fall back to the owner in the currently-building feature (one match only)
149
+ try:
150
+ with open(os.path.join(str(consort_dir), "next.json"), encoding="utf-8") as fh:
151
+ active = json.load(fh).get("feature")
152
+ except (OSError, ValueError):
153
+ active = None
154
+ if active:
155
+ by_feat = [o for o in owners if o[0] == active]
156
+ if len(by_feat) == 1:
157
+ return by_feat[0][:3]
158
+ return None
159
+ except (OSError, ValueError, TypeError):
160
+ return None
161
+
162
+
163
+ def _consort_record_test_result(out, test_id, status, file_rel, ts):
164
+ """Append a schema-shaped ``test.result`` to ``.consort/agent-log.jsonl`` (+ the corpus mirror when
165
+ a recording is in flight), identical in shape to the kit's emitAgentLogEvent output so the dashboard
166
+ fold reads it the same. Only for an attributable test; scoped except, never breaks the suite."""
167
+ try:
168
+ import json
169
+ import os
170
+
171
+ owner = _consort_test_owner(out, test_id, file_rel)
172
+ if not owner:
173
+ return
174
+ feat, story, ac = owner
175
+ line = json.dumps(
176
+ {
177
+ "timestamp": ts,
178
+ "level": "info",
179
+ "role": "driver",
180
+ "event": "test.result",
181
+ "message": f"TEST {test_id} {status}",
182
+ "metadata": {"feature_id": feat, "test_id": test_id, "status": status, "story_id": story, "ac_id": ac},
183
+ }
184
+ )
185
+ with open(os.path.join(str(out), "agent-log.jsonl"), "a", encoding="utf-8") as fh:
186
+ fh.write(line + "\n")
187
+ rec = (os.environ.get("LAKEBASE_CONSORT_RECORD_DIR") or "").strip()
188
+ if rec:
189
+ os.makedirs(rec, exist_ok=True)
190
+ with open(os.path.join(rec, "agent-log.jsonl"), "a", encoding="utf-8") as fh:
191
+ fh.write(line + "\n")
103
192
  except (OSError, ValueError, TypeError):
104
193
  pass
105
194