Repository navigation
Expand file tree
/
Copy pathfake-runner.ts
More file actions
96 lines (81 loc) · 3.32 KB
/
Copy pathfake-runner.ts
File metadata and controls
96 lines (81 loc) · 3.32 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
import { setTimeout as sleep } from "node:timers/promises";
import type { Runner, RunnerEvent, StartOptions } from "@codehands/contracts";
import { EventSequence } from "./events.js";
import { SessionStore } from "./session-store.js";
// A stand-in for the real runner. Midhat's daemon plugs this in today and gets
// events in the agreed shape. Also the demo backup if a live run fails.
export interface FakeRunnerOptions {
delayMs?: number;
}
type Step = [RunnerEvent["type"], Record<string, unknown>];
const FAKE_STAT = " src/date.js | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n";
function script(prompt: string): Step[] {
return [
["task.started", { agent: "claude-code", model: "fake", prompt }],
["message", { text: "I'll look at the failing test first." }],
["file.read", { path: "test/date.test.js" }],
["file.read", { path: "src/date.js" }],
["command.started", { command: "npm test" }],
["command.finished", { command: "npm test", exitCode: 1 }],
["test.result", { command: "npm test", passed: false }],
["file.changed", { path: "src/date.js", tool: "Edit" }],
["command.started", { command: "npm test" }],
["command.finished", { command: "npm test", exitCode: 0 }],
["test.result", { command: "npm test", passed: true }],
["task.finished", { total_cost_usd: 0, duration_ms: 0, num_turns: 4, usage: {}, result: "Fixed the date bug." }],
];
}
export function createFakeRunner({ delayMs = 300 }: FakeRunnerOptions = {}): Runner {
const store = new SessionStore();
const stopped = new Set<string>();
async function* play(sessionId: string, steps: Step[], seq: EventSequence): AsyncIterable<RunnerEvent> {
for (const [type, payload] of steps) {
if (stopped.has(sessionId)) {
const event = seq.next("task.stopped", {});
store.get(sessionId).lastSeq = seq.last;
yield event;
return;
}
await sleep(delayMs);
const event = seq.next(type, payload);
store.get(sessionId).lastSeq = seq.last;
yield event;
}
if (stopped.has(sessionId)) return;
store.transition(sessionId, "awaiting_approval");
const event = seq.next("approval.requested", { diffStat: FAKE_STAT });
store.get(sessionId).lastSeq = seq.last;
yield event;
}
return {
start(o: StartOptions) {
store.create(o);
store.transition(o.sessionId, "running");
return play(o.sessionId, script(o.prompt), new EventSequence(o.sessionId));
},
async stop(sessionId) {
stopped.add(sessionId);
store.transition(sessionId, "stopped");
},
resume(sessionId, prompt) {
const record = store.transition(sessionId, "running");
stopped.delete(sessionId);
const steps = script(prompt ?? record.options.prompt).slice(-4);
return play(sessionId, steps, new EventSequence(sessionId, record.lastSeq));
},
async diff(sessionId) {
store.get(sessionId);
return {
patch: "--- a/src/date.js\n+++ b/src/date.js\n@@ -1 +1 @@\n- return d.getDate();\n+ return d.getUTCDate();\n",
stat: FAKE_STAT
};
},
async approve(sessionId) {
store.transition(sessionId, "approved");
return { branch: `codehands/fake-${sessionId.slice(0, 8)}`, commit: "0000000" };
},
async reject(sessionId) {
store.transition(sessionId, "rejected");
},
};
}