feat(tui): maintain session family index in data context

This commit is contained in:
Dax Raad 2026-07-01 20:22:33 -04:00
commit ce228bfd7c
5 changed files with 183 additions and 21 deletions

View file

@ -915,3 +915,122 @@ test("projects live context updates with their message ID", async () => {
app.renderer.destroy()
}
})
function sessionInfo(id: string, parentID: string | undefined) {
return {
id,
parentID,
projectID: "proj_test",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: id,
location: { directory },
}
}
// Mounts a DataProvider whose `/api/session/:id` responses are driven by the
// given parent map (sessionID -> parentID). Roots omit the entry. Reused across
// the family-index tests below.
async function mountData(parents: Record<string, string>) {
const calls = createFetch((url) => {
const match = url.pathname.match(/^\/api\/session\/([^/]+)$/)
if (match && match[1] !== "active") return json({ data: sessionInfo(match[1], parents[match[1]]) })
})
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
await mounted
return { data, app }
}
test("groups an orphan child under its missing parent until the root arrives", async () => {
const { data, app } = await mountData({ child: "root" })
try {
await data.session.refresh("child")
// Parent info is absent, so the missing parent is the furthest-known ancestor.
expect(data.session.root("child")).toBe("root")
expect(data.session.family("child")).toEqual(["child"])
expect(data.session.family("root")).toEqual(["child"])
await data.session.refresh("root")
expect(data.session.root("root")).toBe("root")
// The tentative root entry folds into the now-known root's family.
expect(data.session.family("child")).toEqual(["child", "root"])
expect(data.session.family("root")).toEqual(["child", "root"])
} finally {
app.renderer.destroy()
}
})
test("indexes arbitrarily deep nesting under a single root", async () => {
const { data, app } = await mountData({ grandchild: "child", child: "root" })
try {
await data.session.refresh("grandchild")
expect(data.session.root("grandchild")).toBe("child")
expect(data.session.family("grandchild")).toEqual(["grandchild"])
await data.session.refresh("child")
// grandchild's tentative family (keyed by the missing "child") merges up
// toward the still-missing "root".
expect(data.session.root("child")).toBe("root")
expect(data.session.family("grandchild")).toEqual(["grandchild", "child"])
await data.session.refresh("root")
expect(data.session.root("grandchild")).toBe("root")
expect(data.session.root("child")).toBe("root")
expect(data.session.family("root")).toEqual(["grandchild", "child", "root"])
} finally {
app.renderer.destroy()
}
})
test("re-registering an existing session is idempotent", async () => {
const { data, app } = await mountData({ grandchild: "child", child: "root" })
try {
await data.session.refresh("grandchild")
await data.session.refresh("child")
await data.session.refresh("root")
const before = data.session.family("root")
expect(before).toEqual(["grandchild", "child", "root"])
await data.session.refresh("child")
await data.session.refresh("root")
await data.session.refresh("grandchild")
expect(data.session.family("root")).toEqual(before)
expect(data.session.family("root")).toHaveLength(3)
} finally {
app.renderer.destroy()
}
})
test("stops at the last non-repeating ancestor on a parent cycle", async () => {
const { data, app } = await mountData({ x: "y", y: "x" })
try {
await data.session.refresh("x")
await data.session.refresh("y")
// Does not hang; walking up from "y" stops before re-entering "x".
expect(data.session.root("y")).toBe("x")
expect(data.session.family("y")).toEqual(["x", "y"])
} finally {
app.renderer.destroy()
}
})