@tpsdev-ai/flair 0.49.0 → 0.50.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.
@@ -16,7 +16,7 @@
16
16
  * Implementation note: does NOT talk to Flair. Fixture-to-fixture only.
17
17
  * Lives here so slice-3c code plugins can reuse the same harness.
18
18
  */
19
- import { promises as fsp } from "node:fs";
19
+ import { promises as fsp, readdirSync, rmSync, statSync } from "node:fs";
20
20
  import { join, isAbsolute } from "node:path";
21
21
  import { tmpdir } from "node:os";
22
22
  import { randomUUID } from "node:crypto";
@@ -25,6 +25,74 @@ import { parseRecords } from "./formats.js";
25
25
  import { applyMap } from "./mapper.js";
26
26
  import { evaluatePredicate } from "./predicate.js";
27
27
  import { writeRecords } from "./writers.js";
28
+ import { hasScratchOwnerStamp, scratchOwnerIsLive, writeScratchOwnerStamp, } from "../../lib/scratch-owner.js";
29
+ /** mkdtemp prefix for this harness. Leaked trees of this name filled a host
30
+ * to zero bytes (flair#1032) — 7,188 leftovers, mostly `…-test-bridge-*`. */
31
+ export const BRIDGE_TEST_DIR_PREFIX = "flair-bridge-test-";
32
+ /** Grace for *unstamped* leftovers only (stamp not written yet, or a tree
33
+ * from before the owner file existed). Stamped trees use pid liveness. */
34
+ export const STALE_BRIDGE_TEST_DIR_MS = 60_000;
35
+ const IN_FLIGHT_BRIDGE_TEST_DIRS = new Set();
36
+ let bridgeTestExitHookInstalled = false;
37
+ function installBridgeTestExitHook() {
38
+ if (bridgeTestExitHookInstalled)
39
+ return;
40
+ bridgeTestExitHookInstalled = true;
41
+ // Exit only — no signal handlers. The integration suite SIGTERMs the
42
+ // runner as a fixture (federation-watch); a handler here would truncate it.
43
+ process.on("exit", () => {
44
+ for (const dir of IN_FLIGHT_BRIDGE_TEST_DIRS) {
45
+ try {
46
+ rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ catch { /* best effort */ }
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Remove leftover `flair-bridge-test-*` trees from earlier interrupted runs.
54
+ *
55
+ * A process-exit hook cannot cover SIGKILL or a hard crash, so the next
56
+ * harness start has to sweep what the last one left. Skips in-flight trees
57
+ * in this process and any tree whose owner pid is still alive. Directory
58
+ * mtime is not a liveness signal (Linux does not update it when files
59
+ * inside subdirs are appended) — it is only a race guard for unstamped dirs.
60
+ *
61
+ * @returns number of trees removed
62
+ */
63
+ export function sweepStaleBridgeTestDirs(opts) {
64
+ const root = opts?.root ?? tmpdir();
65
+ const olderThanMs = opts?.olderThanMs ?? STALE_BRIDGE_TEST_DIR_MS;
66
+ const now = Date.now();
67
+ let names;
68
+ try {
69
+ names = readdirSync(root);
70
+ }
71
+ catch {
72
+ return 0;
73
+ }
74
+ let removed = 0;
75
+ for (const name of names) {
76
+ if (!name.startsWith(BRIDGE_TEST_DIR_PREFIX))
77
+ continue;
78
+ const dir = join(root, name);
79
+ if (IN_FLIGHT_BRIDGE_TEST_DIRS.has(dir))
80
+ continue;
81
+ if (scratchOwnerIsLive(dir))
82
+ continue;
83
+ try {
84
+ if (!hasScratchOwnerStamp(dir)) {
85
+ const st = statSync(dir);
86
+ if (now - st.mtimeMs < olderThanMs)
87
+ continue;
88
+ }
89
+ rmSync(dir, { recursive: true, force: true });
90
+ removed++;
91
+ }
92
+ catch { /* gone, or not ours to remove */ }
93
+ }
94
+ return removed;
95
+ }
28
96
  /** Fields the spec (§8) requires to survive round-trip. */
29
97
  export const ROUND_TRIP_STABLE_FIELDS = ["content", "subject", "tags", "durability"];
30
98
  export async function runRoundTrip(opts) {
@@ -52,8 +120,29 @@ export async function runRoundTrip(opts) {
52
120
  const importSource = descriptor.import.sources[0];
53
121
  const exportTarget = descriptor.export.targets[0];
54
122
  const resolvedFixture = resolvePath(cwd, opts.fixturePath ?? importSource.path);
55
- const tmpDir = await fsp.mkdtemp(join(tmpdir(), `flair-bridge-test-${descriptor.name}-`));
123
+ // Sweep leftovers from interrupted runs *before* creating ours so a crash
124
+ // mid-call is all the next start has to find. In-flight trees in this
125
+ // process are skipped inside the sweep.
126
+ sweepStaleBridgeTestDirs();
127
+ const tmpDir = await fsp.mkdtemp(join(tmpdir(), `${BRIDGE_TEST_DIR_PREFIX}${descriptor.name}-`));
128
+ writeScratchOwnerStamp(tmpDir);
56
129
  const tmpPath = join(tmpDir, `roundtrip.${suffixForFormat(exportTarget.format)}`);
130
+ IN_FLIGHT_BRIDGE_TEST_DIRS.add(tmpDir);
131
+ installBridgeTestExitHook();
132
+ try {
133
+ return await runRoundTripAgainst(descriptor, resolvedFixture, importSource, exportTarget, tmpPath);
134
+ }
135
+ finally {
136
+ IN_FLIGHT_BRIDGE_TEST_DIRS.delete(tmpDir);
137
+ if (!opts.retainTmpDir) {
138
+ try {
139
+ await fsp.rm(tmpDir, { recursive: true, force: true });
140
+ }
141
+ catch { /* best effort */ }
142
+ }
143
+ }
144
+ }
145
+ async function runRoundTripAgainst(descriptor, resolvedFixture, importSource, exportTarget, tmpPath) {
57
146
  // Pass 1: fixture → BridgeMemory[]
58
147
  const pass1 = [];
59
148
  for await (const { record } of parseRecords(descriptor.name, resolvedFixture, importSource.format)) {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.49.0",
3
- "commit": "e6336fcbb25c46fcd53b00488d695659c61cc420",
4
- "builtAt": "2026-08-24T19:36:52.866Z",
2
+ "version": "0.50.0",
3
+ "commit": "d643b7f8addb9fd033209fad1d405f7c935a7247",
4
+ "builtAt": "2026-08-25T23:24:38.875Z",
5
5
  "builder": "tsc"
6
6
  }