Files
openchamber/packages/ui/src/sync/__tests__/session-switch-resync.test.ts
T

160 lines
5.8 KiB
TypeScript
Raw Normal View History

import { describe, expect, test, beforeEach, mock } from "bun:test"
import { create, type StoreApi } from "zustand"
import type { PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2/client"
const listPendingQuestionsCalls: Array<{ directories?: Array<string | null | undefined> }> = []
const listPendingPermissionsCalls: Array<{ directories?: Array<string | null | undefined> }> = []
let pendingQuestionsResponse: QuestionRequest[] = []
let pendingPermissionsResponse: PermissionRequest[] = []
mock.module("@/lib/opencode/client", () => ({
opencodeClient: {
listPendingQuestions: mock(async (opts?: { directories?: Array<string | null | undefined> }) => {
listPendingQuestionsCalls.push(opts ?? {})
return pendingQuestionsResponse
}),
listPendingPermissions: mock(async (opts?: { directories?: Array<string | null | undefined> }) => {
listPendingPermissionsCalls.push(opts ?? {})
return pendingPermissionsResponse
}),
getDirectory: () => "/repo",
getScopedSdkClient: () => ({}),
setDirectory: () => undefined,
},
}))
mock.module("@/stores/permissionStore", () => ({
usePermissionStore: {
getState: () => ({ isSessionAutoAccepting: () => false }),
},
}))
mock.module("@/stores/useConfigStore", () => ({
useConfigStore: {
getState: () => ({ isConnected: true, hasEverConnected: true }),
setState: () => undefined,
},
}))
mock.module("@/stores/useTodosPersistStore", () => ({
useTodosPersistStore: { getState: () => ({}) },
}))
mock.module("@/components/ui", () => ({
toast: { info: () => undefined, error: () => undefined, success: () => undefined },
}))
import { INITIAL_STATE, type State } from "../types"
import type { DirectoryStore } from "../child-store"
import { resyncBlockingRequestsForDirectory } from "../sync-context"
function buildQuestion(overrides: Partial<QuestionRequest> = {}): QuestionRequest {
return {
id: "que_1",
sessionID: "ses_a",
questions: [{ question: "Continue?", header: "Q", options: [{ label: "Yes", description: "" }] }],
...overrides,
} as QuestionRequest
}
function buildPermission(overrides: Partial<PermissionRequest> = {}): PermissionRequest {
return {
id: "perm_1",
sessionID: "ses_a",
permission: "bash",
patterns: [],
metadata: {},
always: [],
...overrides,
} as PermissionRequest
}
function createDirectoryStore(initial: Partial<State>): StoreApi<DirectoryStore> {
return create<DirectoryStore>()((set) => ({
...INITIAL_STATE,
...initial,
session: initial.session ?? [{ id: "ses_a", title: "ses_a", time: { created: 1, updated: 1 }, version: "1" } as State["session"][number]],
patch: (partial) => set(partial),
replace: (next) => set(next),
}))
}
describe("resyncBlockingRequestsForDirectory", () => {
beforeEach(() => {
listPendingQuestionsCalls.length = 0
listPendingPermissionsCalls.length = 0
pendingQuestionsResponse = []
pendingPermissionsResponse = []
})
test("calls listPendingQuestions and listPendingPermissions exactly once for the directory", async () => {
const store = createDirectoryStore({})
pendingQuestionsResponse = [buildQuestion()]
pendingPermissionsResponse = [buildPermission()]
await resyncBlockingRequestsForDirectory("/repo", store)
expect(listPendingQuestionsCalls).toHaveLength(1)
expect(listPendingQuestionsCalls[0]).toEqual({ directories: ["/repo"] })
expect(listPendingPermissionsCalls).toHaveLength(1)
expect(listPendingPermissionsCalls[0]).toEqual({ directories: ["/repo"] })
})
test("merges newly fetched questions/permissions into the directory store", async () => {
const store = createDirectoryStore({})
pendingQuestionsResponse = [buildQuestion()]
pendingPermissionsResponse = [buildPermission()]
await resyncBlockingRequestsForDirectory("/repo", store)
expect(store.getState().question["ses_a"]).toHaveLength(1)
expect(store.getState().question["ses_a"]?.[0]?.id).toBe("que_1")
expect(store.getState().permission["ses_a"]).toHaveLength(1)
expect(store.getState().permission["ses_a"]?.[0]?.id).toBe("perm_1")
})
test("preserves an in-flight SSE-delivered question whose signature changed during the fetch", async () => {
const store = createDirectoryStore({
question: { ses_a: [{ ...buildQuestion(), id: "que_initial" }] },
})
pendingQuestionsResponse = []
const promise = resyncBlockingRequestsForDirectory("/repo", store)
store.setState({
question: { ses_a: [{ ...buildQuestion(), id: "que_sse_arrived" }] },
})
await promise
expect(store.getState().question["ses_a"]).toHaveLength(1)
expect(store.getState().question["ses_a"]?.[0]?.id).toBe("que_sse_arrived")
})
test("clears stale entries when API returns no pending requests and signature unchanged", async () => {
const store = createDirectoryStore({
question: { ses_a: [{ ...buildQuestion(), id: "que_stale" }] },
})
pendingQuestionsResponse = []
pendingPermissionsResponse = []
await resyncBlockingRequestsForDirectory("/repo", store)
expect(store.getState().question["ses_a"]).toEqual(undefined)
})
test("ignores questions for sessions the directory does not know about", async () => {
const store = createDirectoryStore({})
pendingQuestionsResponse = [{ ...buildQuestion(), sessionID: "ses_unknown" }]
await resyncBlockingRequestsForDirectory("/repo", store)
expect(store.getState().question["ses_unknown"]).toEqual(undefined)
})
test("returns early without fetching when no candidate sessions are known", async () => {
const store = createDirectoryStore({ session: [] })
await resyncBlockingRequestsForDirectory("/repo", store)
expect(listPendingQuestionsCalls).toHaveLength(0)
expect(listPendingPermissionsCalls).toHaveLength(0)
})
})