From bed0b6f6222c462570f33ea86b251cbdd9f14649 Mon Sep 17 00:00:00 2001 From: engineer Date: Mon, 1 Jun 2026 23:46:48 -0700 Subject: [PATCH] refactor(sessions): single source of truth for session directory scope (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 3 ++- src/stores/sessionScope.test.ts | 31 +++++++++++++++++++++++++++++++ src/stores/sessionScope.ts | 16 ++++++++++++++++ src/stores/sessions.ts | 24 ++++++++---------------- tsconfig.json | 3 ++- 5 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 src/stores/sessionScope.test.ts create mode 100644 src/stores/sessionScope.ts diff --git a/package.json b/package.json index fd6651f..fdd0697 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/stores/sessionScope.test.ts b/src/stores/sessionScope.test.ts new file mode 100644 index 0000000..98dc3bd --- /dev/null +++ b/src/stores/sessionScope.test.ts @@ -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) + } + } +}) diff --git a/src/stores/sessionScope.ts b/src/stores/sessionScope.ts new file mode 100644 index 0000000..2204567 --- /dev/null +++ b/src/stores/sessionScope.ts @@ -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 +} diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index e41e048..b9a2614 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -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((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((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 }) diff --git a/tsconfig.json b/tsconfig.json index ec96c84..d2c8fa1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] }