@polderlabs/bizar-plugin 0.5.4 → 0.6.1

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.
@@ -92,6 +92,19 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
92
92
  .positive()
93
93
  .optional()
94
94
  .describe("Collect-time timeout in ms (1s..30min, default 5min)."),
95
+ persistent: z
96
+ .boolean()
97
+ .optional()
98
+ .default(false)
99
+ .describe("When true, auto-restart on terminal failure (up to maxRestarts)."),
100
+ maxRestarts: z
101
+ .number()
102
+ .int()
103
+ .min(1)
104
+ .max(10)
105
+ .optional()
106
+ .default(3)
107
+ .describe("Number of auto-restart attempts before giving up."),
95
108
  },
96
109
  execute: async (rawArgs, ctx) => {
97
110
  // 1. Odin-only (MEDIUM-26).
@@ -109,6 +122,8 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
109
122
  prompt: string;
110
123
  model?: string;
111
124
  timeoutMs?: number;
125
+ persistent?: boolean;
126
+ maxRestarts?: number;
112
127
  };
113
128
 
114
129
  // 2. Validate the model parameter (LOW-34 / §1.4).
@@ -118,7 +133,7 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
118
133
  if (m === null) {
119
134
  return {
120
135
  output: JSON.stringify({
121
- error: `model must be in "providerID/modelID" format (e.g. "minimax/MiniMax-M3"). Omit to use the agent's default.`,
136
+ error: `model must be in "providerID/modelID" format (e.g. "openrouter/minimax-m3"). Omit to use the agent's default.`,
122
137
  }),
123
138
  };
124
139
  }
@@ -146,10 +161,15 @@ export function createBgSpawnTool(deps: BgSpawnDeps) {
146
161
  ? `${modelOverride.providerID}/${modelOverride.modelID}`
147
162
  : "agent-default",
148
163
  promptPreview: args.prompt.slice(0, 200),
164
+ prompt: args.prompt, // store full prompt for restart support
149
165
  parentAgent: ctx.agent,
150
166
  logPath: buildLogPath(deps.worktree, instanceId),
151
167
  timeoutMs,
152
168
  toolCallCount: 0,
169
+ // v0.5.5 — persistent auto-restart
170
+ persistent: args.persistent ?? false,
171
+ maxRestarts: args.maxRestarts ?? 3,
172
+ restartCount: 0,
153
173
  };
154
174
  const addRes = await deps.instanceManager.add(draft);
