mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-23 21:04:36 +00:00
fix(core): reconnect editor context for session directory (#24984)
This commit is contained in:
224
packages/opencode/test/cli/tui/editor-context.test.tsx
Normal file
224
packages/opencode/test/cli/tui/editor-context.test.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { afterEach, expect, spyOn, test } from "bun:test"
|
||||
import { createRoot } from "solid-js"
|
||||
import { EditorContextProvider, useEditorContext } from "../../../src/cli/cmd/tui/context/editor"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { FakeWebSocket } from "../../lib/websocket"
|
||||
|
||||
const originalClaudePort = process.env.CLAUDE_CODE_SSE_PORT
|
||||
const originalOpencodePort = process.env.OPENCODE_EDITOR_SSE_PORT
|
||||
|
||||
afterEach(() => {
|
||||
process.env.CLAUDE_CODE_SSE_PORT = originalClaudePort
|
||||
process.env.OPENCODE_EDITOR_SSE_PORT = originalOpencodePort
|
||||
})
|
||||
|
||||
function nextTick() {
|
||||
return new Promise<void>((resolve) => queueMicrotask(resolve))
|
||||
}
|
||||
|
||||
function mountEditorContext(WebSocketImpl?: typeof WebSocket) {
|
||||
let editor!: ReturnType<typeof useEditorContext>
|
||||
let dispose!: () => void
|
||||
|
||||
createRoot((nextDispose) => {
|
||||
dispose = nextDispose
|
||||
|
||||
const Consumer = () => {
|
||||
editor = useEditorContext()
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<EditorContextProvider WebSocketImpl={WebSocketImpl}>
|
||||
<Consumer />
|
||||
</EditorContextProvider>
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
editor,
|
||||
dispose,
|
||||
}
|
||||
}
|
||||
|
||||
function createWebSocketImpl(...sockets: FakeWebSocket[]) {
|
||||
let index = 0
|
||||
|
||||
return class {
|
||||
constructor(url: string, options?: { headers?: Record<string, string> }) {
|
||||
const socket = sockets[index]
|
||||
index += 1
|
||||
expect(socket).toBeDefined()
|
||||
expect(url).toBe(socket!.url)
|
||||
expect(options).toEqual(socket!.options)
|
||||
return socket as unknown as object
|
||||
}
|
||||
} as unknown as typeof WebSocket
|
||||
}
|
||||
|
||||
test("useEditorContext reconnect switches editor server by session directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const startupDirectory = path.join(tmp.path, "startup")
|
||||
const sessionDirectory = path.join(tmp.path, "session")
|
||||
const ideDirectory = path.join(tmp.path, ".claude", "ide")
|
||||
await mkdir(startupDirectory, { recursive: true })
|
||||
await mkdir(sessionDirectory, { recursive: true })
|
||||
await mkdir(ideDirectory, { recursive: true })
|
||||
await writeFile(
|
||||
path.join(ideDirectory, "3001.lock"),
|
||||
JSON.stringify({
|
||||
transport: "ws",
|
||||
workspaceFolders: [startupDirectory],
|
||||
}),
|
||||
)
|
||||
await writeFile(
|
||||
path.join(ideDirectory, "3002.lock"),
|
||||
JSON.stringify({
|
||||
transport: "ws",
|
||||
workspaceFolders: [sessionDirectory],
|
||||
}),
|
||||
)
|
||||
|
||||
process.env.CLAUDE_CODE_SSE_PORT = undefined
|
||||
process.env.OPENCODE_EDITOR_SSE_PORT = undefined
|
||||
spyOn(process, "cwd").mockImplementation(() => startupDirectory)
|
||||
spyOn(os, "homedir").mockImplementation(() => tmp.path)
|
||||
const firstSocket = new FakeWebSocket("ws://127.0.0.1:3001")
|
||||
const secondSocket = new FakeWebSocket("ws://127.0.0.1:3002")
|
||||
|
||||
const mounted = mountEditorContext(createWebSocketImpl(firstSocket, secondSocket))
|
||||
await nextTick()
|
||||
|
||||
expect(firstSocket.closed).toBeFalse()
|
||||
|
||||
mounted.editor.reconnect(sessionDirectory)
|
||||
await nextTick()
|
||||
|
||||
expect(firstSocket.closed).toBeTrue()
|
||||
expect(secondSocket.closed).toBeFalse()
|
||||
|
||||
mounted.dispose()
|
||||
})
|
||||
|
||||
test("useEditorContext favors configured port over lock files", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const startupDirectory = path.join(tmp.path, "startup")
|
||||
const ideDirectory = path.join(tmp.path, ".claude", "ide")
|
||||
await mkdir(startupDirectory, { recursive: true })
|
||||
await mkdir(ideDirectory, { recursive: true })
|
||||
await writeFile(
|
||||
path.join(ideDirectory, "3001.lock"),
|
||||
JSON.stringify({
|
||||
transport: "ws",
|
||||
workspaceFolders: [startupDirectory],
|
||||
}),
|
||||
)
|
||||
|
||||
process.env.CLAUDE_CODE_SSE_PORT = "4010"
|
||||
process.env.OPENCODE_EDITOR_SSE_PORT = undefined
|
||||
spyOn(process, "cwd").mockImplementation(() => startupDirectory)
|
||||
spyOn(os, "homedir").mockImplementation(() => tmp.path)
|
||||
const socket = new FakeWebSocket("ws://127.0.0.1:4010")
|
||||
|
||||
const mounted = mountEditorContext(createWebSocketImpl(socket))
|
||||
await nextTick()
|
||||
|
||||
expect(socket.closed).toBeFalse()
|
||||
|
||||
mounted.dispose()
|
||||
})
|
||||
|
||||
test("useEditorContext resets selection when reconnecting", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const startupDirectory = path.join(tmp.path, "startup")
|
||||
const ideDirectory = path.join(tmp.path, ".claude", "ide")
|
||||
await mkdir(startupDirectory, { recursive: true })
|
||||
await mkdir(ideDirectory, { recursive: true })
|
||||
await writeFile(
|
||||
path.join(ideDirectory, "3001.lock"),
|
||||
JSON.stringify({
|
||||
transport: "ws",
|
||||
workspaceFolders: [startupDirectory],
|
||||
}),
|
||||
)
|
||||
|
||||
process.env.CLAUDE_CODE_SSE_PORT = undefined
|
||||
process.env.OPENCODE_EDITOR_SSE_PORT = undefined
|
||||
spyOn(process, "cwd").mockImplementation(() => startupDirectory)
|
||||
spyOn(os, "homedir").mockImplementation(() => tmp.path)
|
||||
const socket = new FakeWebSocket("ws://127.0.0.1:3001")
|
||||
|
||||
const mounted = mountEditorContext(createWebSocketImpl(socket))
|
||||
await nextTick()
|
||||
|
||||
expect(socket.closed).toBeFalse()
|
||||
expect(mounted.editor.selection()).toBeUndefined()
|
||||
expect(mounted.editor.connected()).toBeFalse()
|
||||
|
||||
socket.open()
|
||||
socket.message(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
result: {
|
||||
protocolVersion: "2025-11-25",
|
||||
serverInfo: { name: "test", version: "0.0.0" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
socket.message(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "selection_changed",
|
||||
params: {
|
||||
text: "foo",
|
||||
filePath: path.join(startupDirectory, "file.ts"),
|
||||
selection: {
|
||||
start: { line: 1, character: 1 },
|
||||
end: { line: 1, character: 4 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mounted.editor.connected()).toBeTrue()
|
||||
expect(mounted.editor.server()).toEqual({
|
||||
protocolVersion: "2025-11-25",
|
||||
serverInfo: { name: "test", version: "0.0.0" },
|
||||
})
|
||||
expect(mounted.editor.selection()).toEqual({
|
||||
text: "foo",
|
||||
filePath: path.join(startupDirectory, "file.ts"),
|
||||
source: "websocket",
|
||||
selection: {
|
||||
start: { line: 1, character: 1 },
|
||||
end: { line: 1, character: 4 },
|
||||
},
|
||||
})
|
||||
|
||||
mounted.editor.reconnect(startupDirectory)
|
||||
|
||||
expect(socket.closed).toBeFalse()
|
||||
expect(mounted.editor.connected()).toBeTrue()
|
||||
expect(mounted.editor.selection()).toBeUndefined()
|
||||
|
||||
mounted.dispose()
|
||||
})
|
||||
|
||||
test("useEditorContext connects with OPENCODE_EDITOR_SSE_PORT", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
process.env.CLAUDE_CODE_SSE_PORT = undefined
|
||||
process.env.OPENCODE_EDITOR_SSE_PORT = "4020"
|
||||
spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const socket = new FakeWebSocket("ws://127.0.0.1:4020")
|
||||
|
||||
const mounted = mountEditorContext(createWebSocketImpl(socket))
|
||||
await nextTick()
|
||||
|
||||
expect(socket.closed).toBeFalse()
|
||||
|
||||
mounted.dispose()
|
||||
})
|
||||
Reference in New Issue
Block a user