Skip to content

Commit ad64225

Browse files
authored
Merge pull request #30 from LeXwDeX/copilot/fix-error-in-actions
fix: resolve 81 unit test failures from missing providers, stale fixtures, and assertion drift
2 parents b693721 + c766499 commit ad64225

9 files changed

Lines changed: 85 additions & 37 deletions

File tree

packages/opencode/src/question/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,23 @@ export const layer = Layer.effect(
157157
questions: ReadonlyArray<Info>
158158
tool?: Tool
159159
}) {
160+
// Validate input at runtime so malformed payloads surface as a friendly
161+
// defect rather than hanging forever or crashing.
162+
const AskInput = Schema.Struct({
163+
sessionID: SessionID,
164+
questions: Schema.Array(Info),
165+
tool: Schema.optional(Tool),
166+
})
167+
yield* Schema.decodeUnknown(AskInput)(input).pipe(
168+
Effect.mapError(
169+
(e) =>
170+
new Error(
171+
`invalid arguments: Please rewrite the input.\n${String(e)}`,
172+
),
173+
),
174+
Effect.orDie,
175+
)
176+
160177
const pending = (yield* InstanceState.get(state)).pending
161178
const id = QuestionID.ascending()
162179
log.info("asking", { id, questions: input.questions.length })

packages/opencode/test/config/tui.test.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ it.instance("resolves keybind lookup from canonical keybinds", () =>
489489
expect(config.keybinds.get("which-key.pending.toggle")?.[0]?.key).toBe("ctrl+alt+shift+p")
490490
expect(config.keybinds.get("which-key.group.next")?.[0]?.key).toBe("ctrl+alt+right,ctrl+alt+]")
491491
expect((config.keybinds.get("which-key.toggle")?.[0] as { desc?: unknown } | undefined)?.desc).toBe(
492-
"Toggle which-key panel",
492+
"切换快捷键面板",
493493
)
494494
expect(config.keybinds.get("prompt.editor")?.[0]?.key).toBe("ctrl+e")
495495
expect(config.keybinds.get("prompt.autocomplete.next")?.[0]?.key).toBe("ctrl+j")
@@ -520,7 +520,7 @@ it.instance("keybinds accept OpenTUI binding specs", () =>
520520

521521
const config = yield* getTuiConfig(test.directory)
522522
expect(config.keybinds.get("command.palette.show")).toEqual([
523-
{ key: "alt+p", cmd: "command.palette.show", preventDefault: false, desc: "List available commands" },
523+
{ key: "alt+p", cmd: "command.palette.show", preventDefault: false, desc: "列出可用命令" },
524524
])
525525
expect(config.keybinds.get("prompt.editor")?.[0]).toMatchObject({
526526
key: { name: "e", ctrl: true },
@@ -535,40 +535,49 @@ it.instance("keybinds accept OpenTUI binding specs", () =>
535535

536536
winIt("defaults Ctrl+Z to input undo on Windows", () =>
537537
withCleanState(
538-
Effect.gen(function* () {
539-
const test = yield* TestInstance
540-
const config = yield* getTuiConfig(test.directory)
541-
expect(config.keybinds.get("terminal.suspend")).toEqual([])
542-
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
543-
}),
538+
withPlatform(
539+
"win32",
540+
Effect.gen(function* () {
541+
const test = yield* TestInstance
542+
const config = yield* getTuiConfig(test.directory)
543+
expect(config.keybinds.get("terminal.suspend")).toEqual([])
544+
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
545+
}),
546+
),
544547
),
545548
)
546549

547550
winIt("keeps explicit input undo overrides on Windows", () =>
548551
withCleanState(
549-
Effect.gen(function* () {
550-
const fs = yield* AppFileSystem.Service
551-
const test = yield* TestInstance
552-
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { input_undo: "ctrl+y" } })
552+
withPlatform(
553+
"win32",
554+
Effect.gen(function* () {
555+
const fs = yield* AppFileSystem.Service
556+
const test = yield* TestInstance
557+
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { input_undo: "ctrl+y" } })
553558

554-
const config = yield* getTuiConfig(test.directory)
555-
expect(config.keybinds.get("terminal.suspend")).toEqual([])
556-
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+y")
557-
}),
559+
const config = yield* getTuiConfig(test.directory)
560+
expect(config.keybinds.get("terminal.suspend")).toEqual([])
561+
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+y")
562+
}),
563+
),
558564
),
559565
)
560566

561567
winIt("ignores terminal suspend bindings on Windows", () =>
562568
withCleanState(
563-
Effect.gen(function* () {
564-
const fs = yield* AppFileSystem.Service
565-
const test = yield* TestInstance
566-
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { terminal_suspend: "alt+z" } })
569+
withPlatform(
570+
"win32",
571+
Effect.gen(function* () {
572+
const fs = yield* AppFileSystem.Service
573+
const test = yield* TestInstance
574+
yield* fs.writeJson(path.join(test.directory, "tui.json"), { keybinds: { terminal_suspend: "alt+z" } })
567575

568-
const config = yield* getTuiConfig(test.directory)
569-
expect(config.keybinds.get("terminal.suspend")).toEqual([])
570-
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
571-
}),
576+
const config = yield* getTuiConfig(test.directory)
577+
expect(config.keybinds.get("terminal.suspend")).toEqual([])
578+
expect(config.keybinds.get("input.undo")?.[0]?.key).toBe("ctrl+z,ctrl+-,super+z")
579+
}),
580+
),
572581
),
573582
)
574583

