* fix(sync): reflect share status from global store after cancel Fix #1551: unshareSession() called updateLiveSession() which silently fails when the child store doesn't exist. The sidebar rendered from the child store first, showing stale share data. Now overlays the global session's share field at merge points. * fix(sync): extract shared mergeLiveSessionWithGlobalSession helper Extracted the share-field overlay into a single shared helper in useGlobalSessionsStore.ts. All 3 merge sites now use the helper instead of duplicating the overlay logic. * test(sync): add unit tests for mergeLiveSessionWithGlobalSession helper --------- Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
109 lines
4.9 KiB
TypeScript
109 lines
4.9 KiB
TypeScript
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
|
|
|
import { resolveGlobalSessionDirectory, mergeLiveSessionWithGlobalSession, useGlobalSessionsStore } from './useGlobalSessionsStore';
|
|
|
|
type SessionExtra = Partial<Session> & {
|
|
directory?: string | null;
|
|
project?: { worktree?: string | null } | null;
|
|
};
|
|
|
|
const buildSession = (shareUrl: string, extra: SessionExtra = {}): Session => ({
|
|
id: 'ses_1',
|
|
title: 'Shared session',
|
|
time: { created: 1, updated: 2 },
|
|
share: { url: shareUrl },
|
|
...extra,
|
|
} as Session);
|
|
|
|
describe('useGlobalSessionsStore', () => {
|
|
beforeEach(() => {
|
|
useGlobalSessionsStore.setState({
|
|
activeSessions: [],
|
|
archivedSessions: [],
|
|
sessionsByDirectory: new Map(),
|
|
hasLoaded: false,
|
|
status: 'idle',
|
|
});
|
|
});
|
|
|
|
test('updates an existing session when the share URL changes', () => {
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/a'));
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/b'));
|
|
|
|
expect(useGlobalSessionsStore.getState().activeSessions[0]?.share?.url).toBe('https://share.example/b');
|
|
});
|
|
|
|
test('preserves directory metadata when a live update omits it', () => {
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/a', { directory: '/repo/app' }));
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/b', {
|
|
time: { created: 1, updated: 3 },
|
|
}));
|
|
|
|
const session = useGlobalSessionsStore.getState().activeSessions[0];
|
|
expect(resolveGlobalSessionDirectory(session)).toBe('/repo/app');
|
|
expect(useGlobalSessionsStore.getState().sessionsByDirectory.get('/repo/app')?.[0]?.id).toBe('ses_1');
|
|
});
|
|
|
|
test('preserves raw directory metadata when a live update only has project worktree', () => {
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/a', { directory: '/repo/app' }));
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/b', {
|
|
project: { worktree: '/repo/app' },
|
|
time: { created: 1, updated: 3 },
|
|
}));
|
|
|
|
const session = useGlobalSessionsStore.getState().activeSessions[0] as Session & { directory?: string | null };
|
|
expect(session.directory).toBe('/repo/app');
|
|
expect(resolveGlobalSessionDirectory(session)).toBe('/repo/app');
|
|
});
|
|
|
|
test('trusts explicit incoming raw directory metadata', () => {
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/a', { directory: '/repo/app' }));
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/b', {
|
|
directory: '/repo/app-worktree',
|
|
time: { created: 1, updated: 3 },
|
|
}));
|
|
|
|
expect(resolveGlobalSessionDirectory(useGlobalSessionsStore.getState().activeSessions[0])).toBe('/repo/app-worktree');
|
|
expect(useGlobalSessionsStore.getState().sessionsByDirectory.get('/repo/app')).toBe(undefined);
|
|
expect(useGlobalSessionsStore.getState().sessionsByDirectory.get('/repo/app-worktree')?.[0]?.id).toBe('ses_1');
|
|
});
|
|
|
|
test('preserves directory metadata when moving a session to archived', () => {
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/a', { directory: '/repo/app' }));
|
|
useGlobalSessionsStore.getState().upsertSession(buildSession('https://share.example/b', {
|
|
time: { created: 1, updated: 3, archived: 4 },
|
|
}));
|
|
|
|
expect(useGlobalSessionsStore.getState().activeSessions).toEqual([]);
|
|
expect(resolveGlobalSessionDirectory(useGlobalSessionsStore.getState().archivedSessions[0])).toBe('/repo/app');
|
|
});
|
|
});
|
|
|
|
describe('mergeLiveSessionWithGlobalSession', () => {
|
|
test('preserves global share over live share', () => {
|
|
const live = buildSession('https://live.example/s', { time: { created: 1, updated: 5 } });
|
|
const global = buildSession('https://global.example/s', { time: { created: 1, updated: 3 } });
|
|
|
|
const merged = mergeLiveSessionWithGlobalSession(live, global);
|
|
expect(merged.share?.url).toBe('https://global.example/s');
|
|
expect(merged.time?.updated).toBe(5);
|
|
});
|
|
|
|
test('preserves directory from global when live omits it', () => {
|
|
const live = buildSession('https://live.example/s', { time: { created: 1, updated: 5 } });
|
|
const global = buildSession('https://global.example/s', { directory: '/repo/app' });
|
|
|
|
const merged = mergeLiveSessionWithGlobalSession(live, global);
|
|
expect(resolveGlobalSessionDirectory(merged)).toBe('/repo/app');
|
|
});
|
|
|
|
test('live directory takes precedence over global when present', () => {
|
|
const live = buildSession('https://live.example/s', { directory: '/repo/worktree' });
|
|
const global = buildSession('https://global.example/s', { directory: '/repo/app' });
|
|
|
|
const merged = mergeLiveSessionWithGlobalSession(live, global);
|
|
expect(resolveGlobalSessionDirectory(merged)).toBe('/repo/worktree');
|
|
});
|
|
});
|