fix(sync): settle a guessed session directory once its owner is known
Selecting a session whose directory this client has not indexed yet routes it through the active directory. That is a deliberate, documented guess: it keeps routing usable while the owning store bootstraps, and it is excluded from both the resolver and persistence. Nothing settled the guess afterwards. `setSessionDirectory` performs exactly that promotion, but only confirmed destinations call it — a completed move or a worktree this client created. A session whose directory the client learned about later, such as one in a worktree created outside this client, kept the guess forever: every message fetch was addressed to the parent repository, which does not own the session. Captured for such a session before this change, with the session already indexed and its owning store known: routedDirectory .../worktree/feature currentSessionDirectory /repo <- guess, never settled opencodeClientDirectory /repo conflict selected -> /repo and after: routedDirectory .../worktree/feature currentSessionDirectory .../worktree/feature opencodeClientDirectory .../worktree/feature conflict null Directory bootstrap completion is the moment the authoritative directory first becomes readable, so the promotion runs there. It only ever promotes a guess: a confirmed selection and a selection that has since moved on are both left alone, and tests cover both directions. This removes a real routing split-brain. It does not by itself fix the reported symptom of a session created mid-session never rendering; that remains open.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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): void => {
|
||||
const store = manager.ensureChild(directory, { bootstrap: false })
|
||||
store.setState({
|
||||
session: [{ id: SESSION_ID, directory, 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("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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user