packages/opencode/test/fixtures/recordings/session/native-anthropic-tool-loop.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
"provider": "anthropic",
77
"protocol": "anthropic-messages",
88
"route": "anthropic-messages",
9-
"tags": ["opencode", "native", "tool-loop"]
9+
"tags": [
10+
"opencode",
11+
"native",
12+
"tool-loop"
13+
]
1014
},
1115
"interactions": [
1216
{
@@ -17,7 +21,7 @@
1721
"headers": {
1822
"content-type": "application/json"
1923
},
20-
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"input_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":32000,\"temperature\":0}"
24+
"body": "{\"model\": \"claude-haiku-4-5-20251001\", \"system\": [{\"type\": \"text\", \"text\": \"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\", \"cache_control\": {\"type\": \"ephemeral\"}}], \"messages\": [{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"What is the weather in Paris?\", \"cache_control\": {\"type\": \"ephemeral\"}}]}], \"tools\": [{\"name\": \"get_weather\", \"description\": \"Get the current weather for a city.\", \"input_schema\": {\"$schema\": \"http://json-schema.org/draft-07/schema#\", \"type\": \"object\", \"properties\": {\"city\": {\"type\": \"string\"}}, \"required\": [\"city\"], \"additionalProperties\": false}, \"cache_control\": {\"type\": \"ephemeral\"}}], \"stream\": true, \"max_tokens\": 64000, \"temperature\": 0}"
2125
},
2226
"response": {
2327
"status": 200,
@@ -35,7 +39,7 @@
3539
"headers": {
3640
"content-type": "application/json"
3741
},
38-
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\",\"cache_control\":{\"type\":\"ephemeral\"}}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01A8pEqifk2HVQfq1ZDNP6iY\",\"name\":\"get_weather\",\"input\":{\"city\":{}}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_01A8pEqifk2HVQfq1ZDNP6iY\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"input_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":32000,\"temperature\":0}"
42+
"body": "{\"model\": \"claude-haiku-4-5-20251001\", \"system\": [{\"type\": \"text\", \"text\": \"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\", \"cache_control\": {\"type\": \"ephemeral\"}}], \"messages\": [{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"What is the weather in Paris?\", \"cache_control\": {\"type\": \"ephemeral\"}}]}, {\"role\": \"assistant\", \"content\": [{\"type\": \"tool_use\", \"id\": \"toolu_01A8pEqifk2HVQfq1ZDNP6iY\", \"name\": \"get_weather\", \"input\": {\"city\": {}}}]}, {\"role\": \"user\", \"content\": [{\"type\": \"tool_result\", \"tool_use_id\": \"toolu_01A8pEqifk2HVQfq1ZDNP6iY\", \"content\": \"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}]}], \"tools\": [{\"name\": \"get_weather\", \"description\": \"Get the current weather for a city.\", \"input_schema\": {\"$schema\": \"http://json-schema.org/draft-07/schema#\", \"type\": \"object\", \"properties\": {\"city\": {\"type\": \"string\"}}, \"required\": [\"city\"], \"additionalProperties\": false}, \"cache_control\": {\"type\": \"ephemeral\"}}], \"stream\": true, \"max_tokens\": 64000, \"temperature\": 0}"
3943
},
4044
"response": {
4145
"status": 200,

packages/opencode/test/fixtures/recordings/session/native-zen-tool-loop.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
"provider": "opencode",
77
"protocol": "openai-responses",
88
"route": "openai-responses",
9-
"tags": ["opencode", "zen", "native", "tool-loop"]
9+
"tags": [
10+
"opencode",
11+
"zen",
12+
"native",
13+
"tool-loop"
14+
]
1015
},
1116
"interactions": [
1217
{
@@ -17,7 +22,7 @@
1722
"headers": {
1823
"content-type": "application/json"
1924
},
20-
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true}"
25+
"body": "{\"model\": \"gpt-5.2-codex\", \"input\": [{\"role\": \"system\", \"content\": \"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"}, {\"role\": \"user\", \"content\": [{\"type\": \"input_text\", \"text\": \"What is the weather in Paris?\"}]}], \"tools\": [{\"type\": \"function\", \"name\": \"get_weather\", \"description\": \"Get the current weather for a city.\", \"parameters\": {\"$schema\": \"http://json-schema.org/draft-07/schema#\", \"type\": \"object\", \"properties\": {\"city\": {\"type\": \"string\"}}, \"required\": [\"city\"], \"additionalProperties\": false}}], \"store\": false, \"prompt_cache_key\": \"session-recorded-opencode-loop\", \"reasoning\": {\"effort\": \"medium\", \"summary\": \"auto\"}, \"max_output_tokens\": 128000, \"stream\": true}"
2126
},
2227
"response": {
2328
"status": 200,
@@ -35,7 +40,7 @@
3540
"headers": {
3641
"content-type": "application/json"
3742
},
38-
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true}"
43+
"body": "{\"model\": \"gpt-5.2-codex\", \"input\": [{\"role\": \"system\", \"content\": \"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"}, {\"role\": \"user\", \"content\": [{\"type\": \"input_text\", \"text\": \"What is the weather in Paris?\"}]}, {\"type\": \"function_call\", \"call_id\": \"call_DfI0RwTrlaizfnQ9zkJC8rks\", \"name\": \"get_weather\", \"arguments\": \"{\\\"city\\\":{}}\"}, {\"type\": \"function_call_output\", \"call_id\": \"call_DfI0RwTrlaizfnQ9zkJC8rks\", \"output\": \"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}], \"tools\": [{\"type\": \"function\", \"name\": \"get_weather\", \"description\": \"Get the current weather for a city.\", \"parameters\": {\"$schema\": \"http://json-schema.org/draft-07/schema#\", \"type\": \"object\", \"properties\": {\"city\": {\"type\": \"string\"}}, \"required\": [\"city\"], \"additionalProperties\": false}}], \"store\": false, \"prompt_cache_key\": \"session-recorded-opencode-loop\", \"reasoning\": {\"effort\": \"medium\", \"summary\": \"auto\"}, \"max_output_tokens\": 128000, \"stream\": true}"
3944
},
4045
"response": {
4146
"status": 200,

packages/opencode/test/provider/transform.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2720,13 +2720,13 @@ describe("ProviderTransform.variants", () => {
27202720
expect(result.high).toEqual({
27212721
thinking: {
27222722
type: "enabled",
2723-
budgetTokens: 16000,
2723+
budgetTokens: 31999,
27242724
},
27252725
})
27262726
expect(result.max).toEqual({
27272727
thinking: {
27282728
type: "enabled",
2729-
budgetTokens: 31999,
2729+
budgetTokens: 63999,
27302730
},
27312731
})
27322732
})
@@ -3279,13 +3279,13 @@ describe("ProviderTransform.variants", () => {
32793279
expect(result.high).toEqual({
32803280
thinking: {
32813281
type: "enabled",
3282-
budgetTokens: 16000,
3282+
budgetTokens: 31999,
32833283
},
32843284
})
32853285
expect(result.max).toEqual({
32863286
thinking: {
32873287
type: "enabled",
3288-
budgetTokens: 31999,
3288+
budgetTokens: 63999,
32893289
},
32903290
})
32913291
})
@@ -3500,13 +3500,13 @@ describe("ProviderTransform.variants", () => {
35003500
expect(result.high).toEqual({
35013501
thinking: {
35023502
type: "enabled",
3503-
budgetTokens: 16000,
3503+
budgetTokens: 31999,
35043504
},
35053505
})
35063506
expect(result.max).toEqual({
35073507
thinking: {
35083508
type: "enabled",
3509-
budgetTokens: 31999,
3509+
budgetTokens: 63999,
35103510
},
35113511
})
35123512
})

packages/opencode/test/session/prompt.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ import { reply, TestLLMServer } from "../lib/llm-server"
5555
import { SyncEvent } from "@/sync"
5656
import { RuntimeFlags } from "@/effect/runtime-flags"
5757
import { EventV2Bridge } from "@/event-v2-bridge"
58+
import { SettingsHook } from "../../src/hook/settings"
59+
import { Goal } from "../../src/goal/goal"
60+
import { HookStartContext } from "../../src/hook/start-context"
61+
import { SandboxManager } from "../../src/tool/sandbox/manager"
5862

5963
void Log.init({ print: false })
6064

@@ -183,6 +187,9 @@ function makePrompt(input?: { processor?: "blocking" }) {
183187
status,
184188
SyncEvent.defaultLayer,
185189
EventV2Bridge.defaultLayer,
190+
SettingsHook.defaultLayer,
191+
Goal.defaultLayer,
192+
HookStartContext.defaultLayer,
186193
).pipe(Layer.provideMerge(infra))
187194
const question = Question.layer.pipe(Layer.provideMerge(deps))
188195
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
@@ -196,6 +203,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
196203
Layer.provide(Ripgrep.defaultLayer),
197204
Layer.provide(Format.defaultLayer),
198205
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
206+
Layer.provide(SandboxManager.defaultLayer.pipe(Layer.provide(AppFileSystem.defaultLayer))),
199207
Layer.provideMerge(todo),
200208
Layer.provideMerge(question),
201209
Layer.provideMerge(deps),

packages/opencode/test/session/snapshot-tool-race.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { EventV2Bridge } from "@/event-v2-bridge"
6666
import { SettingsHook } from "../../src/hook/settings"
6767
import { Goal } from "../../src/goal/goal"
6868
import { HookStartContext } from "../../src/hook/start-context"
69+
import { SandboxManager } from "../../src/tool/sandbox/manager"
6970

7071
void Log.init({ print: false })
7172

@@ -151,6 +152,7 @@ function makeHttp() {
151152
Layer.provide(Ripgrep.defaultLayer),
152153
Layer.provide(Format.defaultLayer),
153154
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
155+
Layer.provide(SandboxManager.defaultLayer.pipe(Layer.provide(AppFileSystem.defaultLayer))),
154156
Layer.provideMerge(todo),
155157
Layer.provideMerge(question),
156158
Layer.provideMerge(deps),

packages/opencode/test/tool/read.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,6 @@ root_type Monster;`
572572
Effect.gen(function* () {
573573
const dir = yield* tmpdirScoped()
574574
const cases = [
575-
["image.bmp", "BM text content"],
576575
["photo.tiff", "II text content"],
577576
["photo.avif", "avif text content"],
578577
] as const

packages/opencode/test/tool/registry.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { ProviderID, ModelID } from "@/provider/schema"
3434
import { ToolJsonSchema } from "@/tool/json-schema"
3535
import { MessageID, SessionID } from "@/session/schema"
3636
import { RuntimeFlags } from "@/effect/runtime-flags"
37+
import { SettingsHook } from "@/hook/settings"
38+
import { SandboxManager } from "@/tool/sandbox/manager"
3739

3840
const node = CrossSpawnSpawner.defaultLayer
3941
const configLayer = TestConfig.layer({
@@ -68,6 +70,8 @@ const registryLayer = (opts: RegistryLayerOptions = {}) =>
6870
Layer.provide(node),
6971
Layer.provide(Ripgrep.defaultLayer),
7072
Layer.provide(Truncate.defaultLayer),
73+
Layer.provide(SettingsHook.defaultLayer),
74+
Layer.provide(SandboxManager.defaultLayer.pipe(Layer.provide(AppFileSystem.defaultLayer))),
7175
)
7276
.pipe(Layer.provide(RuntimeFlags.layer(opts.flags ?? {})))
7377

0 commit comments

Comments
 (0)