155
175
  if (addRes === "cap_reached") {
@@ -14,6 +14,8 @@
14
14
  */
15
15
 
16
16
  import { describe, it, expect, beforeEach } from "bun:test";
17
+ import os from "node:os";
18
+ import path from "node:path";
17
19
 
18
20
  // --- Real InstanceManager (the one under test) ----------------------------
19
21
 
@@ -76,8 +78,8 @@ class InMemoryStateStore {
76
78
 
77
79
  // We import the real InstanceManager after the stubs are defined so the
78
80
  // test file fails fast if the real signature changes.
79
- import { InstanceManager } from "../src/background.ts";
80
- import type { BackgroundState } from "../src/background-state.ts";
81
+ import { InstanceManager } from "../src/background.js";
82
+ import type { BackgroundState } from "../src/background-state.js";
81
83
 
82
84
  function makeDraft(overrides: Partial<BackgroundState> = {}): BackgroundState {
83
85
  return {
@@ -86,14 +88,14 @@ function makeDraft(overrides: Partial<BackgroundState> = {}): BackgroundState {
86
88
  agent: "mimir",
87
89
  status: "pending",
88
90
  startedAt: Date.now(),
89
- model: "minimax/MiniMax-M3",
91
+ model: "openrouter/minimax-m3",
90
92
  promptPreview: "test",
91
93
  resultPreview: undefined,
92
94
  resultMessageIds: [],
93
95
  error: undefined,
94
96
  parentAgent: "odin",
95
97
  parentInstanceId: undefined,
96
- logPath: "/tmp/test.log",
98
+ logPath: path.join(os.tmpdir(), "test.log"),
97
99
  timeoutMs: 300_000,
98
100
  toolCallCount: 0,
99
101
  loopGuardTool: undefined,
@@ -122,7 +124,7 @@ describe("InstanceManager.add — empty sessionId (BUGFIX v0.5.1)", () => {
122
124
  warn: () => {},
123
125
  error: () => {},
124
126
  } as never,
125
- serve: { worktree: "/tmp" } as never,
127
+ serve: { worktree: os.tmpdir() } as never,
126
128
  http: {} as never,
127
129
  stream: stream as never,
128
130
  stallTimeoutMs: 180_000,
@@ -40,7 +40,7 @@ function makeState(overrides: Partial<BackgroundState> = {}): BackgroundState {
40
40
  agent: "mimir",
41
41
  status: "running",
42
42
  startedAt: Date.now(),
43
- model: "minimax/MiniMax-M3",
43
+ model: "openrouter/minimax-m3",
44
44
  promptPreview: "Do the thing",
45
45
  resultPreview: undefined,
46
46
  resultMessageIds: [],
@@ -22,7 +22,7 @@ function makeBgState(overrides: Partial<BackgroundState> = {}): BackgroundState
22
22
  agent: "mimir",
23
23
  status: "pending",
24
24
  startedAt: Date.now(),
25
- model: "minimax/MiniMax-M3",
25
+ model: "openrouter/minimax-m3",
26
26
  promptPreview: "Do the thing",
27
27
  resultPreview: undefined,
28
28
  resultMessageIds: [],
@@ -17,6 +17,8 @@
17
17
  */
18
18
 
19
19
  import { describe, test, expect } from "bun:test";
20
+ import os from "node:os";
21
+ import path from "node:path";
20
22
 
21
23
  import { decide } from "../src/loop.js";
22
24
  import {
@@ -58,7 +60,7 @@ function emptyState(): SessionState {
58
60
 
59
61
  const FP = "fp:read:loop";
60
62
  const TOOL = "read";
61
- const ARGS = { path: "/tmp/example.txt" };
63
+ const ARGS = { path: path.join(os.tmpdir(), "example.txt") };
62
64
  const NOW = 1_700_000_500_000;
63
65
 
64
66
  // For the block tests we need a window size that is at least as large as
@@ -8,25 +8,29 @@
8
8
 
9
9
  import { describe, test, expect } from "bun:test";
10
10
  import { fingerprint } from "../src/fingerprint";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+
14
+ const TMP = path.join(os.tmpdir(), "canonical-key-order-test");
11
15
 
12
16
  describe("fingerprint — canonical key order", () => {
13
17
  test("flat object: same keys/values in different insertion order produce the same fingerprint", () => {
14
18
  const a = {
15
19
  tool: "read",
16
- args: { path: "/tmp/foo.ts", recursive: false, limit: 10 },
20
+ args: { path: path.join(os.tmpdir(), "foo.ts"), recursive: false, limit: 10 },
17
21
  };
18
22
  const b = {
19
23
  tool: "read",
20
- args: { limit: 10, recursive: false, path: "/tmp/foo.ts" },
24
+ args: { limit: 10, recursive: false, path: path.join(os.tmpdir(), "foo.ts") },
21
25
  };
22
26
  // Same keys, same values, different insertion order — must match.
23
- expect(fingerprint(a.tool, a.args, "/tmp")).toBe(fingerprint(b.tool, b.args, "/tmp"));
27
+ expect(fingerprint(a.tool, a.args, TMP)).toBe(fingerprint(b.tool, b.args, TMP));
24
28
  });
25
29
 
26
30
  test("nested objects: different insertion order at both levels also match", () => {
27
31
  const a = { tool: "edit", args: { meta: { z: 1, a: 2 }, path: "/x" } };
28
32
  const b = { tool: "edit", args: { path: "/x", meta: { a: 2, z: 1 } } };
29
- expect(fingerprint(a.tool, a.args, "/tmp")).toBe(fingerprint(b.tool, b.args, "/tmp"));
33
+ expect(fingerprint(a.tool, a.args, TMP)).toBe(fingerprint(b.tool, b.args, TMP));
30
34
  });
31
35
 
32
36
  test("deeply nested: three levels of differing key order all resolve to same fingerprint", () => {
@@ -54,18 +58,18 @@ describe("fingerprint — canonical key order", () => {
54
58
  },
55
59
  },
56
60
  };
57
- expect(fingerprint(a.tool, a.args, "/tmp")).toBe(fingerprint(b.tool, b.args, "/tmp"));
61
+ expect(fingerprint(a.tool, a.args, TMP)).toBe(fingerprint(b.tool, b.args, TMP));
58
62
  });
59
63
 
60
64
  test("array order is preserved (arrays of same values in same order match)", () => {
61
65
  const a = { tool: "bash", args: { commands: ["echo a", "echo b"] } };
62
66
  const b = { tool: "bash", args: { commands: ["echo a", "echo b"] } };
63
- expect(fingerprint(a.tool, a.args, "/tmp")).toBe(fingerprint(b.tool, b.args, "/tmp"));
67
+ expect(fingerprint(a.tool, a.args, TMP)).toBe(fingerprint(b.tool, b.args, TMP));
64
68
  });
65
69
 
66
70
  test("array with different order produces different fingerprint", () => {
67
71
  const a = { tool: "bash", args: { commands: ["echo a", "echo b"] } };
68
72
  const b = { tool: "bash", args: { commands: ["echo b", "echo a"] } };
69
- expect(fingerprint(a.tool, a.args, "/tmp")).not.toBe(fingerprint(b.tool, b.args, "/tmp"));
73
+ expect(fingerprint(a.tool, a.args, TMP)).not.toBe(fingerprint(b.tool, b.args, TMP));
70
74
  });
71
75
  });
@@ -65,7 +65,7 @@ type Event = EventSessionIdle | EventSessionError | EventMessagePartUpdated;
65
65
  class FakeEventStream {
66
66
  private handlers = new Map<string, Array<(event: Event) => void>>();
67
67
  private sessions = new Map<string, string>(); // instanceId → sessionId
68
- private instances = new Map<string, BackgroundState>();
68
+ public instances = new Map<string, BackgroundState>();
69
69
  private closed = false;
70
70
 
71
71
  /** Register an instance's sessionId for event routing */
@@ -406,4 +406,4 @@ describe("EventStream interface contract", () => {
406
406
  expect(typeof stream.onSessionEvent).toBe("function");
407
407
  expect(typeof stream.close).toBe("function");
408
408
  });
409
- });
409
+ });
@@ -125,7 +125,7 @@ class MockPlugin {
125
125
 
126
126
  // ── Test setup ───────────────────────────────────────────────────────────────
127
127
 
128
- const TEST_DIR = "/tmp/bizar-event-test";
128
+ const TEST_DIR = path.join(os.tmpdir(), "bizar-event-test");
129
129
  const TEST_SESSION = "session-evt-001";
130
130
  const TEST_SESSION_2 = "session-evt-002";
131
131
 
@@ -8,10 +8,11 @@ import { describe, test, expect } from "bun:test";
8
8
  import { fingerprint } from "../src/fingerprint";
9
9
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
10
10
  import path from "node:path";
11
+ import os from "node:os";
11
12
 
12
13
  /** Use a real temp directory as the worktree for path normalization tests */
13
- const WORKTREE = "/tmp/bizar-fingerprint-test-worktree";
14
- const OUTSIDE_TREE = "/tmp/bizar-outside-worktree";
14
+ const WORKTREE = path.join(os.tmpdir(), "bizar-fingerprint-test-worktree");
15
+ const OUTSIDE_TREE = path.join(os.tmpdir(), "bizar-outside-worktree");
15
16
 
16
17
  function setupWorktree() {
17
18
  try {
@@ -33,22 +34,22 @@ setupWorktree();
33
34
 
34
35
  describe("fingerprint — stable hash", () => {
35
36
  test("same args produce the same fingerprint", () => {
36
- const args = { path: "/tmp/foo.ts", recursive: false };
37
+ const args = { path: path.join(os.tmpdir(), "foo.ts"), recursive: false };
37
38
  const a = fingerprint("read", args, WORKTREE);
38
39
  const b = fingerprint("read", args, WORKTREE);
39
40
  expect(a).toBe(b);
40
41
  });
41
42
 
42
43
  test("different tool name produces different fingerprint", () => {
43
- const args = { path: "/tmp/foo.ts" };
44
+ const args = { path: path.join(os.tmpdir(), "foo.ts") };
44
45
  const a = fingerprint("read", args, WORKTREE);
45
46
  const b = fingerprint("edit", args, WORKTREE);
46
47
  expect(a).not.toBe(b);
47
48
  });
48
49
 
49
50
  test("different args produce different fingerprint", () => {
50
- const a = fingerprint("read", { path: "/tmp/foo.ts" }, WORKTREE);
51
- const b = fingerprint("read", { path: "/tmp/bar.ts" }, WORKTREE);
51
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts") }, WORKTREE);
52
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "bar.ts") }, WORKTREE);
52
53
  expect(a).not.toBe(b);
53
54
  });
54
55
 
@@ -74,7 +75,7 @@ describe("fingerprint — path normalization", () => {
74
75
  const fp2 = fingerprint("read", { path: outside }, WORKTREE);
75
76
  expect(fp1).toBe(fp2);
76
77
  // Must NOT collide with a different outside path
77
- const different = "/tmp/different/path/file.txt";
78
+ const different = path.join(os.tmpdir(), "different/path/file.txt");
78
79
  const fp3 = fingerprint("read", { path: different }, WORKTREE);
79
80
  expect(fp1).not.toBe(fp3);
80
81
  });
@@ -90,38 +91,38 @@ describe("fingerprint — path normalization", () => {
90
91
 
91
92
  describe("fingerprint — noise field stripping", () => {
92
93
  test("strips timestamp fields", () => {
93
- const a = fingerprint("read", { path: "/tmp/foo.ts", createdAt: 1234567890 }, WORKTREE);
94
- const b = fingerprint("read", { path: "/tmp/foo.ts", createdAt: 9999999999 }, WORKTREE);
94
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), createdAt: 1234567890 }, WORKTREE);
95
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), createdAt: 9999999999 }, WORKTREE);
95
96
  expect(a).toBe(b);
96
97
  });
97
98
 
98
99
  test("strips updatedAt timestamp fields", () => {
99
- const a = fingerprint("read", { path: "/tmp/foo.ts", updatedAt: "2024-01-01T00:00:00Z" }, WORKTREE);
100
- const b = fingerprint("read", { path: "/tmp/foo.ts", updatedAt: "2025-12-31T23:59:59Z" }, WORKTREE);
100
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), updatedAt: "2024-01-01T00:00:00Z" }, WORKTREE);
101
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), updatedAt: "2025-12-31T23:59:59Z" }, WORKTREE);
101
102
  expect(a).toBe(b);
102
103
  });
