diff --git a/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts b/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts new file mode 100644 index 00000000000..208445bf11c --- /dev/null +++ b/src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import { buildAgentLaunchRouteInput, type AgentLaunchRouteStore } from './agent-launch-route-input' +import { getConnectionIdFromState } from './connection-owner-resolution' +import { planAgentSessionLaunch } from './agent-session-launch-plan' + +// Deliberately un-mocked: the defect this pins lives in the owner resolution itself, so a suite +// that stages `getConnectionIdFromState` cannot catch it. Grok is the agent the answer routes on — +// only agents whose hook discloses no transcript path read the local-readability input at all. +const NATIVE_CHAT_SETTINGS = { experimentalNativeChat: true, openAgentTabsInChatByDefault: true } +const WORKTREE_ID = 'repo-1::/repo/wt-1' +const WORKSPACE = { kind: 'git-worktree', worktreeId: WORKTREE_ID, repoId: 'repo-1' } as const + +/** Rival repos publishing the same worktree id on different hosts: the documented case where owner + * resolution refuses to name a connection rather than authorize a local read of a remote path. */ +function storeWithAmbiguousWorktreeRows( + repoConnectionId: string | null +): AgentLaunchRouteStore & { repos: unknown[] } { + return { + settings: NATIVE_CHAT_SETTINGS, + repos: [{ id: 'repo-1', path: '/repo', connectionId: repoConnectionId }], + worktreesByRepo: { + 'repo-1': [{ id: WORKTREE_ID, repoId: 'repo-1', hostId: null }], + 'repo-2': [{ id: WORKTREE_ID, repoId: 'repo-1', hostId: 'ssh:other-box' }] + } + } as unknown as AgentLaunchRouteStore & { repos: unknown[] } +} + +describe('launch route transcript readability', () => { + it('cannot name the connection from the worktree when its rows disagree', () => { + // The premise of the fallback: this is what returns `undefined`, and `undefined` is not + // evidence that the transcript is unreadable. + expect( + getConnectionIdFromState(storeWithAmbiguousWorktreeRows(null), WORKTREE_ID) + ).toBeUndefined() + }) + + it('falls back to the local repo rather than downgrading native chat to a terminal', () => { + const store = storeWithAmbiguousWorktreeRows(null) + + expect( + buildAgentLaunchRouteInput(store, { agent: 'grok', workspace: WORKSPACE }) + .nativeChatTranscriptIsLocalReadable + ).toBe(true) + expect(planAgentSessionLaunch(store, { agent: 'grok', workspace: WORKSPACE }).route).toBe( + 'legacy-native-chat' + ) + }) + + it('keeps a remote repo off native chat through the same fallback', () => { + const store = storeWithAmbiguousWorktreeRows('build-box') + + expect(planAgentSessionLaunch(store, { agent: 'grok', workspace: WORKSPACE }).route).toBe( + 'terminal-tui' + ) + }) + + it('has no repo to fall back to when the workspace names none', () => { + const store = storeWithAmbiguousWorktreeRows(null) + + expect( + planAgentSessionLaunch(store, { + agent: 'grok', + workspace: { kind: 'git-worktree', worktreeId: WORKTREE_ID } + }).route + ).toBe('terminal-tui') + }) +}) diff --git a/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts b/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts index 674c23928d4..40857e8a55d 100644 --- a/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts +++ b/src/renderer/src/lib/agent-launch-routing-caller-census.test.ts @@ -25,12 +25,6 @@ const LAUNCH_AGENT_IN_NEW_TAB_CALLERS = [ const ROUTE_RESOLVER_DEFINITION = 'src/renderer/src/lib/agent-launch-routing.ts' const ROUTE_PLANNER = 'src/renderer/src/lib/agent-session-launch-plan.ts' const DIRECT_ROUTE_RESOLVER_CALL = /\b(?:resolveAgentLaunchRoute|structuredAgentLaunchSupported)\(/ -// Why: asking whether a structured session is POSSIBLE is a query, not a launch decision, so it is -// allowed outside the planner — but only through the planner's own predicate, and only from the -// surfaces pinned here. A UI that builds a plan to answer it is the bypass this census catches. -const STRUCTURED_FEASIBILITY_QUERY_CALLERS = [ - 'src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.ts' -] // Why: adopting a verdict bypasses the resolver by design (a persisted quick-create request, a // resume whose gate already planned), so each adopter is pinned rather than trusted by convention. const VERDICT_ADOPTERS = [ @@ -66,25 +60,6 @@ describe('agent launch routing caller census', () => { expect(directCallers).toEqual([ROUTE_PLANNER]) }) - it('pins every production caller of the structured feasibility query', async () => { - const callers = (await productionFiles()) - .filter((file) => file !== ROUTE_PLANNER) - .filter((file) => - readFileSync(join(REPO_ROOT, file), 'utf8').includes( - 'structuredAgentSessionLaunchFeasible(' - ) - ) - .sort() - expect(callers).toEqual([...STRUCTURED_FEASIBILITY_QUERY_CALLERS].sort()) - }) - - it('keeps the feasibility query out of every launch decision', async () => { - // A query caller that also plans a launch has re-crossed the line the split exists to draw. - for (const file of STRUCTURED_FEASIBILITY_QUERY_CALLERS) { - expect(readFileSync(join(REPO_ROOT, file), 'utf8')).not.toContain('planAgentSessionLaunch(') - } - }) - it('pins every production adopter of a planned verdict', async () => { const adopters = (await productionFiles()) .filter((file) => file !== ROUTE_PLANNER)