A session created in a git worktree while the client was already running did not render: the message list stayed empty while the prompt and the assistant reply were both present in the session, visible on any fresh load. Reported as prompting in a worktree sometimes not working. Ownership was read from which child store holds the session. That is containment, not ownership. A project's session list includes the sessions of its worktrees so the sidebar can group them, so the parent repository holds worktree sessions too, and whichever store bootstrapped first won. Captured mid-failure, the two signals disagreed outright: owningDirectory /repo <- parent, merely holds it recordDirectory /repo/.worktrees/feature <- the session's own directory The parent won, so every fetch was addressed to a directory that does not own the session, the session id resolved to undefined there, and the requests failed as /api/session/undefined in a retry loop. The session's own record is now believed; store membership remains the fallback for a record that carries no directory. This also explains why the previous commit alone was not enough: settling the guessed directory adopted this same wrong value and then cleared the guess, which prevented any later correction. Verified against the reproduction rather than by reasoning. Before: three of four runs never rendered. After, on a clean build with the instrumentation removed: three of three rendered the reply live, each routed to its own worktree. Tests cover ownership disagreeing with containment, plus both directions of the guess promotion.
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test"
|
|
|
|
import { ChildStoreManager } from "./child-store"
|
|
import { setSyncRefs } from "./sync-refs"
|
|
import { useSessionUIStore } from "./session-ui-store"
|
|
|
|
/**
|
|
* Selecting a session whose directory this client has not indexed yet routes it
|
|
* through the active directory as a deliberate guess. Nothing used to settle
|
|
* that guess once the owning directory finished bootstrapping, so every fetch
|
|
* stayed addressed to a directory that does not own the session and the session
|
|
* never rendered.
|
|
*
|
|
* These tests pin both directions: a guess is promoted once the authoritative
|
|
* directory becomes readable, and a confirmed selection is never rewritten.
|
|
*/
|
|
|
|
const PARENT = "/repo"
|
|
const WORKTREE = "/repo/.worktrees/feature"
|
|
const SESSION_ID = "ses_directory_adoption"
|
|
|
|
const indexSessionIn = (
|
|
manager: ChildStoreManager,
|
|
directory: string,
|
|
recordDirectory: string = directory,
|
|
): void => {
|
|
const store = manager.ensureChild(directory, { bootstrap: false })
|
|
store.setState({
|
|
session: [{ id: SESSION_ID, directory: recordDirectory, title: "test" } as never],
|
|
})
|
|
}
|
|
|
|
let manager: ChildStoreManager
|
|
|
|
beforeEach(() => {
|
|
manager = new ChildStoreManager()
|
|
setSyncRefs({} as never, manager, PARENT)
|
|
useSessionUIStore.getState().setCurrentSession(null)
|
|
})
|
|
|
|
describe("adoptAuthoritativeSessionDirectory", () => {
|
|
test("promotes a guessed selection once the owning directory is indexed", () => {
|
|
useSessionUIStore.getState().setCurrentSession(SESSION_ID)
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).not.toBe(WORKTREE)
|
|
|
|
indexSessionIn(manager, WORKTREE)
|
|
useSessionUIStore.getState().adoptAuthoritativeSessionDirectory()
|
|
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE)
|
|
})
|
|
|
|
test("believes the session record over the store that merely holds it", () => {
|
|
// A project's session list includes the sessions of its worktrees so the
|
|
// sidebar can group them, so the parent store holds this session while the
|
|
// session itself reports the worktree. Ownership comes from the record.
|
|
useSessionUIStore.getState().setCurrentSession(SESSION_ID)
|
|
indexSessionIn(manager, PARENT, WORKTREE)
|
|
|
|
useSessionUIStore.getState().adoptAuthoritativeSessionDirectory()
|
|
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE)
|
|
})
|
|
|
|
test("does nothing while the owning directory is still unknown", () => {
|
|
useSessionUIStore.getState().setCurrentSession(SESSION_ID)
|
|
const before = useSessionUIStore.getState().currentSessionDirectory
|
|
|
|
useSessionUIStore.getState().adoptAuthoritativeSessionDirectory()
|
|
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(before)
|
|
})
|
|
|
|
test("never rewrites a selection that was confirmed at selection time", () => {
|
|
useSessionUIStore.getState().setCurrentSession(SESSION_ID, WORKTREE)
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE)
|
|
|
|
// A different directory claiming the session must not move a confirmed
|
|
// selection: the confirmed value outranks anything sync learns later.
|
|
indexSessionIn(manager, PARENT)
|
|
useSessionUIStore.getState().adoptAuthoritativeSessionDirectory()
|
|
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE)
|
|
})
|
|
|
|
test("is a no-op for a session that is no longer selected", () => {
|
|
useSessionUIStore.getState().setCurrentSession(SESSION_ID)
|
|
indexSessionIn(manager, WORKTREE)
|
|
useSessionUIStore.getState().setCurrentSession("ses_other")
|
|
|
|
// Whatever the new selection resolved to, a late adoption for the previous
|
|
// session must not touch it.
|
|
const before = useSessionUIStore.getState().currentSessionDirectory
|
|
useSessionUIStore.getState().adoptAuthoritativeSessionDirectory(SESSION_ID)
|
|
|
|
expect(useSessionUIStore.getState().currentSessionId).toBe("ses_other")
|
|
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(before)
|
|
})
|
|
})
|