103
104
 
104
105
  test("strips id field", () => {
105
- const a = fingerprint("read", { path: "/tmp/foo.ts", id: "abc123" }, WORKTREE);
106
- const b = fingerprint("read", { path: "/tmp/foo.ts", id: "xyz789" }, WORKTREE);
106
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), id: "abc123" }, WORKTREE);
107
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), id: "xyz789" }, WORKTREE);
107
108
  expect(a).toBe(b);
108
109
  });
109
110
 
110
111
  test("strips uuid field", () => {
111
- const a = fingerprint("read", { path: "/tmp/foo.ts", uuid: "550e8400-e29b-41d4-a716-446655440000" }, WORKTREE);
112
- const b = fingerprint("read", { path: "/tmp/foo.ts", uuid: "6ba7b810-9dad-11d1-80b4-00c04fd430c8" }, WORKTREE);
112
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), uuid: "550e8400-e29b-41d4-a716-446655440000" }, WORKTREE);
113
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), uuid: "6ba7b810-9dad-11d1-80b4-00c04fd430c8" }, WORKTREE);
113
114
  expect(a).toBe(b);
114
115
  });
115
116
 
116
117
  test("strips nonce field", () => {
117
- const a = fingerprint("read", { path: "/tmp/foo.ts", nonce: "random1" }, WORKTREE);
118
- const b = fingerprint("read", { path: "/tmp/foo.ts", nonce: "random2" }, WORKTREE);
118
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), nonce: "random1" }, WORKTREE);
119
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), nonce: "random2" }, WORKTREE);
119
120
  expect(a).toBe(b);
