import fs from "node:fs";import os from "node:os";import path from "node:path";import { EventEmitter } from "node:events"; import { describe, expect, it, afterEach, vi } from "vitest";import { createServer } from "./wsServer";import WebSocket from "ws"; import { DEFAULT_TERMINAL_ID, WS_CHANNELS, WS_METHODS, type KeybindingsConfig, type ResolvedKeybindingsConfig, type WsPush, type WsResponse,} from "@t3tools/contracts";import { ProjectRegistry } from "./projectRegistry";import { compileResolvedKeybindingRule, DEFAULT_KEYBINDINGS } from "./keybindings";import type { TerminalClearInput, TerminalCloseInput, TerminalEvent, TerminalOpenInput, TerminalResizeInput, TerminalSessionSnapshot, TerminalWriteInput,} from "@t3tools/contracts";import type { TerminalManager } from "./terminalManager"; interface PendingMessages { queue: unknown[]; waiters: Array<(message: unknown) => void>;} const pendingBySocket = new WeakMap<WebSocket, PendingMessages>(); class MockTerminalManager extends EventEmitter<{ event: [event: TerminalEvent] }> { private readonly sessions = new Map<string, TerminalSessionSnapshot>(); private key(threadId: string, terminalId: string): string { return `${threadId}\u0000${terminalId}`; } async open(input: TerminalOpenInput): Promise<TerminalSessionSnapshot> { const now = new Date().toISOString(); const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; const snapshot: TerminalSessionSnapshot = { threadId: input.threadId, terminalId, cwd: input.cwd, status: "running", pid: 4242, history: "", exitCode: null, exitSignal: null, updatedAt: now, }; this.sessions.set(this.key(input.threadId, terminalId), snapshot); queueMicrotask(() => { this.emit("event", { type: "started", threadId: input.threadId, terminalId, createdAt: now, snapshot, }); }); return snapshot; } async write(input: TerminalWriteInput): Promise<void> { const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; const existing = this.sessions.get(this.key(input.threadId, terminalId)); if (!existing) { throw new Error(`Unknown terminal thread: ${input.threadId}`); } queueMicrotask(() => { this.emit("event", { type: "output", threadId: input.threadId, terminalId, createdAt: new Date().toISOString(), data: input.data, }); }); } async resize(_input: TerminalResizeInput): Promise<void> {} async clear(input: TerminalClearInput): Promise<void> { const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; queueMicrotask(() => { this.emit("event", { type: "cleared", threadId: input.threadId, terminalId, createdAt: new Date().toISOString(), }); }); } async restart(input: TerminalOpenInput): Promise<TerminalSessionSnapshot> { const now = new Date().toISOString(); const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; const snapshot: TerminalSessionSnapshot = { threadId: input.threadId, terminalId, cwd: input.cwd, status: "running", pid: 5252, history: "", exitCode: null, exitSignal: null, updatedAt: now, }; this.sessions.set(this.key(input.threadId, terminalId), snapshot); queueMicrotask(() => { this.emit("event", { type: "restarted", threadId: input.threadId, terminalId, createdAt: now, snapshot, }); }); return snapshot; } async close(input: TerminalCloseInput): Promise<void> { if (input.terminalId) { this.sessions.delete(this.key(input.threadId, input.terminalId)); return; } for (const key of [...this.sessions.keys()]) { if (key.startsWith(`${input.threadId}\u0000`)) { this.sessions.delete(key); } } } dispose(): void {}} function connectWs(port: number, token?: string): Promise<WebSocket> { return new Promise((resolve, reject) => { const query = token ? `?token=${encodeURIComponent(token)}` : ""; const ws = new WebSocket(`ws://127.0.0.1:${port}/${query}`); const pending: PendingMessages = { queue: [], waiters: [] }; pendingBySocket.set(ws, pending); ws.on("message", (raw) => { const parsed = JSON.parse(String(raw)); const waiter = pending.waiters.shift(); if (waiter) { waiter(parsed); return; } pending.queue.push(parsed); }); ws.once("open", () => resolve(ws)); ws.once("error", () => reject(new Error("WebSocket connection failed"))); });} function waitForMessage(ws: WebSocket): Promise<unknown> { const pending = pendingBySocket.get(ws); if (!pending) { return Promise.reject(new Error("WebSocket not initialized")); } const queued = pending.queue.shift(); if (queued !== undefined) { return Promise.resolve(queued); } return new Promise((resolve) => { pending.waiters.push(resolve); });} async function sendRequest(ws: WebSocket, method: string, params?: unknown): Promise<WsResponse> { const id = crypto.randomUUID(); const message = JSON.stringify({ id, method, ...(params !== undefined ? { params } : {}) }); ws.send(message); // Wait for response with matching id while (true) { const parsed = (await waitForMessage(ws)) as Record<string, unknown>; if (parsed.id === id) { return parsed as WsResponse; } }} function compileKeybindings(bindings: KeybindingsConfig): ResolvedKeybindingsConfig { const resolved: ResolvedKeybindingsConfig = []; for (const binding of bindings) { const compiled = compileResolvedKeybindingRule(binding); if (!compiled) { throw new Error(`Unexpected invalid keybinding in test setup: ${binding.command}`); } resolved.push(compiled); } return resolved;} const DEFAULT_RESOLVED_KEYBINDINGS = compileKeybindings([...DEFAULT_KEYBINDINGS]); function mergeWithDefaultsForTest(custom: KeybindingsConfig): ResolvedKeybindingsConfig { if (custom.length === 0) { return DEFAULT_RESOLVED_KEYBINDINGS; } const overriddenCommands = new Set(custom.map((binding) => binding.command)); const retainedDefaults = DEFAULT_KEYBINDINGS.filter( (binding) => !overriddenCommands.has(binding.command), ); return compileKeybindings([...retainedDefaults, ...custom].slice(-256));} describe("WebSocket Server", () => { let server: ReturnType<typeof createServer> | null = null; const connections: WebSocket[] = []; const tempDirs: string[] = []; function makeTempDir(prefix: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.push(dir); return dir; } function createTestServer( options: { cwd?: string; devUrl?: string; authToken?: string; stateDir?: string; gitManager?: { status: (input: { cwd: string }) => Promise<unknown>; runStackedAction: (input: { cwd: string; action: string }) => Promise<unknown>; }; terminalManager?: TerminalManager; } = {}, ): ReturnType<typeof createServer> { const stateDir = options.stateDir ?? makeTempDir("t3code-ws-state-"); return createServer({ port: 0, cwd: options.cwd ?? "/test/project", ...(options.devUrl ? { devUrl: options.devUrl } : {}), ...(options.authToken ? { authToken: options.authToken } : {}), projectRegistry: new ProjectRegistry(stateDir), ...(options.gitManager ? { gitManager: options.gitManager as never } : {}), ...(options.terminalManager ? { terminalManager: options.terminalManager } : {}), }); } afterEach(async () => { for (const ws of connections) { ws.close(); } connections.length = 0; if (server) { await server.stop(); } server = null; for (const dir of tempDirs.splice(0, tempDirs.length)) { fs.rmSync(dir, { recursive: true, force: true }); } vi.restoreAllMocks(); }); it("sends welcome message on connect", async () => { server = createTestServer({ cwd: "/test/project" }); // Get the actual port after listen await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; expect(port).toBeGreaterThan(0); const ws = await connectWs(port); connections.push(ws); const message = (await waitForMessage(ws)) as WsPush; expect(message.type).toBe("push"); expect(message.channel).toBe(WS_CHANNELS.serverWelcome); expect(message.data).toEqual({ cwd: "/test/project", projectName: "project", }); }); it("logs outbound websocket push events in dev mode", async () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => { // Keep test output clean while verifying websocket logs. }); server = createTestServer({ cwd: "/test/project", devUrl: "http://localhost:5173", }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; expect(port).toBeGreaterThan(0); const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); expect( logSpy.mock.calls.some(([message]) => { if (typeof message !== "string") return false; return ( message.includes("[ws]") && message.includes("outgoing push") && message.includes(`channel="${WS_CHANNELS.serverWelcome}"`) ); }), ).toBe(true); }); it("responds to server.getConfig", async () => { const fakeHome = makeTempDir("t3code-home-"); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); // Consume welcome message await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: DEFAULT_RESOLVED_KEYBINDINGS, }); }); it("reads keybindings from ~/.t3/keybindings.json", async () => { const fakeHome = makeTempDir("t3code-home-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( path.join(configDir, "keybindings.json"), JSON.stringify([ { key: "cmd+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, ]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: mergeWithDefaultsForTest([ { key: "cmd+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, ]), }); }); it("warns and ignores invalid keybinding entries", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const fakeHome = makeTempDir("t3code-home-invalid-entry-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( path.join(configDir, "keybindings.json"), JSON.stringify([ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+z", command: "invalid.command", when: "terminalFocus" }, ]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: mergeWithDefaultsForTest([{ key: "mod+j", command: "terminal.toggle" }]), }); expect( warnSpy.mock.calls.some(([message]) => String(message).includes("ignoring invalid keybinding entries"), ), ).toBe(true); }); it("warns and ignores keybindings with malformed when expressions", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const fakeHome = makeTempDir("t3code-home-invalid-when-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( path.join(configDir, "keybindings.json"), JSON.stringify([{ key: "mod+j", command: "terminal.toggle", when: "terminalFocus && (" }]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: DEFAULT_RESOLVED_KEYBINDINGS, }); expect( warnSpy.mock.calls.some(([message]) => String(message).includes("ignoring invalid keybinding entries"), ), ).toBe(true); }); it("reads keybindings once at startup and caches the resolved config", async () => { const fakeHome = makeTempDir("t3code-home-cached-config-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const keybindingsPath = path.join(configDir, "keybindings.json"); fs.writeFileSync( keybindingsPath, JSON.stringify([{ key: "cmd+j", command: "terminal.toggle" }]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const firstResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(firstResponse.error).toBeUndefined(); expect(firstResponse.result).toEqual({ cwd: "/my/workspace", keybindings: mergeWithDefaultsForTest([{ key: "cmd+j", command: "terminal.toggle" }]), }); fs.writeFileSync( keybindingsPath, JSON.stringify([{ key: "cmd+k", command: "terminal.toggle" }]), "utf8", ); const secondResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(secondResponse.error).toBeUndefined(); expect(secondResponse.result).toEqual(firstResponse.result); }); it("upserts keybinding rules and updates cached server config", async () => { const fakeHome = makeTempDir("t3code-home-upsert-keybinding-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const keybindingsPath = path.join(configDir, "keybindings.json");Lines 1–500 of 1060. Load more
import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { EventEmitter } from "node:events"; import { describe, expect, it, afterEach, vi } from "vitest"; import { createServer } from "./wsServer"; import WebSocket from "ws"; import { DEFAULT_TERMINAL_ID, WS_CHANNELS, WS_METHODS, type KeybindingsConfig, type ResolvedKeybindingsConfig, type WsPush, type WsResponse, } from "@t3tools/contracts"; import { ProjectRegistry } from "./projectRegistry"; import { compileResolvedKeybindingRule, DEFAULT_KEYBINDINGS } from "./keybindings"; import type { TerminalClearInput, TerminalCloseInput, TerminalEvent, TerminalOpenInput, TerminalResizeInput, TerminalSessionSnapshot, TerminalWriteInput, } from "@t3tools/contracts"; import type { TerminalManager } from "./terminalManager"; interface PendingMessages { queue: unknown[]; waiters: Array<(message: unknown) => void>; } const pendingBySocket = new WeakMap<WebSocket, PendingMessages>(); class MockTerminalManager extends EventEmitter<{ event: [event: TerminalEvent] }> { private readonly sessions = new Map<string, TerminalSessionSnapshot>(); private key(threadId: string, terminalId: string): string { return `${threadId}\u0000${terminalId}`; } async open(input: TerminalOpenInput): Promise<TerminalSessionSnapshot> { const now = new Date().toISOString(); const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; const snapshot: TerminalSessionSnapshot = { threadId: input.threadId, terminalId, cwd: input.cwd, status: "running", pid: 4242, history: "", exitCode: null, exitSignal: null, updatedAt: now, }; this.sessions.set(this.key(input.threadId, terminalId), snapshot); queueMicrotask(() => { this.emit("event", { type: "started", threadId: input.threadId, terminalId, createdAt: now, snapshot, }); }); return snapshot; } async write(input: TerminalWriteInput): Promise<void> { const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; const existing = this.sessions.get(this.key(input.threadId, terminalId)); if (!existing) { throw new Error(`Unknown terminal thread: ${input.threadId}`); } queueMicrotask(() => { this.emit("event", { type: "output", threadId: input.threadId, terminalId, createdAt: new Date().toISOString(), data: input.data, }); }); } async resize(_input: TerminalResizeInput): Promise<void> {} async clear(input: TerminalClearInput): Promise<void> { const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; queueMicrotask(() => { this.emit("event", { type: "cleared", threadId: input.threadId, terminalId, createdAt: new Date().toISOString(), }); }); } async restart(input: TerminalOpenInput): Promise<TerminalSessionSnapshot> { const now = new Date().toISOString(); const terminalId = input.terminalId ?? DEFAULT_TERMINAL_ID; const snapshot: TerminalSessionSnapshot = { threadId: input.threadId, terminalId, cwd: input.cwd, status: "running", pid: 5252, history: "", exitCode: null, exitSignal: null, updatedAt: now, }; this.sessions.set(this.key(input.threadId, terminalId), snapshot); queueMicrotask(() => { this.emit("event", { type: "restarted", threadId: input.threadId, terminalId, createdAt: now, snapshot, }); }); return snapshot; } async close(input: TerminalCloseInput): Promise<void> { if (input.terminalId) { this.sessions.delete(this.key(input.threadId, input.terminalId)); return; } for (const key of [...this.sessions.keys()]) { if (key.startsWith(`${input.threadId}\u0000`)) { this.sessions.delete(key); } } } dispose(): void {} } function connectWs(port: number, token?: string): Promise<WebSocket> { return new Promise((resolve, reject) => { const query = token ? `?token=${encodeURIComponent(token)}` : ""; const ws = new WebSocket(`ws://127.0.0.1:${port}/${query}`); const pending: PendingMessages = { queue: [], waiters: [] }; pendingBySocket.set(ws, pending); ws.on("message", (raw) => { const parsed = JSON.parse(String(raw)); const waiter = pending.waiters.shift(); if (waiter) { waiter(parsed); return; } pending.queue.push(parsed); }); ws.once("open", () => resolve(ws)); ws.once("error", () => reject(new Error("WebSocket connection failed"))); }); } function waitForMessage(ws: WebSocket): Promise<unknown> { const pending = pendingBySocket.get(ws); if (!pending) { return Promise.reject(new Error("WebSocket not initialized")); } const queued = pending.queue.shift(); if (queued !== undefined) { return Promise.resolve(queued); } return new Promise((resolve) => { pending.waiters.push(resolve); }); } async function sendRequest(ws: WebSocket, method: string, params?: unknown): Promise<WsResponse> { const id = crypto.randomUUID(); const message = JSON.stringify({ id, method, ...(params !== undefined ? { params } : {}) }); ws.send(message); // Wait for response with matching id while (true) { const parsed = (await waitForMessage(ws)) as Record<string, unknown>; if (parsed.id === id) { return parsed as WsResponse; } } } function compileKeybindings(bindings: KeybindingsConfig): ResolvedKeybindingsConfig { const resolved: ResolvedKeybindingsConfig = []; for (const binding of bindings) { const compiled = compileResolvedKeybindingRule(binding); if (!compiled) { throw new Error(`Unexpected invalid keybinding in test setup: ${binding.command}`); } resolved.push(compiled); } return resolved; } const DEFAULT_RESOLVED_KEYBINDINGS = compileKeybindings([...DEFAULT_KEYBINDINGS]); function mergeWithDefaultsForTest(custom: KeybindingsConfig): ResolvedKeybindingsConfig { if (custom.length === 0) { return DEFAULT_RESOLVED_KEYBINDINGS; } const overriddenCommands = new Set(custom.map((binding) => binding.command)); const retainedDefaults = DEFAULT_KEYBINDINGS.filter( (binding) => !overriddenCommands.has(binding.command), ); return compileKeybindings([...retainedDefaults, ...custom].slice(-256)); } describe("WebSocket Server", () => { let server: ReturnType<typeof createServer> | null = null; const connections: WebSocket[] = []; const tempDirs: string[] = []; function makeTempDir(prefix: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.push(dir); return dir; } function createTestServer( options: { cwd?: string; devUrl?: string; authToken?: string; stateDir?: string; gitManager?: { status: (input: { cwd: string }) => Promise<unknown>; runStackedAction: (input: { cwd: string; action: string }) => Promise<unknown>; }; terminalManager?: TerminalManager; } = {}, ): ReturnType<typeof createServer> { const stateDir = options.stateDir ?? makeTempDir("t3code-ws-state-"); return createServer({ port: 0, cwd: options.cwd ?? "/test/project", ...(options.devUrl ? { devUrl: options.devUrl } : {}), ...(options.authToken ? { authToken: options.authToken } : {}), projectRegistry: new ProjectRegistry(stateDir), ...(options.gitManager ? { gitManager: options.gitManager as never } : {}), ...(options.terminalManager ? { terminalManager: options.terminalManager } : {}), }); } afterEach(async () => { for (const ws of connections) { ws.close(); } connections.length = 0; if (server) { await server.stop(); } server = null; for (const dir of tempDirs.splice(0, tempDirs.length)) { fs.rmSync(dir, { recursive: true, force: true }); } vi.restoreAllMocks(); }); it("sends welcome message on connect", async () => { server = createTestServer({ cwd: "/test/project" }); // Get the actual port after listen await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; expect(port).toBeGreaterThan(0); const ws = await connectWs(port); connections.push(ws); const message = (await waitForMessage(ws)) as WsPush; expect(message.type).toBe("push"); expect(message.channel).toBe(WS_CHANNELS.serverWelcome); expect(message.data).toEqual({ cwd: "/test/project", projectName: "project", }); }); it("logs outbound websocket push events in dev mode", async () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => { // Keep test output clean while verifying websocket logs. }); server = createTestServer({ cwd: "/test/project", devUrl: "http://localhost:5173", }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; expect(port).toBeGreaterThan(0); const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); expect( logSpy.mock.calls.some(([message]) => { if (typeof message !== "string") return false; return ( message.includes("[ws]") && message.includes("outgoing push") && message.includes(`channel="${WS_CHANNELS.serverWelcome}"`) ); }), ).toBe(true); }); it("responds to server.getConfig", async () => { const fakeHome = makeTempDir("t3code-home-"); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); // Consume welcome message await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: DEFAULT_RESOLVED_KEYBINDINGS, }); }); it("reads keybindings from ~/.t3/keybindings.json", async () => { const fakeHome = makeTempDir("t3code-home-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( path.join(configDir, "keybindings.json"), JSON.stringify([ { key: "cmd+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, ]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: mergeWithDefaultsForTest([ { key: "cmd+j", command: "terminal.toggle" }, { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, { key: "mod+n", command: "terminal.new", when: "terminalFocus" }, ]), }); }); it("warns and ignores invalid keybinding entries", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const fakeHome = makeTempDir("t3code-home-invalid-entry-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( path.join(configDir, "keybindings.json"), JSON.stringify([ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+z", command: "invalid.command", when: "terminalFocus" }, ]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: mergeWithDefaultsForTest([{ key: "mod+j", command: "terminal.toggle" }]), }); expect( warnSpy.mock.calls.some(([message]) => String(message).includes("ignoring invalid keybinding entries"), ), ).toBe(true); }); it("warns and ignores keybindings with malformed when expressions", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const fakeHome = makeTempDir("t3code-home-invalid-when-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync( path.join(configDir, "keybindings.json"), JSON.stringify([{ key: "mod+j", command: "terminal.toggle", when: "terminalFocus && (" }]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const response = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(response.error).toBeUndefined(); expect(response.result).toEqual({ cwd: "/my/workspace", keybindings: DEFAULT_RESOLVED_KEYBINDINGS, }); expect( warnSpy.mock.calls.some(([message]) => String(message).includes("ignoring invalid keybinding entries"), ), ).toBe(true); }); it("reads keybindings once at startup and caches the resolved config", async () => { const fakeHome = makeTempDir("t3code-home-cached-config-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const keybindingsPath = path.join(configDir, "keybindings.json"); fs.writeFileSync( keybindingsPath, JSON.stringify([{ key: "cmd+j", command: "terminal.toggle" }]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); server = createTestServer({ cwd: "/my/workspace" }); await server.start(); const addr = server.httpServer.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; const ws = await connectWs(port); connections.push(ws); await waitForMessage(ws); const firstResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(firstResponse.error).toBeUndefined(); expect(firstResponse.result).toEqual({ cwd: "/my/workspace", keybindings: mergeWithDefaultsForTest([{ key: "cmd+j", command: "terminal.toggle" }]), }); fs.writeFileSync( keybindingsPath, JSON.stringify([{ key: "cmd+k", command: "terminal.toggle" }]), "utf8", ); const secondResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); expect(secondResponse.error).toBeUndefined(); expect(secondResponse.result).toEqual(firstResponse.result); }); it("upserts keybinding rules and updates cached server config", async () => { const fakeHome = makeTempDir("t3code-home-upsert-keybinding-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const keybindingsPath = path.join(configDir, "keybindings.json");