forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub-action.test.ts
More file actions
198 lines (174 loc) · 6.28 KB
/
github-action.test.ts
File metadata and controls
198 lines (174 loc) · 6.28 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { test, expect, describe } from "bun:test"
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
// Helper to create minimal valid parts
function createTextPart(text: string): MessageV2.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("s"),
messageID: MessageID.make("m"),
type: "text" as const,
text,
}
}
function createReasoningPart(text: string): MessageV2.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("s"),
messageID: MessageID.make("m"),
type: "reasoning" as const,
text,
time: { start: 0 },
}
}
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
if (status === "completed") {
return {
id: PartID.ascending(),
sessionID: SessionID.make("s"),
messageID: MessageID.make("m"),
type: "tool" as const,
callID: "c1",
tool,
state: {
status: "completed",
input: {},
output: "",
title,
metadata: {},
time: { start: 0, end: 1 },
},
}
}
return {
id: PartID.ascending(),
sessionID: SessionID.make("s"),
messageID: MessageID.make("m"),
type: "tool" as const,
callID: "c1",
tool,
state: {
status: "running",
input: {},
time: { start: 0 },
},
}
}
function createStepStartPart(): MessageV2.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("s"),
messageID: MessageID.make("m"),
type: "step-start" as const,
}
}
function createStepFinishPart(): MessageV2.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("s"),
messageID: MessageID.make("m"),
type: "step-finish" as const,
reason: "done",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
}
}
describe("extractResponseText", () => {
test("returns text from text part", () => {
const parts = [createTextPart("Hello world")]
expect(extractResponseText(parts)).toBe("Hello world")
})
test("returns last text part when multiple exist", () => {
const parts = [createTextPart("First"), createTextPart("Last")]
expect(extractResponseText(parts)).toBe("Last")
})
test("returns text even when tool parts follow", () => {
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
expect(extractResponseText(parts)).toBe("I'll help with that.")
})
test("returns null for reasoning-only response (signals summary needed)", () => {
const parts = [createReasoningPart("Let me think about this...")]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for tool-only response (signals summary needed)", () => {
// This is the exact scenario from the bug report - todowrite with no text
const parts = [createToolPart("todowrite", "8 todos")]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for multiple completed tools", () => {
const parts = [
createToolPart("read", "src/file.ts"),
createToolPart("edit", "src/file.ts"),
createToolPart("bash", "bun test"),
]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for running tool parts (signals summary needed)", () => {
const parts = [createToolPart("bash", "", "running")]
expect(extractResponseText(parts)).toBeNull()
})
test("throws on empty array", () => {
expect(() => extractResponseText([])).toThrow("no parts returned")
})
test("returns null for step-start only", () => {
const parts = [createStepStartPart()]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for step-finish only", () => {
const parts = [createStepFinishPart()]
expect(extractResponseText(parts)).toBeNull()
})
test("returns null for step-start and step-finish", () => {
const parts = [createStepStartPart(), createStepFinishPart()]
expect(extractResponseText(parts)).toBeNull()
})
test("returns text from multi-step response", () => {
const parts = [
createStepStartPart(),
createToolPart("read", "src/file.ts"),
createTextPart("Done"),
createStepFinishPart(),
]
expect(extractResponseText(parts)).toBe("Done")
})
test("prefers text over reasoning when both present", () => {
const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
expect(extractResponseText(parts)).toBe("Final answer")
})
test("prefers text over tools when both present", () => {
const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
expect(extractResponseText(parts)).toBe("Here's what I found")
})
})
describe("formatPromptTooLargeError", () => {
test("formats error without files", () => {
const result = formatPromptTooLargeError([])
expect(result).toBe("PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.")
})
test("formats error with files (base64 content)", () => {
// Base64 is ~33% larger than original, so we multiply by 0.75 to get original size
// 400 KB base64 = 300 KB original, 200 KB base64 = 150 KB original
const files = [
{ filename: "screenshot.png", content: "a".repeat(400 * 1024) },
{ filename: "diagram.png", content: "b".repeat(200 * 1024) },
]
const result = formatPromptTooLargeError(files)
expect(result).toStartWith("PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.")
expect(result).toInclude("Files in prompt:")
expect(result).toInclude("screenshot.png (300 KB)")
expect(result).toInclude("diagram.png (150 KB)")
})
test("lists all files when multiple present", () => {
// Base64 sizes: 4KB -> 3KB, 8KB -> 6KB, 12KB -> 9KB
const files = [
{ filename: "img1.png", content: "x".repeat(4 * 1024) },
{ filename: "img2.jpg", content: "y".repeat(8 * 1024) },
{ filename: "img3.gif", content: "z".repeat(12 * 1024) },
]
const result = formatPromptTooLargeError(files)
expect(result).toInclude("img1.png (3 KB)")
expect(result).toInclude("img2.jpg (6 KB)")
expect(result).toInclude("img3.gif (9 KB)")
})
})