Skip to content

Commit 65bc5cd

Browse files
Merge pull request #6 from devweekends/feat/approval-requested
runner: send approval.requested after task.finished (contracts v0.2.0)
2 parents a4af903 + 948add2 commit 65bc5cd

4 files changed

Lines changed: 32 additions & 10 deletions

File tree

‎runner/src/fake-runner.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export interface FakeRunnerOptions {
1212

1313
type Step = [RunnerEvent["type"], Record<string, unknown>];
1414

15+
const FAKE_STAT = " src/date.js | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n";
16+
17+
1518
function script(prompt: string): Step[] {
1619
return [
1720
["task.started", { agent: "claude-code", model: "fake", prompt }],
@@ -46,7 +49,12 @@ export function createFakeRunner({ delayMs = 300 }: FakeRunnerOptions = {}): Run
4649
store.get(sessionId).lastSeq = seq.last;
4750
yield event;
4851
}
49-
if (!stopped.has(sessionId)) store.transition(sessionId, "awaiting_approval");
52+
53+
if (stopped.has(sessionId)) return;
54+
store.transition(sessionId, "awaiting_approval");
55+
const event = seq.next("approval.requested", { diffStat: FAKE_STAT });
56+
store.get(sessionId).lastSeq = seq.last;
57+
yield event;
5058
}
5159

5260
return {
@@ -72,7 +80,7 @@ export function createFakeRunner({ delayMs = 300 }: FakeRunnerOptions = {}): Run
7280
store.get(sessionId);
7381
return {
7482
patch: "--- a/src/date.js\n+++ b/src/date.js\n@@ -1 +1 @@\n- return d.getDate();\n+ return d.getUTCDate();\n",
75-
stat: " src/date.js | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n",
83+
stat: FAKE_STAT
7684
};
7785
},
7886

‎runner/src/runner.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export function createRunner({
125125
}
126126

127127
// 5. Store the change and put its summary on task.finished, so the phone shows it right away.
128+
// Then ask for approval: the daemon shows Approve / Reject on approval.requested.
128129
try {
129130
record.diff = await diffSince(worktreePath, baseCommit);
130131
finished.payload.diffStat = record.diff.stat;
@@ -133,6 +134,7 @@ export function createRunner({
133134
}
134135
store.transition(sessionId, "awaiting_approval");
135136
yield emit(finished);
137+
yield emit(seq.next("approval.requested", { diffStat: record.diff?.stat ?? "" }));
136138
}
137139

138140
// Everything below needs the worktree that start() made.

‎runner/test/fake-runner.test.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("fake runner", () => {
1515
const runner = createFakeRunner({ delayMs: 0 });
1616
const events = await collect(runner.start(opts));
1717
expect(events[0]!.type).toBe("task.started");
18-
expect(events.at(-1)!.type).toBe("task.finished");
18+
expect(events.map((e) => e.type).slice(-2)).toEqual(["task.finished", "approval.requested"]);
1919
expect(events.map((e) => e.seq)).toEqual(events.map((_, i) => i + 1));
2020
});
2121

@@ -38,6 +38,16 @@ describe("fake runner", () => {
3838
expect(last.type).toBe("task.stopped");
3939
const resumed = await collect(runner.resume(opts.sessionId));
4040
expect(resumed[0]!.seq).toBe(last.seq + 1);
41-
expect(resumed.at(-1)!.type).toBe("task.finished");
41+
expect(resumed.map((e) => e.type).slice(-2)).toEqual(["task.finished", "approval.requested"]);
42+
});
43+
44+
it("does not ask for approval when stopped", async () => {
45+
const runner = createFakeRunner({ delayMs: 0 });
46+
const it1 = runner.start(opts)[Symbol.asyncIterator]();
47+
await it1.next();
48+
await runner.stop(opts.sessionId);
49+
const rest: RunnerEvent[] = [];
50+
for (let r = await it1.next(); !r.done; r = await it1.next()) rest.push(r.value);
51+
expect(rest.map((e) => e.type)).toEqual(["task.stopped"]);
4252
});
4353
});

‎runner/test/runner.test.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ describe("runner.start", () => {
5959
const events = await collect(o, { agentCommand: replayAgent(spike) });
6060

6161
expect(events[0]!.type).toBe("task.started");
62-
expect(events.at(-1)!.type).toBe("task.finished");
63-
expect(events).toHaveLength(17);
62+
expect(events.map((e) => e.type).slice(-2)).toEqual(["task.finished", "approval.requested"]);
63+
expect(events).toHaveLength(18);
6464
expect(events.map((e) => e.seq)).toEqual(events.map((_, i) => i + 1));
6565
expect(events.every((e) => e.sessionId === o.sessionId)).toBe(true);
6666

@@ -116,8 +116,10 @@ describe("runner diff, approve and reject", () => {
116116

117117
it("puts the diff summary on task.finished and returns the full patch, new files included", async () => {
118118
const { runner, o, events } = await finishedRun();
119-
expect(events.at(-1)!.type).toBe("task.finished");
120-
expect(events.at(-1)!.payload.diffStat).toMatch(/2 files changed/);
119+
const finished = events.at(-2)!;
120+
expect(finished.type).toBe("task.finished");
121+
expect(finished.payload.diffStat).toMatch(/2 files changed/);
122+
expect(events.at(-1)).toMatchObject({ type: "approval.requested", payload: { diffStat: finished.payload.diffStat } });
121123
const { patch, stat } = await runner.diff(o.sessionId);
122124
expect(patch).toContain("+world");
123125
expect(patch).toContain("new.txt");
@@ -229,8 +231,8 @@ describe("runner.resume", () => {
229231
const { repo, o, runner, events } = await stoppedRun({ resumeCommand: resumeWith("edit") });
230232
const resumed = await drain(runner.resume(o.sessionId));
231233

232-
expect(resumed.at(-1)!.type).toBe("task.finished");
233-
expect(resumed.at(-1)!.payload.diffStat).toMatch(/2 files changed/);
234+
expect(resumed.map((e) => e.type).slice(-2)).toEqual(["task.finished", "approval.requested"]);
235+
expect(resumed.at(-2)!.payload.diffStat).toMatch(/2 files changed/);
234236
expect([...events, ...resumed].map((e) => e.seq)).toEqual([...events, ...resumed].map((_, i) => i + 1));
235237

236238
const { branch } = await runner.approve(o.sessionId, "Fix the failing test");

0 commit comments

Comments
 (0)