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:
engineer
2026-06-01 23:46:48 -07:00
parent 0615ab898e
commit bed0b6f622
5 changed files with 59 additions and 18 deletions

View File

@@ -8,7 +8,8 @@
"start:clear": "expo start --clear",
"ios": "expo run:ios",
"android": "expo run:android",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "node --test 'src/**/*.test.ts'"
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",

View 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)
}
}
})

View 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
}

View File

@@ -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 })

View File

@@ -6,5 +6,6 @@
"@/*": ["./src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
"exclude": ["**/*.test.ts", "**/*.test.tsx"]
}