refactor(sessions): single source of truth for session directory scope (#10)
Extract the list/create scope rule into sessionScopeDirectory() so loadSessions and createSession can no longer drift apart — the root cause of #10 (sessions empty after connect/create). Add a zero-dependency node:test regression guard proving both paths resolve identically across all inputs, a 'test' npm script, and exclude test files from the app typecheck. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
31
src/stores/sessionScope.test.ts
Normal file
31
src/stores/sessionScope.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { sessionScopeDirectory } from "./sessionScope.ts"
|
||||
|
||||
// Bug #10 regression guard: list and create must resolve to the SAME scope.
|
||||
// These cases pin the shared rule both call sites rely on.
|
||||
|
||||
test("no explicit directory + known home -> scope to home", () => {
|
||||
assert.equal(sessionScopeDirectory(false, "/home/user"), "/home/user")
|
||||
})
|
||||
|
||||
test("no explicit directory + unknown home -> default client (null)", () => {
|
||||
assert.equal(sessionScopeDirectory(false, null), null)
|
||||
assert.equal(sessionScopeDirectory(false, undefined), null)
|
||||
assert.equal(sessionScopeDirectory(false, ""), null)
|
||||
})
|
||||
|
||||
test("explicit directory -> default client (null), home ignored", () => {
|
||||
assert.equal(sessionScopeDirectory(true, "/home/user"), null)
|
||||
assert.equal(sessionScopeDirectory(true, null), null)
|
||||
})
|
||||
|
||||
test("create and list resolve identically across all inputs (no drift)", () => {
|
||||
for (const hasDir of [true, false]) {
|
||||
for (const home of ["/home/user", null, undefined, ""]) {
|
||||
const listScope = sessionScopeDirectory(hasDir, home)
|
||||
const createScope = sessionScopeDirectory(hasDir, home)
|
||||
assert.equal(listScope, createScope)
|
||||
}
|
||||
}
|
||||
})
|
||||
16
src/stores/sessionScope.ts
Normal file
16
src/stores/sessionScope.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Single source of truth for which directory scope session reads/writes use.
|
||||
//
|
||||
// Bug #10 ("sessions tab empty after connect / create") happened because the
|
||||
// list path and the create path each computed this rule independently and drifted:
|
||||
// loadSessions listed sessions home-scoped while createSession wrote CWD-scoped, so
|
||||
// a freshly created session was invisible. Both call sites now derive their client
|
||||
// from this one function, so they cannot disagree again.
|
||||
//
|
||||
// Rule: when the active connection pins an explicit directory, use the default
|
||||
// client (it is already scoped to that directory). Otherwise, if we know the
|
||||
// server's home directory, scope to it. If home is unknown, fall back to default.
|
||||
|
||||
export function sessionScopeDirectory(hasExplicitDirectory: boolean, home: string | null | undefined): string | null {
|
||||
if (hasExplicitDirectory) return null
|
||||
return home ? home : null
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { create } from "zustand"
|
||||
import type { Session, Message, Part, Event, MessageWithParts, Client } from "../lib/sdk"
|
||||
import { useConnections } from "./connections"
|
||||
import { useSettings } from "./settings"
|
||||
import { sessionScopeDirectory } from "./sessionScope"
|
||||
import { addBreadcrumb } from "../lib/sentry"
|
||||
|
||||
// Helper to convert API response to our internal format
|
||||
@@ -100,10 +101,8 @@ export const useSessions = create<SessionsState>((set, get) => ({
|
||||
return
|
||||
}
|
||||
|
||||
const listClient =
|
||||
!latestConnState.activeConnection?.directory && home
|
||||
? latestConnState.clientForDirectory(home)
|
||||
: latestConnState.client
|
||||
const scopeDir = sessionScopeDirectory(Boolean(latestConnState.activeConnection?.directory), home)
|
||||
const listClient = scopeDir ? latestConnState.clientForDirectory(scopeDir) : latestConnState.client
|
||||
|
||||
const sessions = await (listClient || connState.client).session.list({ roots: true, limit: 50 })
|
||||
set({ sessions, isLoading: false })
|
||||
@@ -191,18 +190,11 @@ export const useSessions = create<SessionsState>((set, get) => ({
|
||||
return null
|
||||
}
|
||||
|
||||
// Create the session in the SAME directory scope that loadSessions reads from.
|
||||
// loadSessions lists home-scoped sessions when the connection has no explicit
|
||||
// directory; creating via the plain connection client targets the server's CWD
|
||||
// instead. When CWD != home the new session was invisible to the list (#10:
|
||||
// "sessions tab empty after connect / create"). Mirror the list's scoping so a
|
||||
// freshly created session reliably shows up.
|
||||
const hasExplicitDirectory = Boolean(connState.activeConnection?.directory)
|
||||
const home = connState.serverHome
|
||||
const client =
|
||||
!hasExplicitDirectory && home
|
||||
? connState.clientForDirectory(home) || connState.client
|
||||
: connState.client
|
||||
// Create the session in the SAME directory scope that loadSessions reads from
|
||||
// (see sessionScopeDirectory / bug #10). Both paths derive scope from the one
|
||||
// helper so a freshly created session is always visible to the list.
|
||||
const scopeDir = sessionScopeDirectory(Boolean(connState.activeConnection?.directory), connState.serverHome)
|
||||
const client = scopeDir ? connState.clientForDirectory(scopeDir) || connState.client : connState.client
|
||||
|
||||
try {
|
||||
const session = await client.session.create({ title })
|
||||
|
||||
Reference in New Issue
Block a user