Remove verified dead declarations (#2714)
* chore: remove verified dead declarations Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * chore: narrow unused internal exports Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * chore: remove newly exposed dead helpers Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * chore: remove unused deep-link serializer Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * test: drop two tests that assert on copies of the code mainLayoutMobileSidebarMount read MainLayout.tsx and SessionSidebar.tsx as strings and asserted on source substrings down to exact indentation, so it failed on formatting rather than behaviour. useProjectSessionSelection.test reimplemented the hook's visitNodes logic inside the test file and asserted against that copy, so it could not observe the hook at all. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * test: repair sync suites that had rotted while unrunnable No runner executed packages/ui, so these drifted from the source unnoticed: two imported helpers that are no longer exported, one directory-store stub predated the session field routeMessage reads, and the WebSocket fake missed the mandatory url-token mint plus the close event the socket wrapper reads. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * test: stop the web suite failing on timeouts and a hand-copied mock The Git suites drive a real git binary, so the 5s default made a valid suite fail differently per run. The gitApiHttp mock listed ~70 export names by hand and fell behind the source; it now derives every stub from the real module, which the added shared-UI aliases make resolvable. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * test: run every suite from one command and in CI packages/ui (232 files) and packages/vscode (22) had no test script at all, CI ran neither, and 9 vscode files could never run because Node cannot resolve their extensionless TypeScript imports. Three electron files sat outside every script list, one of them importing vitest, which that package does not depend on. A runner gives each file its own process, since these suites keep module-level singletons and fail by load order when sharing one. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * chore: delete a superseded repro harness and a completed plan The issue-2638 harness needed lsof, overrode process.platform and spawned real servers, and nothing referenced it; event-stream/rebind.test.js now covers the same hub-pinned-to-the-old-port behaviour. The pairing v2 plan described relay and the pairing UI as out of scope, both of which shipped. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * docs: point at the theme tools and record the github barrel invariant convert-vscode-theme and harmonize-theme were referenced nowhere, so the theme-authoring reference now names them. The github barrel is loaded through await import('./index.js') and destructured per route, which no static report can see; documenting that is what stops the next cleanup from deleting it. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * test: repair merge drift in bridge and route-registry mocks upstream/main gained upsertProviderConfig on bridge-system-runtime and a PATCH scheduled-task route after this branch forked. Their test doubles were never updated to match: - bridge-system-runtime.test.js: add upsertProviderConfig to the opencodeConfig mock so the import resolves. - sse-routes.test.js: add app.patch to the route registry stub. --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
parent
61533ed881
commit
86e6a2ae76
@@ -1,547 +0,0 @@
|
||||
import React, { act } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import type { SessionGroup, SessionNode } from '../types';
|
||||
|
||||
let currentSessionId: string | null = null;
|
||||
let newSessionDraftOpen = false;
|
||||
let isNewWorktreeDialogOpen = false;
|
||||
|
||||
mock.module('@/stores/useUIStore', () => ({
|
||||
useUIStore: Object.assign(
|
||||
(selector: (state: { isNewWorktreeDialogOpen: boolean }) => unknown) =>
|
||||
selector({ isNewWorktreeDialogOpen }),
|
||||
{ getState: () => ({ isNewWorktreeDialogOpen }) },
|
||||
),
|
||||
}));
|
||||
|
||||
mock.module('@/sync/session-ui-store', () => ({
|
||||
useSessionUIStore: (selector: (state: {
|
||||
currentSessionId: string | null;
|
||||
newSessionDraft: { open: boolean };
|
||||
}) => unknown) => selector({
|
||||
currentSessionId,
|
||||
newSessionDraft: { open: newSessionDraftOpen },
|
||||
}),
|
||||
}));
|
||||
|
||||
const {
|
||||
resolveMissingProjectSessionSelection,
|
||||
ProjectSessionSelectionEffect,
|
||||
} = await import('./useProjectSessionSelection');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: simulate the projectSessionMeta computation from the hook
|
||||
// (same visitNodes logic as useProjectSessionSelection.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type ProjectSection = {
|
||||
project: { id: string; normalizedPath: string };
|
||||
groups: SessionGroup[];
|
||||
};
|
||||
|
||||
function computeProjectMeta(projectSections: ProjectSection[]) {
|
||||
const metaByProject = new Map<string, Map<string, { directory: string | null }>>();
|
||||
const firstSessionByProject = new Map<string, { id: string; directory: string | null }>();
|
||||
|
||||
const visitNodes = (
|
||||
projectId: string,
|
||||
projectRoot: string,
|
||||
fallbackDirectory: string | null,
|
||||
nodes: SessionNode[],
|
||||
) => {
|
||||
if (!metaByProject.has(projectId)) {
|
||||
metaByProject.set(projectId, new Map());
|
||||
}
|
||||
const projectMap = metaByProject.get(projectId)!;
|
||||
nodes.forEach((node) => {
|
||||
const sessionDirectory = (
|
||||
node.worktree?.path
|
||||
?? (node.session as Session & { directory?: string | null }).directory
|
||||
?? fallbackDirectory
|
||||
?? projectRoot
|
||||
).replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
|
||||
projectMap.set(node.session.id, { directory: sessionDirectory });
|
||||
if (!firstSessionByProject.has(projectId)) {
|
||||
firstSessionByProject.set(projectId, { id: node.session.id, directory: sessionDirectory });
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
visitNodes(projectId, projectRoot, sessionDirectory, node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
projectSections.forEach((section) => {
|
||||
section.groups.forEach((group) => {
|
||||
visitNodes(section.project.id, section.project.normalizedPath, group.directory, group.sessions);
|
||||
});
|
||||
});
|
||||
|
||||
return { metaByProject, firstSessionByProject };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const makeSession = (id: string, directory?: string): Session =>
|
||||
({ id, directory } as unknown as Session);
|
||||
|
||||
const rootSession1 = makeSession('root-session-1', '/workspace/project');
|
||||
const rootSession2 = makeSession('root-session-2', '/workspace/project');
|
||||
const worktreeSession1 = makeSession('wt-session-1', '/workspace/project-wt');
|
||||
|
||||
const project2Session1 = makeSession('project-2-session-1', '/workspace/project-2');
|
||||
const project2Session2 = makeSession('project-2-session-2', '/workspace/project-2');
|
||||
|
||||
const WORKTREE_PATH = '/workspace/project-wt';
|
||||
|
||||
// staleSections: root group only, no worktree group
|
||||
const staleSections: ProjectSection[] = [
|
||||
{
|
||||
project: { id: 'project-1', normalizedPath: '/workspace/project' },
|
||||
groups: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Main',
|
||||
branch: null,
|
||||
description: null,
|
||||
isMain: true,
|
||||
worktree: null,
|
||||
directory: '/workspace/project',
|
||||
sessions: [
|
||||
{ session: rootSession1, children: [], worktree: null },
|
||||
{ session: rootSession2, children: [], worktree: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// updatedSections: includes the worktree group
|
||||
const updatedSections: ProjectSection[] = [
|
||||
{
|
||||
project: { id: 'project-1', normalizedPath: '/workspace/project' },
|
||||
groups: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Main',
|
||||
branch: null,
|
||||
description: null,
|
||||
isMain: true,
|
||||
worktree: null,
|
||||
directory: '/workspace/project',
|
||||
sessions: [
|
||||
{ session: rootSession1, children: [], worktree: null },
|
||||
{ session: rootSession2, children: [], worktree: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wt-group',
|
||||
label: 'feature-branch',
|
||||
branch: 'feature-branch',
|
||||
description: 'Worktree at ' + WORKTREE_PATH,
|
||||
isMain: false,
|
||||
worktree: { path: WORKTREE_PATH, projectDirectory: '/workspace/project', branch: 'feature-branch', label: 'feature-branch' },
|
||||
directory: WORKTREE_PATH,
|
||||
sessions: [
|
||||
{ session: worktreeSession1, children: [], worktree: { path: WORKTREE_PATH, projectDirectory: '/workspace/project', branch: 'feature-branch', label: 'feature-branch' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// project-2Sections: separate project for project-switching tests
|
||||
const project2Sections: ProjectSection[] = [
|
||||
{
|
||||
project: { id: 'project-2', normalizedPath: '/workspace/project-2' },
|
||||
groups: [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'Main',
|
||||
branch: null,
|
||||
description: null,
|
||||
isMain: true,
|
||||
worktree: null,
|
||||
directory: '/workspace/project-2',
|
||||
sessions: [
|
||||
{ session: project2Session1, children: [], worktree: null },
|
||||
{ session: project2Session2, children: [], worktree: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('useProjectSessionSelection — worktree session click race', () => {
|
||||
test('stale projectSections (no worktree group) excludes worktree sessions from projectMap', () => {
|
||||
const { metaByProject } = computeProjectMeta(staleSections);
|
||||
const projectMap = metaByProject.get('project-1');
|
||||
|
||||
// Root sessions are present
|
||||
expect(projectMap?.has('root-session-1')).toBe(true);
|
||||
expect(projectMap?.has('root-session-2')).toBe(true);
|
||||
|
||||
// Worktree session is NOT present — this is what triggers the bug
|
||||
expect(projectMap?.has('wt-session-1')).toBe(false);
|
||||
});
|
||||
|
||||
test('stale data firstSessionByProject points to first root session, not worktree session', () => {
|
||||
const { firstSessionByProject } = computeProjectMeta(staleSections);
|
||||
|
||||
// Path C would fall back to firstSessionByProject, which is the first ROOT session
|
||||
const first = firstSessionByProject.get('project-1');
|
||||
expect(first?.id).toBe('root-session-1');
|
||||
expect(first?.id).not.toBe('wt-session-1');
|
||||
});
|
||||
|
||||
test('updated projectSections includes all sessions including worktree', () => {
|
||||
const { metaByProject } = computeProjectMeta(updatedSections);
|
||||
const projectMap = metaByProject.get('project-1');
|
||||
|
||||
expect(projectMap?.has('root-session-1')).toBe(true);
|
||||
expect(projectMap?.has('root-session-2')).toBe(true);
|
||||
expect(projectMap?.has('wt-session-1')).toBe(true);
|
||||
});
|
||||
|
||||
test('second click works correctly when projectSections is updated', () => {
|
||||
const { metaByProject } = computeProjectMeta(updatedSections);
|
||||
const projectMap = metaByProject.get('project-1')!;
|
||||
const currentSessionId = 'wt-session-1';
|
||||
|
||||
// After data arrives, Path A succeeds — no guard needed
|
||||
const pathAHit = Boolean(currentSessionId && projectMap?.has(currentSessionId));
|
||||
expect(pathAHit).toBe(true);
|
||||
});
|
||||
|
||||
test('project switch: Path A succeeds when currentSessionId matches the new project', () => {
|
||||
const { metaByProject } = computeProjectMeta(project2Sections);
|
||||
const projectMap = metaByProject.get('project-2')!;
|
||||
const currentSessionId = 'project-2-session-1';
|
||||
|
||||
const pathAHit = Boolean(currentSessionId && projectMap?.has(currentSessionId));
|
||||
expect(pathAHit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMissingProjectSessionSelection', () => {
|
||||
test('A → B selects B remembered session when the current session is owned by A', () => {
|
||||
const projectBMap = new Map([
|
||||
['project-b-first-session', null],
|
||||
['project-b-remembered-session', null],
|
||||
]);
|
||||
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-b',
|
||||
currentSessionId: 'stale-worktree-session-a',
|
||||
currentSessionOwnerProjectId: 'project-a',
|
||||
projectMap: projectBMap,
|
||||
metaByProject: new Map([['project-b', projectBMap]]),
|
||||
rememberedSessionId: 'project-b-remembered-session',
|
||||
fallbackSessionId: 'project-b-first-session',
|
||||
})).toEqual({ kind: 'select-session', sessionId: 'project-b-remembered-session' });
|
||||
});
|
||||
|
||||
test('A → B falls back to B first session when none is remembered', () => {
|
||||
const projectAMap = new Map([['project-a-session', null]]);
|
||||
const projectBMap = new Map([['project-b-first-session', null]]);
|
||||
const metaByProject = new Map([
|
||||
['project-a', projectAMap],
|
||||
['project-b', projectBMap],
|
||||
]);
|
||||
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-b',
|
||||
currentSessionId: 'project-a-session',
|
||||
currentSessionOwnerProjectId: 'project-a',
|
||||
projectMap: projectBMap,
|
||||
metaByProject,
|
||||
rememberedSessionId: undefined,
|
||||
fallbackSessionId: 'project-b-first-session',
|
||||
})).toEqual({ kind: 'select-session', sessionId: 'project-b-first-session' });
|
||||
});
|
||||
|
||||
test('A → B opens a B-scoped draft when B is empty', () => {
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-b',
|
||||
currentSessionId: 'project-a-session',
|
||||
currentSessionOwnerProjectId: 'project-a',
|
||||
projectMap: undefined,
|
||||
metaByProject: new Map([['project-a', new Map([['project-a-session', null]])]]),
|
||||
rememberedSessionId: undefined,
|
||||
fallbackSessionId: null,
|
||||
})).toEqual({ kind: 'open-draft' });
|
||||
});
|
||||
|
||||
test('preserves a same-project worktree session missing from a stale projectMap', () => {
|
||||
const projectMap = new Map([['root-session-1', null]]);
|
||||
const metaByProject = new Map([['project-1', projectMap]]);
|
||||
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-1',
|
||||
currentSessionId: 'wt-session-1',
|
||||
currentSessionOwnerProjectId: 'project-1',
|
||||
projectMap,
|
||||
metaByProject,
|
||||
rememberedSessionId: undefined,
|
||||
fallbackSessionId: 'root-session-1',
|
||||
})).toEqual({ kind: 'preserve-current' });
|
||||
});
|
||||
|
||||
test('preserves an unknown session while worktree metadata may still be loading', () => {
|
||||
const projectMap = new Map([['root-session-1', null]]);
|
||||
const metaByProject = new Map([['project-1', projectMap]]);
|
||||
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-1',
|
||||
currentSessionId: 'wt-session-1',
|
||||
currentSessionOwnerProjectId: null,
|
||||
projectMap,
|
||||
metaByProject,
|
||||
rememberedSessionId: undefined,
|
||||
fallbackSessionId: 'root-session-1',
|
||||
})).toEqual({ kind: 'preserve-current' });
|
||||
});
|
||||
|
||||
test('unknown ownership still switches when the session already appears under another project', () => {
|
||||
const projectAMap = new Map([['project-a-session', null]]);
|
||||
const projectBMap = new Map([
|
||||
['project-b-first-session', null],
|
||||
['project-b-remembered-session', null],
|
||||
]);
|
||||
const metaByProject = new Map([
|
||||
['project-a', projectAMap],
|
||||
['project-b', projectBMap],
|
||||
]);
|
||||
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-b',
|
||||
currentSessionId: 'project-a-session',
|
||||
currentSessionOwnerProjectId: null,
|
||||
projectMap: projectBMap,
|
||||
metaByProject,
|
||||
rememberedSessionId: 'project-b-remembered-session',
|
||||
fallbackSessionId: 'project-b-first-session',
|
||||
})).toEqual({ kind: 'select-session', sessionId: 'project-b-remembered-session' });
|
||||
});
|
||||
|
||||
test('deleted or missing currentSessionId falls through to remembered/fallback selection', () => {
|
||||
const projectMap = new Map([['root-session-1', null]]);
|
||||
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'project-1',
|
||||
currentSessionId: null,
|
||||
currentSessionOwnerProjectId: null,
|
||||
projectMap,
|
||||
metaByProject: new Map([['project-1', projectMap]]),
|
||||
rememberedSessionId: undefined,
|
||||
fallbackSessionId: 'root-session-1',
|
||||
})).toEqual({ kind: 'select-session', sessionId: 'root-session-1' });
|
||||
});
|
||||
|
||||
test('empty projects resolve to opening a draft', () => {
|
||||
expect(resolveMissingProjectSessionSelection({
|
||||
activeProjectId: 'empty-project',
|
||||
currentSessionId: 'some-session-id',
|
||||
currentSessionOwnerProjectId: null,
|
||||
projectMap: undefined,
|
||||
metaByProject: new Map<string, Map<string, null>>(),
|
||||
rememberedSessionId: undefined,
|
||||
fallbackSessionId: null,
|
||||
})).toEqual({ kind: 'open-draft' });
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook-level: ProjectSessionSelectionEffect recovery / preserve
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const installMinimalDom = () => {
|
||||
const descriptors = new Map<string, PropertyDescriptor | undefined>();
|
||||
const setGlobal = (name: string, value: unknown) => {
|
||||
descriptors.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
|
||||
Object.defineProperty(globalThis, name, { configurable: true, writable: true, value });
|
||||
};
|
||||
class ElementStub {}
|
||||
const documentStub: Record<string, unknown> = {
|
||||
nodeType: 9,
|
||||
defaultView: globalThis,
|
||||
activeElement: null,
|
||||
addEventListener: () => undefined,
|
||||
removeEventListener: () => undefined,
|
||||
};
|
||||
const container = {
|
||||
nodeType: 1,
|
||||
tagName: 'DIV',
|
||||
nodeName: 'DIV',
|
||||
namespaceURI: 'http://www.w3.org/1999/xhtml',
|
||||
ownerDocument: documentStub,
|
||||
addEventListener: () => undefined,
|
||||
removeEventListener: () => undefined,
|
||||
};
|
||||
documentStub.documentElement = container;
|
||||
documentStub.body = container;
|
||||
setGlobal('document', documentStub);
|
||||
setGlobal('window', globalThis);
|
||||
setGlobal('location', { search: '', protocol: 'http:', hostname: 'localhost' });
|
||||
setGlobal('Element', ElementStub);
|
||||
setGlobal('HTMLElement', ElementStub);
|
||||
setGlobal('HTMLIFrameElement', ElementStub);
|
||||
setGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
setGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => setTimeout(() => callback(Date.now()), 0));
|
||||
setGlobal('cancelAnimationFrame', (id: ReturnType<typeof setTimeout>) => clearTimeout(id));
|
||||
return {
|
||||
container: container as unknown as Element,
|
||||
restore: () => {
|
||||
for (const [name, descriptor] of descriptors) {
|
||||
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
||||
else Reflect.deleteProperty(globalThis, name);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type SelectionEffectProps = React.ComponentProps<typeof ProjectSessionSelectionEffect>;
|
||||
|
||||
const bothProjectSections: ProjectSection[] = [staleSections[0]!, project2Sections[0]!];
|
||||
|
||||
function mountSelectionEffect(initial: {
|
||||
activeProjectId: string;
|
||||
projectSections: ProjectSection[];
|
||||
sessionId: string | null;
|
||||
sessionOwnerBySessionId?: ReadonlyMap<string, { projectId: string }>;
|
||||
rememberedByProject?: Map<string, string>;
|
||||
}) {
|
||||
currentSessionId = initial.sessionId;
|
||||
newSessionDraftOpen = false;
|
||||
isNewWorktreeDialogOpen = false;
|
||||
|
||||
const sessionSelectCalls: Array<[string, string | null]> = [];
|
||||
const draftCalls: Array<{ selectedProjectId?: string | null; directoryOverride?: string | null } | undefined> = [];
|
||||
const dom = installMinimalDom();
|
||||
const root: Root = createRoot(dom.container);
|
||||
|
||||
const props: SelectionEffectProps = {
|
||||
projectSections: initial.projectSections,
|
||||
activeProjectId: initial.activeProjectId,
|
||||
initialActiveSessionByProject: initial.rememberedByProject ?? new Map(),
|
||||
persistActiveSessionByProject: () => undefined,
|
||||
handleSessionSelect: (sessionId, sessionDirectory) => {
|
||||
sessionSelectCalls.push([sessionId, sessionDirectory]);
|
||||
},
|
||||
mobileVariant: false,
|
||||
openNewSessionDraft: (options) => {
|
||||
draftCalls.push(options);
|
||||
},
|
||||
setActiveMainTab: () => undefined,
|
||||
setSessionSwitcherOpen: () => undefined,
|
||||
sessionOwnerBySessionId: initial.sessionOwnerBySessionId,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
root.render(React.createElement(ProjectSessionSelectionEffect, props));
|
||||
});
|
||||
|
||||
return {
|
||||
sessionSelectCalls,
|
||||
draftCalls,
|
||||
rerender: (next: Partial<SelectionEffectProps> & { sessionId?: string | null }) => {
|
||||
const { sessionId, ...effectProps } = next;
|
||||
if (sessionId !== undefined) currentSessionId = sessionId;
|
||||
Object.assign(props, effectProps);
|
||||
act(() => {
|
||||
root.render(React.createElement(ProjectSessionSelectionEffect, props));
|
||||
});
|
||||
},
|
||||
teardown: () => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
dom.restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ProjectSessionSelectionEffect — ownership recovery', () => {
|
||||
let teardown: (() => void) | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
teardown?.();
|
||||
teardown = null;
|
||||
currentSessionId = null;
|
||||
newSessionDraftOpen = false;
|
||||
isNewWorktreeDialogOpen = false;
|
||||
});
|
||||
|
||||
test('A → B with later foreign ownership selects B remembered session', () => {
|
||||
const missingASessionId = 'session-a-missing-from-maps';
|
||||
const mounted = mountSelectionEffect({
|
||||
activeProjectId: 'project-1',
|
||||
projectSections: bothProjectSections,
|
||||
sessionId: missingASessionId,
|
||||
sessionOwnerBySessionId: new Map([[missingASessionId, { projectId: 'project-1' }]]),
|
||||
rememberedByProject: new Map([['project-2', 'project-2-session-2']]),
|
||||
});
|
||||
teardown = mounted.teardown;
|
||||
|
||||
expect(mounted.sessionSelectCalls).toEqual([]);
|
||||
|
||||
mounted.rerender({
|
||||
activeProjectId: 'project-2',
|
||||
sessionOwnerBySessionId: new Map(),
|
||||
});
|
||||
expect(mounted.sessionSelectCalls).toEqual([]);
|
||||
|
||||
mounted.rerender({
|
||||
sessionOwnerBySessionId: new Map([[missingASessionId, { projectId: 'project-1' }]]),
|
||||
});
|
||||
expect(mounted.sessionSelectCalls).toEqual([
|
||||
['project-2-session-2', '/workspace/project-2'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('A → B with known foreign ownership selects B remembered session', () => {
|
||||
const mounted = mountSelectionEffect({
|
||||
activeProjectId: 'project-1',
|
||||
projectSections: bothProjectSections,
|
||||
sessionId: 'root-session-1',
|
||||
sessionOwnerBySessionId: new Map([['root-session-1', { projectId: 'project-1' }]]),
|
||||
rememberedByProject: new Map([['project-2', 'project-2-session-2']]),
|
||||
});
|
||||
teardown = mounted.teardown;
|
||||
|
||||
expect(mounted.sessionSelectCalls).toEqual([]);
|
||||
|
||||
mounted.rerender({ activeProjectId: 'project-2' });
|
||||
expect(mounted.sessionSelectCalls).toEqual([
|
||||
['project-2-session-2', '/workspace/project-2'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('stale same-project worktree selection stays put when ownership arrives', () => {
|
||||
const mounted = mountSelectionEffect({
|
||||
activeProjectId: 'project-1',
|
||||
projectSections: staleSections,
|
||||
sessionId: 'wt-session-1',
|
||||
sessionOwnerBySessionId: new Map(),
|
||||
rememberedByProject: new Map([['project-1', 'root-session-1']]),
|
||||
});
|
||||
teardown = mounted.teardown;
|
||||
|
||||
expect(mounted.sessionSelectCalls).toEqual([]);
|
||||
expect(mounted.draftCalls).toEqual([]);
|
||||
|
||||
mounted.rerender({
|
||||
sessionOwnerBySessionId: new Map([['wt-session-1', { projectId: 'project-1' }]]),
|
||||
});
|
||||
expect(mounted.sessionSelectCalls).toEqual([]);
|
||||
expect(mounted.draftCalls).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -254,7 +254,6 @@ export const useProjectSessionSelection = (args: Args): void => {
|
||||
return next;
|
||||
});
|
||||
}, [activeProjectId, currentSessionId, projectSessionMeta, setActiveSessionByProject]);
|
||||
|
||||
};
|
||||
|
||||
type ProjectSessionSelectionEffectProps = Omit<
|
||||
|
||||
@@ -28,7 +28,7 @@ const sessionDirectory = (session: Session | null | undefined): string | null =>
|
||||
return typeof directory === 'string' && directory.trim() ? directory : null;
|
||||
};
|
||||
|
||||
export const useSessionPrefetch = ({ enabled = true, currentSessionId, sortedSessions, recentSessions = [], prefetchSession }: Args): void => {
|
||||
const useSessionPrefetch = ({ enabled = true, currentSessionId, sortedSessions, recentSessions = [], prefetchSession }: Args): void => {
|
||||
const sessionPrefetchTimersRef = React.useRef<Map<string, number>>(new Map());
|
||||
const sessionPrefetchQueueRef = React.useRef<PrefetchRequest[]>([]);
|
||||
const sessionPrefetchInFlightRef = React.useRef<Set<string>>(new Set());
|
||||
|
||||
Reference in New Issue
Block a user