Merge remote-tracking branch 'origin/dev' into fix/cli-run-premature-exit-with-background-tasks

This commit is contained in:
柯杨
2026-04-22 09:45:56 +08:00
390 changed files with 49705 additions and 41270 deletions
+11 -11
View File
@@ -98,7 +98,7 @@ describe("message.part.delta handling", () => {
sessionID: "ses_main",
role: "assistant",
agent: "Sisyphus - Ultraworker",
modelID: "claude-opus-4-6",
modelID: "claude-opus-4-7",
variant: "max",
},
},
@@ -113,7 +113,7 @@ describe("message.part.delta handling", () => {
//#then
const rendered = stdoutSpy.mock.calls.map((call) => String(call[0] ?? "")).join("")
expect(rendered).toContain("\u001b[38;2;0;206;209m")
expect(rendered).toContain("claude-opus-4-6 (max)")
expect(rendered).toContain("claude-opus-4-7 (max)")
expect(rendered).toContain("└─")
expect(rendered).toContain("Sisyphus - Ultraworker")
stdoutSpy.mockRestore()
@@ -128,7 +128,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6" },
info: { sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7" },
},
},
{
@@ -187,7 +187,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6" },
info: { sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7" },
},
},
{
@@ -242,7 +242,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6" },
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7" },
},
},
{
@@ -309,7 +309,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6" },
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7" },
},
},
{
@@ -353,7 +353,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6", variant: "max" },
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7", variant: "max" },
},
},
{
@@ -388,7 +388,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_user", sessionID: "ses_main", role: "user", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6" },
info: { id: "msg_user", sessionID: "ses_main", role: "user", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7" },
},
},
{
@@ -410,7 +410,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-6" },
info: { id: "msg_assistant", sessionID: "ses_main", role: "assistant", agent: "Sisyphus - Ultraworker", modelID: "claude-opus-4-7" },
},
},
{
@@ -619,7 +619,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_1", sessionID: "ses_main", role: "assistant", agent: "Sisyphus", modelID: "claude-opus-4-6" },
info: { id: "msg_1", sessionID: "ses_main", role: "assistant", agent: "Sisyphus", modelID: "claude-opus-4-7" },
},
},
{
@@ -634,7 +634,7 @@ describe("message.part.delta handling", () => {
{
type: "message.updated",
properties: {
info: { id: "msg_1", sessionID: "ses_main", role: "assistant", agent: "Sisyphus", modelID: "claude-opus-4-6" },
info: { id: "msg_1", sessionID: "ses_main", role: "assistant", agent: "Sisyphus", modelID: "claude-opus-4-7" },
},
},
{
+55 -3
View File
@@ -3,6 +3,7 @@ import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun
import * as originalSdk from "@opencode-ai/sdk"
import * as originalPortUtils from "../../shared/port-utils"
import * as originalBinaryResolver from "./opencode-binary-resolver"
import * as originalServerAuth from "../../shared/opencode-server-auth"
const originalConsole = globalThis.console
@@ -13,11 +14,15 @@ const mockCreateOpencode = mock(() =>
server: { url: "http://127.0.0.1:4096", close: mockServerClose },
})
)
const mockCreateOpencodeClient = mock(() => ({ session: {} }))
const mockCreateOpencodeClient = mock((options?: { baseUrl?: string }) => ({
session: {},
baseUrl: options?.baseUrl,
}))
const mockIsPortAvailable = mock(() => Promise.resolve(true))
const mockGetAvailableServerPort = mock(() => Promise.resolve({ port: 4096, wasAutoSelected: false }))
const mockConsoleLog = mock(() => {})
const mockWithWorkingOpencodePath = mock((startServer: () => Promise<unknown>) => startServer())
const mockInjectServerAuthIntoClient = mock(() => {})
mock.module("@opencode-ai/sdk", () => ({
createOpencode: mockCreateOpencode,
@@ -34,10 +39,15 @@ mock.module("./opencode-binary-resolver", () => ({
withWorkingOpencodePath: mockWithWorkingOpencodePath,
}))
mock.module("../../shared/opencode-server-auth", () => ({
injectServerAuthIntoClient: mockInjectServerAuthIntoClient,
}))
afterAll(() => {
mock.module("@opencode-ai/sdk", () => originalSdk)
mock.module("../../shared/port-utils", () => originalPortUtils)
mock.module("./opencode-binary-resolver", () => originalBinaryResolver)
mock.module("../../shared/opencode-server-auth", () => originalServerAuth)
mock.restore()
})
@@ -52,6 +62,7 @@ describe("createServerConnection", () => {
mockServerClose.mockClear()
mockConsoleLog.mockClear()
mockWithWorkingOpencodePath.mockClear()
mockInjectServerAuthIntoClient.mockClear()
globalThis.console = { ...console, log: mockConsoleLog } as typeof console
})
@@ -59,6 +70,49 @@ describe("createServerConnection", () => {
globalThis.console = originalConsole
})
it("attach mode injects auth only for loopback URLs", async () => {
// given
const signal = new AbortController().signal
// when
const localhostResult = await createServerConnection({ attach: "http://localhost:8080", signal })
const loopbackResult = await createServerConnection({ attach: "http://127.0.0.1:8080", signal })
const anyBindResult = await createServerConnection({ attach: "http://0.0.0.0:8080", signal })
const remoteResult = await createServerConnection({ attach: "https://example.com", signal })
// then
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: "http://localhost:8080" })
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: "http://127.0.0.1:8080" })
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: "http://0.0.0.0:8080" })
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: "https://example.com" })
expect(mockInjectServerAuthIntoClient).toHaveBeenCalledTimes(3)
expect(mockInjectServerAuthIntoClient).toHaveBeenNthCalledWith(1, localhostResult.client)
expect(mockInjectServerAuthIntoClient).toHaveBeenNthCalledWith(2, loopbackResult.client)
expect(mockInjectServerAuthIntoClient).toHaveBeenNthCalledWith(3, anyBindResult.client)
expect(mockInjectServerAuthIntoClient).not.toHaveBeenCalledWith(remoteResult.client)
expect(mockWithWorkingOpencodePath).not.toHaveBeenCalled()
localhostResult.cleanup()
loopbackResult.cleanup()
anyBindResult.cleanup()
remoteResult.cleanup()
expect(mockServerClose).not.toHaveBeenCalled()
})
it("attach mode skips auth injection for invalid attach URLs", async () => {
// given
const signal = new AbortController().signal
const attachUrl = "not-a-url"
// when
const result = await createServerConnection({ attach: attachUrl, signal })
// then
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: attachUrl })
expect(mockInjectServerAuthIntoClient).not.toHaveBeenCalled()
result.cleanup()
expect(mockServerClose).not.toHaveBeenCalled()
})
it("attach mode returns client with no-op cleanup", async () => {
// given
const signal = new AbortController().signal
@@ -68,8 +122,6 @@ describe("createServerConnection", () => {
const result = await createServerConnection({ attach: attachUrl, signal })
// then
expect(mockCreateOpencodeClient).toHaveBeenCalledWith({ baseUrl: attachUrl })
expect(mockWithWorkingOpencodePath).not.toHaveBeenCalled()
expect(result.client).toBeDefined()
expect(result.cleanup).toBeDefined()
result.cleanup()
+18
View File
@@ -1,9 +1,21 @@
import { createOpencode, createOpencodeClient } from "@opencode-ai/sdk"
import pc from "picocolors"
import type { ServerConnection } from "./types"
import { injectServerAuthIntoClient } from "../../shared/opencode-server-auth"
import { getAvailableServerPort, isPortAvailable, DEFAULT_SERVER_PORT } from "../../shared/port-utils"
import { withWorkingOpencodePath } from "./opencode-binary-resolver"
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0"])
function isLoopbackAttachUrl(url: string): boolean {
try {
const parsed = new URL(url)
return LOOPBACK_HOSTS.has(parsed.hostname)
} catch {
return false
}
}
function isPortStartFailure(error: unknown, port: number): boolean {
if (!(error instanceof Error)) {
return false
@@ -40,6 +52,9 @@ export async function createServerConnection(options: {
if (attach !== undefined) {
console.log(pc.dim("Attaching to existing server at"), pc.cyan(attach))
const client = createOpencodeClient({ baseUrl: attach })
if (isLoopbackAttachUrl(attach)) {
injectServerAuthIntoClient(client)
}
return { client, cleanup: () => {} }
}
@@ -66,12 +81,14 @@ export async function createServerConnection(options: {
console.log(pc.dim("Port"), pc.cyan(port.toString()), pc.dim("became occupied, attaching to existing server"))
const client = createOpencodeClient({ baseUrl: `http://127.0.0.1:${port}` })
injectServerAuthIntoClient(client)
return { client, cleanup: () => {} }
}
}
console.log(pc.dim("Port"), pc.cyan(port.toString()), pc.dim("is occupied, attaching to existing server"))
const client = createOpencodeClient({ baseUrl: `http://127.0.0.1:${port}` })
injectServerAuthIntoClient(client)
return { client, cleanup: () => {} }
}
@@ -93,6 +110,7 @@ export async function createServerConnection(options: {
console.log(pc.dim("Port range exhausted, attaching to existing server on"), pc.cyan(DEFAULT_SERVER_PORT.toString()))
const client = createOpencodeClient({ baseUrl: `http://127.0.0.1:${DEFAULT_SERVER_PORT}` })
injectServerAuthIntoClient(client)
return { client, cleanup: () => {} }
}