-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdispatch-server.test.ts
More file actions
167 lines (158 loc) · 9.69 KB
/
Copy pathdispatch-server.test.ts
File metadata and controls
167 lines (158 loc) · 9.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import { createFakePluginHost, makeThreadResponse } from "@get-bb/plugin-sdk/testing";
import { describe, expect, it, vi } from "vitest";
import type { RawUnit } from "./contract.js";
import plugin, { type Board } from "./server.js";
const PATH = "/p/app-abc-101";
const URL = "https://github.com/acme/app/pull/42";
const HOST = "host-a";
function unit(mergeStateStatus: "DIRTY" | "CLEAN"): RawUnit {
return {
path: PATH, dirName: "app-abc-101", repo: "acme/app", branch: "abc-101-fix", dirty: false,
observed: { status: true, pr: true }, ahead: 0, behind: 0, lastCommitAt: "2026-09-23T12:00:00Z",
defaultBranch: "main", shipped: null, changedPaths: ["src/app.ts"],
pr: {
number: 42, state: "OPEN", isDraft: false, reviewDecision: "APPROVED", checkConclusions: ["SUCCESS"],
url: URL, title: "Fix app", mergeable: mergeStateStatus === "DIRTY" ? "CONFLICTING" : "MERGEABLE",
baseRefName: "main", headRefName: "abc-101-fix", latestReviewStates: ["APPROVED"], mergedAt: null,
mergeStateStatus, reviewRequests: [], latestReviews: [], unresolvedReviewThreads: 0, resolvedReviewThreads: 0,
},
};
}
async function setup(second = false) {
const beforeProjects = vi.fn(async () => {});
const spawn = vi.fn(async () => makeThreadResponse({ id: "thr-dispatch", status: "active" }));
const another = { ...unit("DIRTY"), path: "/p/web-abc-101", dirName: "web-abc-101", repo: "acme/web",
pr: { ...unit("DIRTY").pr!, number: 43, url: "https://github.com/acme/web/pull/43" } };
const inspected = new Map([[PATH, unit("DIRTY")], ...(second ? [[another.path, another] as const] : [])]);
let fullScan = second ? [unit("DIRTY"), another] : [unit("DIRTY")];
let inspectCount = 0;
const { bb, harness } = createFakePluginHost({
pluginId: "workstreams",
settings: { scanRoots: "/p" },
sdk: {
system: { config: async () => ({ primaryHostId: HOST }) as never },
projects: { list: async () => { await beforeProjects(); return [{ id: "proj-a", sources: [{ hostId: HOST, path: "/p" }] }] as never; } },
threads: {
list: async () => [] as never, spawn,
get: async ({ threadId }: { threadId: string }) => ({ ...makeThreadResponse({ id: threadId, status: "idle" }), canSpawnChild: true }) as never,
context: async () => ({ usage: null }) as never,
output: async () => ({ output: "Result: Local repair proposed" }),
getPluginMetadata: async () => ({}) as never,
events: { list: async () => [] }, interactions: { list: async () => [] as never },
},
},
experimental_callHostRpc: (call) => {
if (call.method === "scan") return { units: fullScan, warnings: [] };
if (call.method === "inspectPaths") {
inspectCount++;
const inspectedUnit = inspected.get((call.input as { paths: string[] }).paths[0]!);
return { units: inspectedUnit === undefined ? [] : [inspectedUnit], warnings: [] };
}
throw new Error(`Unexpected host call: ${call.method}`);
},
});
await plugin(bb);
expect((await harness.runCli(["refresh"])).exitCode).toBe(0);
const board = async () => await harness.callRpc("board_get", null) as Board;
const current = await board();
const leaf = current.groups.find((group) => !current.groups.some((child) => child.parentKey === group.key));
expect(leaf).toBeDefined();
return { harness, board, leafKey: leaf!.key, spawn, beforeProjects, inspectCount: () => inspectCount,
setInspection: (next: RawUnit) => { inspected.set(next.path, next); },
setFullScan: (next: RawUnit) => { fullScan = [next]; } };
}
describe("dispatcher server wiring", () => {
it("excludes held work from Auto while still allowing a manual agent", async () => {
const env = await setup();
await env.harness.callRpc("pr_hold_set", { prUrl: URL, held: true });
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
expect((await env.board()).dispatch.candidate).toBeNull();
expect(env.spawn).not.toHaveBeenCalled();
expect(await env.harness.callRpc("agent_run", { path: PATH, action: "resolve-conflicts", mode: "new", threadId: null, prompt: "Fix the conflict without merging" })).toMatchObject({ ok: true });
expect(env.spawn).toHaveBeenCalledOnce();
});
it("refuses Auto when a hold arrives during the final project lookup", async () => {
const env = await setup();
env.beforeProjects.mockImplementationOnce(async () => { await env.harness.callRpc("pr_hold_set", { prUrl: URL, held: true }); });
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(async () => expect((await env.board()).dispatch.attempts[0]).toMatchObject({ status: "failed", detail: expect.stringContaining("On hold") }));
expect(env.spawn).not.toHaveBeenCalled();
expect((await env.board()).runs).toEqual([]);
});
it("keeps Off and Shadow read-only, then preflights and launches once after Auto is selected", async () => {
const env = await setup();
expect((await env.board()).dispatch).toMatchObject({ mode: "off", candidate: null, attempts: [] });
await env.harness.callRpc("dispatch_set", { mode: "shadow", effortKey: env.leafKey });
expect((await env.board()).dispatch.candidate).toMatchObject({ action: "resolve-conflicts", path: PATH });
expect(env.spawn).not.toHaveBeenCalled();
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(() => expect(env.spawn).toHaveBeenCalledTimes(1));
expect(env.inspectCount()).toBe(1);
expect((await env.board()).dispatch).toMatchObject({ candidate: null, attempts: [expect.objectContaining({ status: "running", threadId: "thr-dispatch" })] });
expect(await env.harness.callRpc("agent_run", { path: PATH, action: "resolve-conflicts", mode: "new", threadId: null, prompt: "Fix it" })).toMatchObject({ ok: false });
await env.harness.callRpc("board_get", null);
expect(env.spawn).toHaveBeenCalledTimes(1);
});
it("makes one more selection pass when preflight clears the first candidate", async () => {
const env = await setup(true);
env.setInspection(unit("CLEAN"));
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(() => expect(env.spawn).toHaveBeenCalledTimes(1));
expect(env.inspectCount()).toBe(2);
expect((await env.board()).dispatch.attempts[0]).toMatchObject({ path: "/p/web-abc-101", status: "running" });
});
it("marks a completed agent verified only after a fresh inspection clears its gate", async () => {
const env = await setup();
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(() => expect(env.spawn).toHaveBeenCalledTimes(1));
env.setInspection(unit("CLEAN"));
await env.harness.emitThreadEvent("thread.idle", {
thread: makeThreadResponse({ id: "thr-dispatch", status: "idle" }),
lastAssistantText: "Result: Local repair proposed",
});
await vi.waitFor(async () => expect((await env.board()).dispatch.attempts[0]?.status).toBe("verified"));
expect(env.inspectCount()).toBe(2);
});
it("recognizes a merged PR as the endpoint when the fresh inspection reports it", async () => {
const env = await setup();
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(() => expect(env.spawn).toHaveBeenCalledTimes(1));
const merged = unit("CLEAN");
merged.pr = { ...merged.pr!, state: "MERGED" };
env.setInspection(merged);
await env.harness.emitThreadEvent("thread.idle", {
thread: makeThreadResponse({ id: "thr-dispatch", status: "idle" }),
lastAssistantText: "Result: Local repair proposed",
});
await vi.waitFor(async () => expect((await env.board()).dispatch.attempts[0]).toMatchObject({ status: "verified", detail: expect.stringContaining("merged") }));
});
it("pauses when a fresh inspection still shows the gate", async () => {
const env = await setup();
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(() => expect(env.spawn).toHaveBeenCalledTimes(1));
await env.harness.emitThreadEvent("thread.idle", {
thread: makeThreadResponse({ id: "thr-dispatch", status: "idle" }),
lastAssistantText: "Result: Local repair proposed",
});
await vi.waitFor(async () => expect((await env.board()).dispatch.attempts[0]?.status).toBe("needs-you"));
expect((await env.board()).dispatch.candidate).toBeNull();
expect(env.spawn).toHaveBeenCalledTimes(1);
expect(await env.harness.callRpc("agent_run", { path: PATH, action: "resolve-conflicts", mode: "new", threadId: null, prompt: "Fix it" })).toMatchObject({ ok: false });
env.setFullScan(unit("CLEAN"));
expect((await env.harness.runCli(["refresh"])).exitCode).toBe(0);
expect((await env.board()).dispatch.attempts[0]?.status).toBe("verified");
});
it("keeps the effort paused when a full scan clears the gate while the agent awaits a decision", async () => {
const env = await setup();
await env.harness.callRpc("dispatch_set", { mode: "auto", effortKey: env.leafKey });
await vi.waitFor(() => expect(env.spawn).toHaveBeenCalledTimes(1));
await env.harness.emitThreadEvent("interaction.pending", {
thread: makeThreadResponse({ id: "thr-dispatch", status: "active" }), interaction: {} as never,
});
await vi.waitFor(async () => expect((await env.board()).dispatch.attempts[0]?.status).toBe("needs-you"));
env.setFullScan(unit("CLEAN"));
expect((await env.harness.runCli(["refresh"])).exitCode).toBe(0);
expect((await env.board()).dispatch).toMatchObject({ candidate: null, attempts: [expect.objectContaining({ status: "needs-you" })] });
expect(env.spawn).toHaveBeenCalledTimes(1);
});
});