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:
@@ -8,7 +8,8 @@
|
|||||||
"start:clear": "expo start --clear",
|
"start:clear": "expo start --clear",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "node --test 'src/**/*.test.ts'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
|||||||
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 type { Session, Message, Part, Event, MessageWithParts, Client } from "../lib/sdk"
|
||||||
import { useConnections } from "./connections"
|
import { useConnections } from "./connections"
|
||||||
import { useSettings } from "./settings"
|
import { useSettings } from "./settings"
|
||||||
|
import { sessionScopeDirectory } from "./sessionScope"
|
||||||
import { addBreadcrumb } from "../lib/sentry"
|
import { addBreadcrumb } from "../lib/sentry"
|
||||||
|
|
||||||
// Helper to convert API response to our internal format
|
// Helper to convert API response to our internal format
|
||||||
@@ -100,10 +101,8 @@ export const useSessions = create<SessionsState>((set, get) => ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const listClient =
|
const scopeDir = sessionScopeDirectory(Boolean(latestConnState.activeConnection?.directory), home)
|
||||||
!latestConnState.activeConnection?.directory && home
|
const listClient = scopeDir ? latestConnState.clientForDirectory(scopeDir) : latestConnState.client
|
||||||
? latestConnState.clientForDirectory(home)
|
|
||||||
: latestConnState.client
|
|
||||||
|
|
||||||
const sessions = await (listClient || connState.client).session.list({ roots: true, limit: 50 })
|
const sessions = await (listClient || connState.client).session.list({ roots: true, limit: 50 })
|
||||||
set({ sessions, isLoading: false })
|
set({ sessions, isLoading: false })
|
||||||
@@ -191,18 +190,11 @@ export const useSessions = create<SessionsState>((set, get) => ({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the session in the SAME directory scope that loadSessions reads from.
|
// Create the session in the SAME directory scope that loadSessions reads from
|
||||||
// loadSessions lists home-scoped sessions when the connection has no explicit
|
// (see sessionScopeDirectory / bug #10). Both paths derive scope from the one
|
||||||
// directory; creating via the plain connection client targets the server's CWD
|
// helper so a freshly created session is always visible to the list.
|
||||||
// instead. When CWD != home the new session was invisible to the list (#10:
|
const scopeDir = sessionScopeDirectory(Boolean(connState.activeConnection?.directory), connState.serverHome)
|
||||||
// "sessions tab empty after connect / create"). Mirror the list's scoping so a
|
const client = scopeDir ? connState.clientForDirectory(scopeDir) || connState.client : connState.client
|
||||||
// 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
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await client.session.create({ title })
|
const session = await client.session.create({ title })
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user