120
121
  });
121
122
 
122
123
  test("strips cwd field entirely", () => {
123
- const a = fingerprint("read", { path: "/tmp/foo.ts", cwd: "/home/user" }, WORKTREE);
124
- const b = fingerprint("read", { path: "/tmp/foo.ts", cwd: "/completely/different" }, WORKTREE);
124
+ const a = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), cwd: "/home/user" }, WORKTREE);
125
+ const b = fingerprint("read", { path: path.join(os.tmpdir(), "foo.ts"), cwd: "/completely/different" }, WORKTREE);
125
126
  expect(a).toBe(b);
126
127
  });
127
128
  });
@@ -130,10 +131,10 @@ describe("fingerprint — nested objects", () => {
130
131
  test("nested objects are normalized recursively", () => {
131
132
  const a = fingerprint("edit", {
132
133
  meta: { author: "Alice", timestamp: 1000 },
133
- path: "/tmp/foo.ts",
134
+ path: path.join(os.tmpdir(), "foo.ts"),
134
135
  }, WORKTREE);
135
136
  const b = fingerprint("edit", {
136
- path: "/tmp/foo.ts",
137
+ path: path.join(os.tmpdir(), "foo.ts"),
137
138
  meta: { timestamp: 9999, author: "Alice" },
138
139
  }, WORKTREE);
139
140
  expect(a).toBe(b);
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect } from "bun:test";
9
+ import os from "node:os";
10
+ import path from "node:path";
9
11
 
10
12
  // ---------------------------------------------------------------------------
11
13
  // Types that mirror the expected HttpClient API from §1 / §2
@@ -33,10 +35,7 @@ interface Message {
33
35
  parts: MessagePart[];
34
36
  }
35
37
 
36
- interface CollectedMessage {
37
- info: Message;
38
- parts: MessagePart[];
39
- }
38
+ type CollectedMessage = Message;
40
39
 
41
40
  // ---------------------------------------------------------------------------
42
41
  // Fake HttpClient matching the expected interface
@@ -129,12 +128,12 @@ describe("HttpClient.createSession", () => {
129
128
  parentID: "parent_sess",
130
129
  title: "bgr:mimir:bgr_01",
131
130
  agent: "mimir",
132
- directory: "/tmp/worktree",
131
+ directory: path.join(os.tmpdir(), "worktree"),
133
132
  });
134
133
 
135
134
  expect(session.id).toBeTruthy();
136
135
  expect(session.projectID).toBe("proj_test");
137
- expect(session.directory).toBe("/tmp/worktree");
136
+ expect(session.directory).toBe(path.join(os.tmpdir(), "worktree"));
138
137
  expect(session.parentID).toBe("parent_sess");
139
138
  expect(session.title).toBe("bgr:mimir:bgr_01");
140
139
  });
@@ -185,8 +184,10 @@ describe("HttpClient.sendPrompt", () => {
185
184
  const msgs = await client.listMessages(session.id, "/tmp");
186
185
  const userMsg = msgs.find((m) => m.info.role === "user");
187
186
  expect(userMsg).toBeDefined();
188
- expect(userMsg!.parts[0].type).toBe("text");
189
- expect(userMsg!.parts[0].text).toBe("Do the research on X");
187
+ const firstPart = userMsg?.parts[0];
188
+ expect(firstPart).toBeDefined();
189
+ expect(firstPart?.type).toBe("text");
190
+ expect(firstPart?.text).toBe("Do the research on X");
190
191
  });
191
192
 
192
193
  it("generates a unique messageID per call (HIGH-2)", async () => {
@@ -261,7 +262,7 @@ describe("HttpClient.sendPrompt", () => {
261
262
  messageID: "msg_model_test",
262
263
  prompt: "Use a specific model",
263
264
  agent: "mimir",
264
- model: { providerID: "minimax", modelID: "MiniMax-M3" },
265
+ model: { providerID: "openrouter", modelID: "minimax-m3" },
265
266
  directory: "/tmp",
266
267
  });
267
268
 
@@ -400,4 +401,4 @@ describe("HTTP timeout handling (HIGH-36)", () => {
400
401
  // This is ensured by the implementation not writing on AbortError
401
402
  expect(true).toBe(true);
402
403
  });
403
- });
404
+ });
@@ -182,10 +182,10 @@ describe("readValidSessionIds — postmortem Layer 1 fix (v0.5.2)", () => {
182
182
  // no-op `.catch(() => undefined)` to suppress the unhandled-
183
183
  // rejection. We can't directly observe unhandled rejections in
184
184
  // bun:test, but we can verify the function returns cleanly.
185
- let rejectFn: ((err: Error) => void) | null = null;
185
+ let rejectFn: ((err: Error) => void) | undefined;
186
186
  const input = makeInput({
187
187
  session: {
188
- list: () => new Promise((_, reject) => {
188
+ list: () => new Promise((_, reject: (err: Error) => void) => {
189
189
  rejectFn = reject;
190
190
  }),
191
191
  },
@@ -195,7 +195,7 @@ describe("readValidSessionIds — postmortem Layer 1 fix (v0.5.2)", () => {
195
195
  // Now reject the hanging promise AFTER the function returned.
196
196
  // If the no-op catch is missing, this would be an unhandled
197
197
  // rejection. With it, the process keeps running.
198
- if (rejectFn) rejectFn(new Error("late rejection"));
198
+ rejectFn?.(new Error("late rejection"));
199
199
  // Give the microtask queue a chance to run.
200
200
  await new Promise((r) => setTimeout(r, 10));
201
201
  },
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import os from "node:os";
3
+ import path from "node:path";
2
4
  import {
3
5
  DEFAULT_OPTIONS,
4
6
  expandHome,
@@ -126,9 +128,9 @@ describe("normalizeOptions", () => {
126
128
  });
127
129
 
128
130
  test("custom logDir and stateDir are preserved", () => {
129
- const { options } = normalizeOptions({ logDir: "/tmp/my-logs", stateDir: "/tmp/my-state" });
130
- expect(options.logDir).toBe("/tmp/my-logs");
131
- expect(options.stateDir).toBe("/tmp/my-state");
131
+ const { options } = normalizeOptions({ logDir: path.join(os.tmpdir(), "my-logs"), stateDir: path.join(os.tmpdir(), "my-state") });
132
+ expect(options.logDir).toBe(path.join(os.tmpdir(), "my-logs"));
133
+ expect(options.stateDir).toBe(path.join(os.tmpdir(), "my-state"));
132
134
  });
133
135
  });
134
136
 
@@ -138,7 +140,7 @@ describe("findSecretDirMatch", () => {
138
140
  const home = process.env.HOME ?? "/home/test";
139
141
 
140
142
  test("returns null for safe paths", () => {
141
- expect(findSecretDirMatch("/tmp/foo")).toBeNull();
143
+ expect(findSecretDirMatch(path.join(os.tmpdir(), "foo"))).toBeNull();
142
144
  expect(findSecretDirMatch("/home/user/project")).toBeNull();
143
145
  });
144
146
 
@@ -181,8 +183,8 @@ describe("findOffendingPath", () => {
181
183
  test("returns null when both paths are safe", () => {
182
184
  const opts: NormalizedOptions = {
183
185
  ...D,
184
- logDir: "/tmp/bizar-logs",
185
- stateDir: "/tmp/bizar-state",
186
+ logDir: path.join(os.tmpdir(), "bizar-logs"),
187
+ stateDir: path.join(os.tmpdir(), "bizar-state"),
186
188
  };
187
189
  expect(findOffendingPath(opts)).toBeNull();
188
190
  });
@@ -192,7 +194,7 @@ describe("findOffendingPath", () => {
192
194
  const opts: NormalizedOptions = {
193
195
  ...D,
194
196
  logDir: `${home}/.ssh/evil`,
195
- stateDir: "/tmp/bizar-state",
197
+ stateDir: path.join(os.tmpdir(), "bizar-state"),
196
198
  };
197
199
  const result = findOffendingPath(opts);
198
200
  expect(result).not.toBeNull();
@@ -204,7 +206,7 @@ describe("findOffendingPath", () => {
204
206
  const home = process.env.HOME ?? "/home/test";
205
207
  const opts: NormalizedOptions = {
206
208
  ...D,
207
- logDir: "/tmp/bizar-logs",
209
+ logDir: path.join(os.tmpdir(), "bizar-logs"),
208
210
  stateDir: `${home}/.aws/creds`,
209
211
  };
210
212
  const result = findOffendingPath(opts);
@@ -155,7 +155,7 @@ describe("ENOENT and EACCES handling", () => {
155
155
  describe("unexpected exit", () => {
156
156
  it("marks all running instances failed when serve child exits unexpectedly", async () => {
157
157
  // Simulate: proc.exited resolves with non-zero code (not intentional shutdown)
158
- const exitCode = 1;
158
+ const exitCode: number = 1;
159
159
  const runningInstances = [
160
160
  { instanceId: "bgr_01", status: "running", error: undefined as string | undefined },
161
161
  { instanceId: "bgr_02", status: "pending", error: undefined as string | undefined },
@@ -171,14 +171,18 @@ describe("unexpected exit", () => {
171
171
  }
172
172
  }
173
173
 
174
- expect(runningInstances[0].status).toBe("failed");
175
- expect(runningInstances[0].error).toBe("serve child exited unexpectedly");
176
- expect(runningInstances[1].status).toBe("failed");
177
- expect(runningInstances[1].error).toBe("serve child exited unexpectedly");
174
+ const first = runningInstances[0];
175
+ const second = runningInstances[1];
176
+ expect(first).toBeDefined();
177
+ expect(second).toBeDefined();
178
+ expect(first?.status).toBe("failed");
179
+ expect(first?.error).toBe("serve child exited unexpectedly");
180
+ expect(second?.status).toBe("failed");
181
+ expect(second?.error).toBe("serve child exited unexpectedly");
178
182
  });
179
183
 
180
184
  it("clean exit (code 0) does not mark instances failed", async () => {
181
- const exitCode = 0;
185
+ const exitCode: number = 0;
182
186
  const runningInstances = [
183
187
  { instanceId: "bgr_01", status: "running" },
184
188
  ];
@@ -189,11 +193,11 @@ describe("unexpected exit", () => {
189
193
  }
190
194
  }
191
195
 
192
- expect(runningInstances[0].status).toBe("running"); // unchanged
196
+ expect(runningInstances[0]?.status).toBe("running"); // unchanged
193
197
  });
194
198
 
195
199
  it("intentional shutdown does not trigger unexpected-exit path", async () => {
196
- const exitCode = 1;
200
+ const exitCode: number = 1;
197
201
  const intentionalShutdown = true;
198
202
 
199
203
  const runningInstances = [{ instanceId: "bgr_01", status: "running" }];
@@ -204,7 +208,7 @@ describe("unexpected exit", () => {
204
208
  for (const inst of runningInstances) inst.status = "failed";
205
209
  }
206
210
 
207
- expect(runningInstances[0].status).toBe("running");
211
+ expect(runningInstances[0]?.status).toBe("running");
208
212
  });
209
213
 
210
214
  it("servePID is cleared after unexpected exit", async () => {
@@ -332,4 +336,4 @@ describe("ServeLifecycle interface contract", () => {
332
336
  expect(typeof fake.password).toBe("string");
333
337
  expect(typeof fake.baseUrl).toBe("string");
334
338
  });
335
- });
339
+ });
@@ -44,7 +44,7 @@ class MockLogger {
44
44
  error(m: string) { this.messages.push({ level: "error", message: m }); }
45
45
  }
46
46
 
47
- const TEST_DIR = "/tmp/bizar-settings-test";
47
+ const TEST_DIR = path.join(os.tmpdir(), "bizar-settings-test");
48
48
 
49
49
  beforeEach(() => {
50
50
  try { rmSync(TEST_DIR, { recursive: true, force: true }); } catch { /* ok */ }
@@ -252,7 +252,7 @@ describe("SettingsStore — file path", () => {
252
252
  });
253
253
 
254
254
  test("settings written with ~ path land in the expanded location", async () => {
255
- const tmp = "/tmp/bizar-settings-expansion-test";
255
+ const tmp = path.join(os.tmpdir(), "bizar-settings-expansion-test");
256
256
  rmSync(tmp, { recursive: true, force: true });
257
257
  try {
258
258
  const logger = makeLogger();
@@ -16,6 +16,7 @@
16
16
  import { describe, it, expect, beforeEach, afterEach } from "bun:test";
17
17
  import { writeFileSync, mkdirSync, unlinkSync, rmSync } from "node:fs";
18
18
  import path from "node:path";
19
+ import os from "node:os";
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // Group 1 — researchInterventionPrompt
@@ -130,7 +131,7 @@ const silentLogger = {
130
131
  };
131
132
 
132
133
  function makeTempDir(prefix: string): string {
133
- const dir = `/tmp/bizar-stall-test-${prefix}-${process.pid}`;
134
+ const dir = path.join(os.tmpdir(), `bizar-stall-test-${prefix}-${process.pid}`);
134
135
  mkdirSync(dir, { recursive: true });
135
136
  return dir;
136
137
  }
@@ -161,11 +162,11 @@ describe("BackgroundState schema backfill", () => {
161
162
  agent: "mimir",
162
163
  status: "running",
163
164
  startedAt,
164
- model: "minimax/MiniMax-M3",
165
+ model: "openrouter/minimax-m3",
165
166
  promptPreview: "Do the thing",
166
167
  toolCallCount: 0,
167
168
  parentAgent: "odin",
168
- logPath: "/tmp/test.log",
169
+ logPath: path.join(os.tmpdir(), "test.log"),
169
170
  timeoutMs: 300_000,
170
171
  // lastEventAt is intentionally absent
171
172
  lastToolOrTextAt: startedAt,
@@ -188,11 +189,11 @@ describe("BackgroundState schema backfill", () => {
188
189
  agent: "mimir",
189
190
  status: "running",
190
191
  startedAt,
191
- model: "minimax/MiniMax-M3",
192
+ model: "openrouter/minimax-m3",
192
193
  promptPreview: "Do the thing",
193
194
  toolCallCount: 0,
194
195
  parentAgent: "odin",
195
- logPath: "/tmp/test.log",
196
+ logPath: path.join(os.tmpdir(), "test.log"),
196
197
  timeoutMs: 300_000,
197
198
  lastEventAt: startedAt,
198
199
  // lastToolOrTextAt is intentionally absent
@@ -215,11 +216,11 @@ describe("BackgroundState schema backfill", () => {
215
216
  agent: "mimir",
216
217
  status: "running",
217
218
  startedAt,
218
- model: "minimax/MiniMax-M3",
219
+ model: "openrouter/minimax-m3",
219
220
  promptPreview: "Do the thing",
220
221
  toolCallCount: 0,
221
222
  parentAgent: "odin",
222
- logPath: "/tmp/test.log",
223
+ logPath: path.join(os.tmpdir(), "test.log"),
223
224
  timeoutMs: 300_000,
224
225
  lastEventAt: startedAt,
225
226
  lastToolOrTextAt: startedAt,
@@ -436,7 +437,7 @@ function makeBgState(overrides: Partial<BackgroundState> = {}): BackgroundState
436
437
  agent: "mimir",
437
438
  status: "running",
438
439
  startedAt: now,
439
- model: "minimax/MiniMax-M3",
440
+ model: "openrouter/minimax-m3",
440
441
  promptPreview: "Do the thing",
441
442
  resultPreview: undefined,
442
443
  resultMessageIds: [],
@@ -699,11 +700,11 @@ describe("bg-status toView — v0.3.0 fields", () => {
699
700
  agent: "mimir",
700
701
  status: "running",
701
702
  startedAt: now - 600_000,
702
- model: "minimax/MiniMax-M3",
703
+ model: "openrouter/minimax-m3",
703
704
  promptPreview: "Research X",
704
705
  toolCallCount: 0,
705
706
  parentAgent: "odin",
706
- logPath: "/tmp/test.log",
707
+ logPath: path.join(os.tmpdir(), "test.log"),
707
708
  timeoutMs: 300_000,
708
709
  lastEventAt: now - 60_000,
709
710
  lastToolOrTextAt: now - 60_000,
@@ -728,11 +729,11 @@ describe("bg-status toView — v0.3.0 fields", () => {
728
729
  agent: "mimir",
729
730
  status: "running",
730
731
  startedAt: now,
731
- model: "minimax/MiniMax-M3",
732
+ model: "openrouter/minimax-m3",
732
733
  promptPreview: "Research Y",
733
734
  toolCallCount: 0,
734
735
  parentAgent: "odin",
735
- logPath: "/tmp/test.log",
736
+ logPath: path.join(os.tmpdir(), "test.log"),
736
737
  timeoutMs: 300_000,
737
738
  // v0.3.0 fields are absent (no intervention yet)
738
739
  };