feat(sessions): move sessions to existing worktrees

This commit is contained in:
mattv8
2026-08-27 18:42:57 -06:00
parent b8465ae133
commit 9babd19d9b
29 changed files with 1967 additions and 98 deletions
@@ -11,6 +11,7 @@ type WorktreeListEntry = {
const listCalls: string[] = [];
const listResolvers: Array<(value: WorktreeListEntry[]) => void> = [];
const listRejecters: Array<(reason: Error) => void> = [];
const createPayloads: unknown[] = [];
const validatePayloads: unknown[] = [];
const createdWorktree = {
@@ -78,8 +79,9 @@ mock.module('@/lib/gitApi', () => ({
worktree: {
list: (directory: string) => {
listCalls.push(directory);
return new Promise<WorktreeListEntry[]>((resolve) => {
return new Promise<WorktreeListEntry[]>((resolve, reject) => {
listResolvers.push(resolve);
listRejecters.push((reason: Error) => reject(reason));
});
},
create: mock((_directory: string, payload: unknown) => {
@@ -118,6 +120,7 @@ describe('worktreeManager list invalidation', () => {
beforeEach(() => {
listCalls.length = 0;
listResolvers.length = 0;
listRejecters.length = 0;
createPayloads.length = 0;
validatePayloads.length = 0;
bootstrapWatcherCalls.length = 0;
@@ -152,6 +155,85 @@ describe('worktreeManager list invalidation', () => {
expect(result.map((entry) => entry.path)).toEqual(['/repo-feature']);
});
test('forced refresh bypasses a fresh cached result', async () => {
const project = { id: 'project-force-cache', path: '/repo-force-cache' };
const initialListing = listProjectWorktrees(project);
await waitForListCallCount(1);
listResolvers[0]([]);
const initialResult = await initialListing;
expect(initialResult).toEqual([]);
const cachedResult = await listProjectWorktrees(project);
expect(cachedResult).toEqual([]);
expect(listCalls).toEqual(['/repo-force-cache']);
const forcedListing = listProjectWorktrees(project, { force: true });
await waitForListCallCount(2);
listResolvers[1]([createdWorktree]);
const forcedResult = await forcedListing;
expect(forcedResult.map((entry) => entry.path)).toEqual(['/repo-feature']);
expect(listCalls).toEqual(['/repo-force-cache', '/repo-force-cache']);
const refreshedCachedResult = await listProjectWorktrees(project);
expect(refreshedCachedResult.map((entry) => entry.path)).toEqual(['/repo-feature']);
expect(listCalls).toEqual(['/repo-force-cache', '/repo-force-cache']);
});
test('forced refresh starts a new request instead of joining an older in-flight list', async () => {
const project = { id: 'project-force-inflight', path: '/repo-force-inflight' };
const initialListing = listProjectWorktrees(project);
await waitForListCallCount(1);
const forcedListing = listProjectWorktrees(project, { force: true });
await waitForListCallCount(2);
listResolvers[1]([createdWorktree]);
const forcedResult = await forcedListing;
expect(forcedResult.map((entry) => entry.path)).toEqual(['/repo-feature']);
listResolvers[0]([]);
await waitForListCallCount(3);
listResolvers[2]([createdWorktree]);
const initialResult = await initialListing;
expect(initialResult.map((entry) => entry.path)).toEqual(['/repo-feature']);
expect(listCalls).toEqual([
'/repo-force-inflight',
'/repo-force-inflight',
'/repo-force-inflight',
]);
});
test('older completions do not replace a forced refresh result with stale topology', async () => {
const project = { id: 'project-force-stale', path: '/repo-force-stale' };
void listProjectWorktrees(project);
await waitForListCallCount(1);
const forcedListing = listProjectWorktrees(project, { force: true });
await waitForListCallCount(2);
listResolvers[1]([createdWorktree]);
const forcedResult = await forcedListing;
expect(forcedResult.map((entry) => entry.path)).toEqual(['/repo-feature']);
listResolvers[0]([{ path: '/repo-stale', branch: 'stale', name: 'stale' }]);
await waitForListCallCount(3);
const cachedResult = await listProjectWorktrees(project);
expect(cachedResult.map((entry) => entry.path)).toEqual(['/repo-feature']);
});
test('rejects when git worktree listing fails', async () => {
const project = { id: 'project-force-failure', path: '/repo-force-failure' };
const listing = listProjectWorktrees(project);
await waitForListCallCount(1);
listRejecters[0](new Error('git failed'));
await expect(listing).rejects.toThrow('git failed');
});
test('marks fast-created worktrees pending until bootstrap settles', async () => {
const metadata = await createWorktree({ id: 'project-1', path: '/repo' }, {
preferredName: 'feature',
@@ -389,6 +471,7 @@ describe('worktreeManager fork remote payload wiring', () => {
beforeEach(() => {
listCalls.length = 0;
listResolvers.length = 0;
listRejecters.length = 0;
createPayloads.length = 0;
validatePayloads.length = 0;
bootstrapWatcherCalls.length = 0;