mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-10 18:58:48 +00:00
497 lines
14 KiB
TypeScript
497 lines
14 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
|
import { createStore } from "solid-js/store"
|
|
import type { State } from "./types"
|
|
import { applyDirectoryEvent, applyGlobalEvent } from "./event-reducer"
|
|
|
|
const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
|
|
({
|
|
id: input.id,
|
|
parentID: input.parentID,
|
|
time: {
|
|
created: 1,
|
|
updated: 1,
|
|
archived: input.archived,
|
|
},
|
|
}) as Session
|
|
|
|
const userMessage = (id: string, sessionID: string) =>
|
|
({
|
|
id,
|
|
sessionID,
|
|
role: "user",
|
|
time: { created: 1 },
|
|
agent: "assistant",
|
|
model: { providerID: "openai", modelID: "gpt" },
|
|
}) as Message
|
|
|
|
const textPart = (id: string, sessionID: string, messageID: string) =>
|
|
({
|
|
id,
|
|
sessionID,
|
|
messageID,
|
|
type: "text",
|
|
text: id,
|
|
}) as Part
|
|
|
|
const permissionRequest = (id: string, sessionID: string, title = id) =>
|
|
({
|
|
id,
|
|
sessionID,
|
|
permission: title,
|
|
patterns: ["*"],
|
|
metadata: {},
|
|
always: [],
|
|
}) as PermissionRequest
|
|
|
|
const questionRequest = (id: string, sessionID: string, title = id) =>
|
|
({
|
|
id,
|
|
sessionID,
|
|
questions: [
|
|
{
|
|
question: title,
|
|
header: title,
|
|
options: [{ label: title, description: title }],
|
|
},
|
|
],
|
|
}) as QuestionRequest
|
|
|
|
const baseState = (input: Partial<State> = {}) =>
|
|
({
|
|
status: "complete",
|
|
agent: [],
|
|
command: [],
|
|
project: "",
|
|
projectMeta: undefined,
|
|
icon: undefined,
|
|
provider: {} as State["provider"],
|
|
config: {} as State["config"],
|
|
path: { directory: "/tmp" } as State["path"],
|
|
session: [],
|
|
sessionTotal: 0,
|
|
session_status: {},
|
|
session_diff: {},
|
|
todo: {},
|
|
permission: {},
|
|
question: {},
|
|
mcp: {},
|
|
lsp: [],
|
|
vcs: undefined,
|
|
limit: 10,
|
|
message: {},
|
|
part: {},
|
|
...input,
|
|
}) as State
|
|
|
|
describe("applyGlobalEvent", () => {
|
|
test("upserts project.updated in sorted position", () => {
|
|
const project = [{ id: "a" }, { id: "c" }] as Project[]
|
|
let refreshCount = 0
|
|
applyGlobalEvent({
|
|
event: { type: "project.updated", properties: { id: "b" } },
|
|
project,
|
|
refresh: () => {
|
|
refreshCount += 1
|
|
},
|
|
setGlobalProject(next) {
|
|
if (typeof next === "function") next(project)
|
|
},
|
|
})
|
|
|
|
expect(project.map((x) => x.id)).toEqual(["a", "b", "c"])
|
|
expect(refreshCount).toBe(0)
|
|
})
|
|
|
|
test("handles global.disposed by triggering refresh", () => {
|
|
let refreshCount = 0
|
|
applyGlobalEvent({
|
|
event: { type: "global.disposed" },
|
|
project: [],
|
|
refresh: () => {
|
|
refreshCount += 1
|
|
},
|
|
setGlobalProject() {},
|
|
})
|
|
|
|
expect(refreshCount).toBe(1)
|
|
})
|
|
|
|
test("handles server.connected by triggering refresh", () => {
|
|
let refreshCount = 0
|
|
applyGlobalEvent({
|
|
event: { type: "server.connected" },
|
|
project: [],
|
|
refresh: () => {
|
|
refreshCount += 1
|
|
},
|
|
setGlobalProject() {},
|
|
})
|
|
|
|
expect(refreshCount).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe("applyDirectoryEvent", () => {
|
|
test("inserts root sessions in sorted order and updates sessionTotal", () => {
|
|
const [store, setStore] = createStore(
|
|
baseState({
|
|
session: [rootSession({ id: "b" })],
|
|
sessionTotal: 1,
|
|
}),
|
|
)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "session.created", properties: { info: rootSession({ id: "a" }) } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.session.map((x) => x.id)).toEqual(["a", "b"])
|
|
expect(store.sessionTotal).toBe(2)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "session.created", properties: { info: rootSession({ id: "c", parentID: "a" }) } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.sessionTotal).toBe(2)
|
|
})
|
|
|
|
test("cleans session caches when archived", () => {
|
|
const message = userMessage("msg_1", "ses_1")
|
|
const [store, setStore] = createStore(
|
|
baseState({
|
|
session: [rootSession({ id: "ses_1" }), rootSession({ id: "ses_2" })],
|
|
sessionTotal: 2,
|
|
message: { ses_1: [message] },
|
|
part: { [message.id]: [textPart("prt_1", "ses_1", message.id)] },
|
|
session_diff: { ses_1: [] },
|
|
todo: { ses_1: [] },
|
|
permission: { ses_1: [] },
|
|
question: { ses_1: [] },
|
|
session_status: { ses_1: { type: "busy" } },
|
|
}),
|
|
)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "session.updated", properties: { info: rootSession({ id: "ses_1", archived: 10 }) } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.session.map((x) => x.id)).toEqual(["ses_2"])
|
|
expect(store.sessionTotal).toBe(1)
|
|
expect(store.message.ses_1).toBeUndefined()
|
|
expect(store.part[message.id]).toBeUndefined()
|
|
expect(store.session_diff.ses_1).toBeUndefined()
|
|
expect(store.todo.ses_1).toBeUndefined()
|
|
expect(store.permission.ses_1).toBeUndefined()
|
|
expect(store.question.ses_1).toBeUndefined()
|
|
expect(store.session_status.ses_1).toBeUndefined()
|
|
})
|
|
|
|
test("cleans session caches when deleted and decrements only root totals", () => {
|
|
const cases = [
|
|
{ info: rootSession({ id: "ses_1" }), expectedTotal: 1 },
|
|
{ info: rootSession({ id: "ses_2", parentID: "ses_1" }), expectedTotal: 2 },
|
|
]
|
|
|
|
for (const item of cases) {
|
|
const message = userMessage("msg_1", item.info.id)
|
|
const [store, setStore] = createStore(
|
|
baseState({
|
|
session: [
|
|
rootSession({ id: "ses_1" }),
|
|
rootSession({ id: "ses_2", parentID: "ses_1" }),
|
|
rootSession({ id: "ses_3" }),
|
|
],
|
|
sessionTotal: 2,
|
|
message: { [item.info.id]: [message] },
|
|
part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] },
|
|
session_diff: { [item.info.id]: [] },
|
|
todo: { [item.info.id]: [] },
|
|
permission: { [item.info.id]: [] },
|
|
question: { [item.info.id]: [] },
|
|
session_status: { [item.info.id]: { type: "busy" } },
|
|
}),
|
|
)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "session.deleted", properties: { info: item.info } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.session.find((x) => x.id === item.info.id)).toBeUndefined()
|
|
expect(store.sessionTotal).toBe(item.expectedTotal)
|
|
expect(store.message[item.info.id]).toBeUndefined()
|
|
expect(store.part[message.id]).toBeUndefined()
|
|
expect(store.session_diff[item.info.id]).toBeUndefined()
|
|
expect(store.todo[item.info.id]).toBeUndefined()
|
|
expect(store.permission[item.info.id]).toBeUndefined()
|
|
expect(store.question[item.info.id]).toBeUndefined()
|
|
expect(store.session_status[item.info.id]).toBeUndefined()
|
|
}
|
|
})
|
|
|
|
test("upserts and removes messages while clearing orphaned parts", () => {
|
|
const sessionID = "ses_1"
|
|
const [store, setStore] = createStore(
|
|
baseState({
|
|
message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] },
|
|
part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] },
|
|
}),
|
|
)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"])
|
|
|
|
applyDirectoryEvent({
|
|
event: {
|
|
type: "message.updated",
|
|
properties: {
|
|
info: {
|
|
...userMessage("msg_2", sessionID),
|
|
role: "assistant",
|
|
} as Message,
|
|
},
|
|
},
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant")
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"])
|
|
expect(store.part.msg_2).toBeUndefined()
|
|
})
|
|
|
|
test("upserts and prunes message parts", () => {
|
|
const sessionID = "ses_1"
|
|
const messageID = "msg_1"
|
|
const [store, setStore] = createStore(
|
|
baseState({
|
|
part: { [messageID]: [textPart("prt_1", sessionID, messageID), textPart("prt_3", sessionID, messageID)] },
|
|
}),
|
|
)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "message.part.updated", properties: { part: textPart("prt_2", sessionID, messageID) } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.part[messageID]?.map((x) => x.id)).toEqual(["prt_1", "prt_2", "prt_3"])
|
|
|
|
applyDirectoryEvent({
|
|
event: {
|
|
type: "message.part.updated",
|
|
properties: {
|
|
part: {
|
|
...textPart("prt_2", sessionID, messageID),
|
|
text: "changed",
|
|
} as Part,
|
|
},
|
|
},
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
const updated = store.part[messageID]?.find((x) => x.id === "prt_2")
|
|
expect(updated?.type).toBe("text")
|
|
if (updated?.type === "text") expect(updated.text).toBe("changed")
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "message.part.removed", properties: { messageID, partID: "prt_1" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
applyDirectoryEvent({
|
|
event: { type: "message.part.removed", properties: { messageID, partID: "prt_2" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
applyDirectoryEvent({
|
|
event: { type: "message.part.removed", properties: { messageID, partID: "prt_3" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
|
|
expect(store.part[messageID]).toBeUndefined()
|
|
})
|
|
|
|
test("tracks permission and question request lifecycles", () => {
|
|
const sessionID = "ses_1"
|
|
const [store, setStore] = createStore(
|
|
baseState({
|
|
permission: { [sessionID]: [permissionRequest("perm_1", sessionID), permissionRequest("perm_3", sessionID)] },
|
|
question: { [sessionID]: [questionRequest("q_1", sessionID), questionRequest("q_3", sessionID)] },
|
|
}),
|
|
)
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID) },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_2", "perm_3"])
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "permission.asked", properties: permissionRequest("perm_2", sessionID, "updated") },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.permission[sessionID]?.find((x) => x.id === "perm_2")?.permission).toBe("updated")
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "permission.replied", properties: { sessionID, requestID: "perm_2" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"])
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "question.asked", properties: questionRequest("q_2", sessionID) },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"])
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated")
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
})
|
|
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_3"])
|
|
})
|
|
|
|
test("updates vcs branch in store and cache", () => {
|
|
const [store, setStore] = createStore(baseState())
|
|
const [cacheStore, setCacheStore] = createStore({ value: undefined as State["vcs"] })
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } },
|
|
store,
|
|
setStore,
|
|
push() {},
|
|
directory: "/tmp",
|
|
loadLsp() {},
|
|
vcsCache: {
|
|
store: cacheStore,
|
|
setStore: setCacheStore,
|
|
ready: () => true,
|
|
},
|
|
})
|
|
|
|
expect(store.vcs).toEqual({ branch: "feature/test" })
|
|
expect(cacheStore.value).toEqual({ branch: "feature/test" })
|
|
})
|
|
|
|
test("routes disposal and lsp events to side-effect handlers", () => {
|
|
const [store, setStore] = createStore(baseState())
|
|
const pushes: string[] = []
|
|
let lspLoads = 0
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "server.instance.disposed" },
|
|
store,
|
|
setStore,
|
|
push(directory) {
|
|
pushes.push(directory)
|
|
},
|
|
directory: "/tmp",
|
|
loadLsp() {
|
|
lspLoads += 1
|
|
},
|
|
})
|
|
|
|
applyDirectoryEvent({
|
|
event: { type: "lsp.updated" },
|
|
store,
|
|
setStore,
|
|
push(directory) {
|
|
pushes.push(directory)
|
|
},
|
|
directory: "/tmp",
|
|
loadLsp() {
|
|
lspLoads += 1
|
|
},
|
|
})
|
|
|
|
expect(pushes).toEqual(["/tmp"])
|
|
expect(lspLoads).toBe(1)
|
|
})
|
|
})
|