summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-08 05:02:19 -0600
committerGitHub <[email protected]>2026-02-08 05:02:19 -0600
commitd1ebe0767c264d395c8bc504c0957ccc3af90103 (patch)
tree4e10adf10def7a5ec731d1b33a6f21dbe67769f6 /packages/app/src/context
parent19b1222cd85060f7b0999b99586e93f84821b94b (diff)
downloadopencode-d1ebe0767c264d395c8bc504c0957ccc3af90103.tar.gz
opencode-d1ebe0767c264d395c8bc504c0957ccc3af90103.zip
chore: refactoring and tests (#12629)
Diffstat (limited to 'packages/app/src/context')
-rw-r--r--packages/app/src/context/global-sync/event-reducer.test.ts283
-rw-r--r--packages/app/src/context/layout-scroll.test.ts60
2 files changed, 316 insertions, 27 deletions
diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts
index f79b9fc95..ad63f3c20 100644
--- a/packages/app/src/context/global-sync/event-reducer.test.ts
+++ b/packages/app/src/context/global-sync/event-reducer.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
-import type { Message, Part, Project, Session } from "@opencode-ai/sdk/v2/client"
+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"
@@ -34,6 +34,29 @@ const textPart = (id: string, sessionID: string, messageID: string) =>
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",
@@ -164,6 +187,264 @@ describe("applyDirectoryEvent", () => {
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[] = []
diff --git a/packages/app/src/context/layout-scroll.test.ts b/packages/app/src/context/layout-scroll.test.ts
index c421a58b6..2a13e4020 100644
--- a/packages/app/src/context/layout-scroll.test.ts
+++ b/packages/app/src/context/layout-scroll.test.ts
@@ -1,36 +1,44 @@
-import { describe, expect, test } from "bun:test"
+import { describe, expect, test, vi } from "bun:test"
import { createScrollPersistence } from "./layout-scroll"
describe("createScrollPersistence", () => {
- test("debounces persisted scroll writes", async () => {
- const snapshot = {
- session: {
- review: { x: 0, y: 0 },
- },
- } as Record<string, Record<string, { x: number; y: number }>>
- const writes: Array<Record<string, { x: number; y: number }>> = []
- const scroll = createScrollPersistence({
- debounceMs: 10,
- getSnapshot: (sessionKey) => snapshot[sessionKey],
- onFlush: (sessionKey, next) => {
- snapshot[sessionKey] = next
- writes.push(next)
- },
- })
+ test("debounces persisted scroll writes", () => {
+ vi.useFakeTimers()
+ try {
+ const snapshot = {
+ session: {
+ review: { x: 0, y: 0 },
+ },
+ } as Record<string, Record<string, { x: number; y: number }>>
+ const writes: Array<Record<string, { x: number; y: number }>> = []
+ const scroll = createScrollPersistence({
+ debounceMs: 10,
+ getSnapshot: (sessionKey) => snapshot[sessionKey],
+ onFlush: (sessionKey, next) => {
+ snapshot[sessionKey] = next
+ writes.push(next)
+ },
+ })
- for (const i of Array.from({ length: 30 }, (_, n) => n + 1)) {
- scroll.setScroll("session", "review", { x: 0, y: i })
- }
+ for (const i of Array.from({ length: 30 }, (_, n) => n + 1)) {
+ scroll.setScroll("session", "review", { x: 0, y: i })
+ }
+
+ vi.advanceTimersByTime(9)
+ expect(writes).toHaveLength(0)
- await new Promise((resolve) => setTimeout(resolve, 40))
+ vi.advanceTimersByTime(1)
- expect(writes).toHaveLength(1)
- expect(writes[0]?.review).toEqual({ x: 0, y: 30 })
+ expect(writes).toHaveLength(1)
+ expect(writes[0]?.review).toEqual({ x: 0, y: 30 })
- scroll.setScroll("session", "review", { x: 0, y: 30 })
- await new Promise((resolve) => setTimeout(resolve, 20))
+ scroll.setScroll("session", "review", { x: 0, y: 30 })
+ vi.advanceTimersByTime(20)
- expect(writes).toHaveLength(1)
- scroll.dispose()
+ expect(writes).toHaveLength(1)
+ scroll.dispose()
+ } finally {
+ vi.useRealTimers()
+ }
})
})