diff --git a/apps/macos/Tests/OpenClawIPCTests/ExecApprovalQueueStoreTests.swift b/apps/macos/Tests/OpenClawIPCTests/ExecApprovalQueueStoreTests.swift index d2f53198495a..7e4d2be0ac4f 100644 --- a/apps/macos/Tests/OpenClawIPCTests/ExecApprovalQueueStoreTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/ExecApprovalQueueStoreTests.swift @@ -123,6 +123,10 @@ private final class ApprovalGatewayFixture: @unchecked Sendable { socket.emitReceiveSuccessOnce(.data(Data(event.utf8))) } + func waitUntilReady() async throws { + _ = try await self.readySocket() + } + private func readySocket() async throws -> GatewayTestWebSocketTask { let deadline = ContinuousClock.now + .seconds(2) while ContinuousClock.now < deadline { @@ -210,16 +214,20 @@ struct ExecApprovalQueueStoreTests { try #require(await self.waitUntil { store.requests.isEmpty }) } - @Test func `expired requests disappear without a gateway resolution event`() async throws { - let fixture = ApprovalGatewayFixture(initialRequests: [ - ApprovalFixtureRequest(id: "short-lived", expiresOffsetMs: 500), - ]) + @Test func `live requests disappear at expiry without a gateway resolution event`() async throws { + let fixture = ApprovalGatewayFixture() let store = ExecApprovalQueueStore(gateway: fixture.gateway) defer { store.stop() } - + store.start() await store.refresh() - #expect(store.requests.map(\.id) == ["short-lived"]) + + try await fixture.waitUntilReady() + let request = ApprovalFixtureRequest(id: "short-lived", expiresOffsetMs: 500) + try await fixture.sendEvent(name: "exec.approval.requested", payload: request.json) + + try #require(await self.waitUntil { store.requests.map(\.id) == ["short-lived"] }) try #require(await self.waitUntil { store.requests.isEmpty }) + #expect(await fixture.requestLog.requests(method: "exec.approval.resolve").isEmpty) } @Test func `explicit decision policy excludes allow always and blocks unavailable decisions`() async { diff --git a/extensions/opencode-go/index.test.ts b/extensions/opencode-go/index.test.ts index bc5c7003195c..9b8fa3a39181 100644 --- a/extensions/opencode-go/index.test.ts +++ b/extensions/opencode-go/index.test.ts @@ -598,25 +598,32 @@ describe("opencode-go provider plugin", () => { }); it.each([ - ["openai-completions", "https://opencode.ai/zen/go/v1"], - ["anthropic-messages", "https://opencode.ai/zen/go"], - ] as const)("sends stable conversation identity to the %s endpoint", async (api, baseUrl) => { - const provider = await registerSingleProviderPlugin(plugin); - const streamFn = vi.fn(() => ({}) as never); - const wrapped = provider.wrapStreamFn?.({ streamFn } as never); + ["stream", "openai-completions", "https://opencode.ai/zen/go/v1"], + ["stream", "anthropic-messages", "https://opencode.ai/zen/go"], + ["simple completion", "openai-completions", "https://opencode.ai/zen/go/v1"], + ["simple completion", "anthropic-messages", "https://opencode.ai/zen/go"], + ] as const)( + "sends stable conversation identity through the %s %s endpoint", + async (surface, api, baseUrl) => { + const provider = await registerSingleProviderPlugin(plugin); + const streamFn = vi.fn(() => ({}) as never); + const wrapper = + surface === "stream" ? provider.wrapStreamFn : provider.wrapSimpleCompletionStreamFn; + const wrapped = wrapper?.({ streamFn } as never); - await wrapped?.( - { provider: "opencode-go", id: "fixture", api, baseUrl } as never, - { messages: [] } as never, - { sessionId: "conversation-123" }, - ); + await wrapped?.( + { provider: "opencode-go", id: "fixture", api, baseUrl } as never, + { messages: [] } as never, + { sessionId: "conversation-123" }, + ); - expect(streamFn).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ headers: { "x-opencode-session": "conversation-123" } }), - ); - }); + expect(streamFn).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ headers: { "x-opencode-session": "conversation-123" } }), + ); + }, + ); it("preserves explicit session routing and leaves custom proxies unchanged", async () => { const provider = await registerSingleProviderPlugin(plugin); diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts index dde8248e94a4..baff7397bdd6 100644 --- a/extensions/opencode-go/index.ts +++ b/extensions/opencode-go/index.ts @@ -14,7 +14,7 @@ import { resolveOpencodeGoStarterModel, } from "./provider-catalog.js"; import { resolveThinkingProfile } from "./provider-policy-api.js"; -import { createOpencodeGoWrapper } from "./stream.js"; +import { createOpencodeGoSessionHeaderWrapper, createOpencodeGoWrapper } from "./stream.js"; const PROVIDER_ID = "opencode-go"; type OpencodeGoCatalogAuth = { apiKey?: string; discoveryApiKey?: string }; @@ -119,6 +119,7 @@ export default defineSingleProviderPluginEntry({ ...buildProviderReplayFamilyHooks({ family: "passthrough-gemini" }), resolveThinkingProfile, wrapStreamFn: (ctx) => createOpencodeGoWrapper(ctx.streamFn, ctx.thinkingLevel), + wrapSimpleCompletionStreamFn: (ctx) => createOpencodeGoSessionHeaderWrapper(ctx.streamFn), isModernModelRef: () => true, }, register(api) { diff --git a/extensions/opencode-go/stream.ts b/extensions/opencode-go/stream.ts index 3f77d5e2732f..f90f9f5ca4b2 100644 --- a/extensions/opencode-go/stream.ts +++ b/extensions/opencode-go/stream.ts @@ -24,7 +24,7 @@ function hasOpencodeSessionHeader(headers: Record | undefined): return Object.keys(headers ?? {}).some((name) => name.toLowerCase() === OPENCODE_SESSION_HEADER); } -function createOpencodeGoSessionHeaderWrapper( +export function createOpencodeGoSessionHeaderWrapper( baseStreamFn: ProviderWrapStreamFnContext["streamFn"], ): ProviderWrapStreamFnContext["streamFn"] { if (!baseStreamFn) {