@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.
- package/README.md +1 -1
- package/dist/index.js +29901 -0
- package/index.ts +94 -11
- package/package.json +1 -1
- package/src/background-state.ts +56 -4
- package/src/background.ts +166 -12
- package/src/commands-impl.ts +95 -0
- package/src/commands.ts +321 -91
- package/src/plan-fs.ts +2 -2
- package/src/reasoning-clean.ts +360 -0
- package/src/serve-info.ts +228 -0
- package/src/serve.ts +24 -4
- package/src/tools/bg-spawn.ts +21 -1
- package/tests/attach-handler-bug.test.ts +7 -5
- package/tests/background-state.test.ts +1 -1
- package/tests/background.test.ts +1 -1
- package/tests/block.test.ts +3 -1
- package/tests/canonical-key-order.test.ts +11 -7
- package/tests/event-stream.test.ts +2 -2
- package/tests/event.test.ts +1 -1
- package/tests/fingerprint.test.ts +22 -21
- package/tests/http-client.test.ts +11 -10
- package/tests/init-helpers.test.ts +3 -3
- package/tests/options.test.ts +10 -8
- package/tests/serve.test.ts +14 -10
- package/tests/settings.test.ts +2 -2
- package/tests/stall-think.test.ts +13 -12
- package/tests/state.test.ts +2 -1
- package/tests/tools/bg-get-comments.test.ts +2 -2
- package/tests/tools/bg-kill.test.ts +9 -5
- package/tests/tools/bg-spawn.test.ts +12 -12
- package/tests/tools/bg-status.test.ts +2 -1
- package/tests/tools/plan-action.test.ts +2 -2
- package/tests/tools/wait-for-feedback.test.ts +2 -2
- package/tests/update-deadlock.test.ts +144 -0
package/src/tools/bg-spawn.ts
CHANGED
|
@@ -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
|
|
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.
|
|
80
|
-
import type { BackgroundState } from "../src/background-state.
|
|
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
|
|
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: "
|
|
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:
|
|
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
|
|
43
|
+
model: "openrouter/minimax-m3",
|
|
44
44
|
promptPreview: "Do the thing",
|
|
45
45
|
resultPreview: undefined,
|
|
46
46
|
resultMessageIds: [],
|
package/tests/background.test.ts
CHANGED
|
@@ -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
|
|
25
|
+
model: "openrouter/minimax-m3",
|
|
26
26
|
promptPreview: "Do the thing",
|
|
27
27
|
resultPreview: undefined,
|
|
28
28
|
resultMessageIds: [],
|
package/tests/block.test.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
+
});
|
package/tests/event.test.ts
CHANGED
|
@@ -125,7 +125,7 @@ class MockPlugin {
|
|
|
125
125
|
|
|
126
126
|
// ── Test setup ───────────────────────────────────────────────────────────────
|
|
127
127
|
|
|
128
|
-
const TEST_DIR = "
|
|
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 = "
|
|
14
|
-
const OUTSIDE_TREE = "
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
51
|
-
const b = fingerprint("read", { path: "
|
|
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 = "
|
|
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: "
|
|
94
|
-
const b = fingerprint("read", { path: "
|
|
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: "
|
|
100
|
-
const b = fingerprint("read", { path: "
|
|
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: "
|
|
106
|
-
const b = fingerprint("read", { path: "
|
|
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: "
|
|
112
|
-
const b = fingerprint("read", { path: "
|
|
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: "
|
|
118
|
-
const b = fingerprint("read", { path: "
|
|
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: "
|
|
124
|
-
const b = fingerprint("read", { path: "
|
|
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: "
|
|
134
|
+
path: path.join(os.tmpdir(), "foo.ts"),
|
|
134
135
|
}, WORKTREE);
|
|
135
136
|
const b = fingerprint("edit", {
|
|
136
|
-
path: "
|
|
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
|
-
|
|
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: "
|
|
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("
|
|
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
|
-
|
|
189
|
-
expect(
|
|
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: "
|
|
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) |
|
|
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
|
-
|
|
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
|
},
|
package/tests/options.test.ts
CHANGED
|
@@ -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: "
|
|
130
|
-
expect(options.logDir).toBe("
|
|
131
|
-
expect(options.stateDir).toBe("
|
|
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("
|
|
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: "
|
|
185
|
-
stateDir: "
|
|
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: "
|
|
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: "
|
|
209
|
+
logDir: path.join(os.tmpdir(), "bizar-logs"),
|
|
208
210
|
stateDir: `${home}/.aws/creds`,
|
|
209
211
|
};
|
|
210
212
|
const result = findOffendingPath(opts);
|
package/tests/serve.test.ts
CHANGED
|
@@ -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
|
-
|
|
175
|
-
|
|
176
|
-
expect(
|
|
177
|
-
expect(
|
|
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]
|
|
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]
|
|
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
|
+
});
|
package/tests/settings.test.ts
CHANGED
|
@@ -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 = "
|
|
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 = "
|
|
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 =
|
|
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
|
|
165
|
+
model: "openrouter/minimax-m3",
|
|
165
166
|
promptPreview: "Do the thing",
|
|
166
167
|
toolCallCount: 0,
|
|
167
168
|
parentAgent: "odin",
|
|
168
|
-
logPath: "
|
|
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
|
|
192
|
+
model: "openrouter/minimax-m3",
|
|
192
193
|
promptPreview: "Do the thing",
|
|
193
194
|
toolCallCount: 0,
|
|
194
195
|
parentAgent: "odin",
|
|
195
|
-
logPath: "
|
|
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
|
|
219
|
+
model: "openrouter/minimax-m3",
|
|
219
220
|
promptPreview: "Do the thing",
|
|
220
221
|
toolCallCount: 0,
|
|
221
222
|
parentAgent: "odin",
|
|
222
|
-
logPath: "
|
|
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
|
|
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
|
|
703
|
+
model: "openrouter/minimax-m3",
|
|
703
704
|
promptPreview: "Research X",
|
|
704
705
|
toolCallCount: 0,
|
|
705
706
|
parentAgent: "odin",
|
|
706
|
-
logPath: "
|
|
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
|
|
732
|
+
model: "openrouter/minimax-m3",
|
|
732
733
|
promptPreview: "Research Y",
|
|
733
734
|
toolCallCount: 0,
|
|
734
735
|
parentAgent: "odin",
|
|
735
|
-
logPath: "
|
|
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
|
};
|