-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathstream-harness.ts
More file actions
152 lines (126 loc) · 3.25 KB
/
Copy pathstream-harness.ts
File metadata and controls
152 lines (126 loc) · 3.25 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
import path from "path"
import { fileURLToPath } from "url"
import readline from "readline"
import { execa } from "execa"
export type StreamEvent = {
type?: string
subtype?: string
requestId?: string
command?: string
content?: string
code?: string
success?: boolean
done?: boolean
id?: number
queueDepth?: number
queue?: Array<{ id?: string; text?: string; imageCount?: number; timestamp?: number }>
tool_use?: {
name?: string
input?: Record<string, unknown>
}
tool_result?: {
name?: string
output?: string
}
}
export type StreamCommand = {
command: "start" | "message" | "cancel" | "ping" | "shutdown"
requestId: string
prompt?: string
images?: string[]
}
export interface StreamCaseContext {
readonly cliRoot: string
readonly timeoutMs: number
nextRequestId(prefix: string): string
sendCommand(command: StreamCommand): void
}
export interface RunStreamCaseOptions {
timeoutMs?: number
onEvent: (event: StreamEvent, context: StreamCaseContext) => void
onTimeoutMessage?: (context: StreamCaseContext) => string
}
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const defaultCliRoot = path.resolve(__dirname, "../../..")
function parseEvent(line: string): StreamEvent | null {
const trimmed = line.trim()
if (!trimmed.startsWith("{")) {
return null
}
try {
return JSON.parse(trimmed) as StreamEvent
} catch {
return null
}
}
export async function runStreamCase(options: RunStreamCaseOptions): Promise<void> {
const cliRoot = process.env.ROO_CLI_ROOT ? path.resolve(process.env.ROO_CLI_ROOT) : defaultCliRoot
const timeoutMs = options.timeoutMs ?? 120_000
const child = execa(
"pnpm",
["dev", "--print", "--stdin-prompt-stream", "--provider", "openrouter", "--output-format", "stream-json"],
{
cwd: cliRoot,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
reject: false,
forceKillAfterDelay: 2_000,
},
)
child.stderr?.on("data", (chunk) => {
process.stderr.write(chunk)
})
let requestCounter = 0
const context: StreamCaseContext = {
cliRoot,
timeoutMs,
nextRequestId(prefix: string): string {
requestCounter += 1
return `${prefix}-${Date.now()}-${requestCounter}`
},
sendCommand(command: StreamCommand): void {
if (child.stdin?.destroyed) {
return
}
child.stdin.write(`${JSON.stringify(command)}\n`)
},
}
let handlerError: Error | null = null
let timedOut = false
const timeout = setTimeout(() => {
timedOut = true
const message = options.onTimeoutMessage?.(context) ?? "timed out waiting for stream scenario completion"
handlerError = new Error(message)
child.kill("SIGTERM")
}, timeoutMs)
const rl = readline.createInterface({
input: child.stdout!,
crlfDelay: Infinity,
})
rl.on("line", (line) => {
process.stdout.write(`${line}\n`)
const event = parseEvent(line)
if (!event) {
return
}
try {
options.onEvent(event, context)
} catch (error) {
handlerError = error instanceof Error ? error : new Error(String(error))
child.kill("SIGTERM")
}
})
const result = await child
clearTimeout(timeout)
rl.close()
if (handlerError) {
throw handlerError
}
if (timedOut) {
throw new Error("stream scenario timed out")
}
if (result.exitCode !== 0) {
throw new Error(`CLI exited with non-zero code: ${result.exitCode}`)
}
}