Post-merge hardening of the session-to-worktree move (#2998), driven by review findings on the follow-up pass: - an ambiguous transport failure (relay abort, timeout) on the change-carrying move no longer force-deletes the fresh worktree that may hold the user's only copy of their changes; both intent kinds surface honest guidance and refresh both directories - assertSdkSuccess re-tags ambiguous transport errors when wrapping SDK failures, so ambiguity classification survives the wrapper on every path, matching the prompt-send precedent - session liveness checks scan all child stores plus the global status index, and report unknown (not idle) when no store covers the session — an evicted background directory can no longer make a busy session look movable - incomplete-rollback errors carry the changes-may-be-in-destination guidance instead of swallowing it - move-message assembly shared across the three call sites; tests now exercise the real ambiguity classifier (extracted to send-failure-classification.ts) instead of a hand-mirrored mock - i18n fallout from the merge train: Turkish gains the 21 worktree-move keys, all 12 locales get the hedged ambiguous-failure toast; owning DOCUMENTATION.md files record the new contracts
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import React from 'react';
|
|
import { describe, expect, mock, test } from 'bun:test';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
|
|
import { I18nProvider } from '@/lib/i18n';
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
|
import type {
|
|
SessionTreeMoveIntent,
|
|
SessionTreeMoveMessages,
|
|
} from '@/lib/worktrees/sessionWorktreeMove';
|
|
|
|
type MockDialogProps = React.PropsWithChildren<{
|
|
open?: boolean;
|
|
id?: string;
|
|
className?: string;
|
|
}>;
|
|
|
|
mock.module('@/components/ui/dialog', () => ({
|
|
Dialog: ({ children, open = true }: MockDialogProps) => (open ? <>{children}</> : null),
|
|
DialogContent: ({ children, id, className }: MockDialogProps) => (
|
|
<div id={id} className={className}>{children}</div>
|
|
),
|
|
DialogDescription: ({ children }: MockDialogProps) => <p>{children}</p>,
|
|
DialogFooter: ({ children, className }: MockDialogProps) => <div className={className}>{children}</div>,
|
|
DialogHeader: ({ children }: MockDialogProps) => <div>{children}</div>,
|
|
DialogTitle: ({ children }: MockDialogProps) => <h2>{children}</h2>,
|
|
}));
|
|
|
|
const { SessionWorktreeMoveConfirmDialog } = await import('./SessionWorktreeMoveConfirmDialog');
|
|
|
|
const makeMoveMessages = (): SessionTreeMoveMessages => ({
|
|
success: 'move succeeded',
|
|
failure: 'move failed',
|
|
sourceVerificationFailed: 'source verification failed',
|
|
applyChangesFailed: 'apply changes failed',
|
|
changesMayBeInDestination: 'changes may be in destination',
|
|
});
|
|
|
|
const makeExistingIntent = (): SessionTreeMoveIntent => ({
|
|
kind: 'existing',
|
|
root: {
|
|
id: 'root',
|
|
slug: 'root',
|
|
projectID: 'project-1',
|
|
directory: '/source',
|
|
title: 'Root session',
|
|
version: '1',
|
|
time: { created: 0, updated: 0 },
|
|
} satisfies Session,
|
|
descendants: [],
|
|
sourceDirectory: '/source',
|
|
destination: {
|
|
path: '/destination',
|
|
projectDirectory: '/repo',
|
|
branch: 'feature',
|
|
label: 'Destination',
|
|
worktreeStatus: 'ready',
|
|
worktreeSource: 'existing',
|
|
},
|
|
messages: makeMoveMessages(),
|
|
});
|
|
|
|
describe('SessionWorktreeMoveConfirmDialog', () => {
|
|
test('renders stable semantic hooks, dirty file count, and the staged warning', () => {
|
|
const markup = renderToStaticMarkup(
|
|
<I18nProvider>
|
|
<SessionWorktreeMoveConfirmDialog
|
|
value={{
|
|
intent: makeExistingIntent(),
|
|
dirtyFileCount: 2,
|
|
stagedFileCount: 1,
|
|
}}
|
|
onMoveSessionOnly={() => {}}
|
|
onMoveAllChanges={() => {}}
|
|
onCancel={() => {}}
|
|
/>
|
|
</I18nProvider>,
|
|
);
|
|
|
|
expect(markup).toContain('id="session-worktree-move-confirm-dialog"');
|
|
expect(markup).toContain('data-session-worktree-move-action="session-only"');
|
|
expect(markup).toContain('data-session-worktree-move-action="all-changes"');
|
|
expect(markup).toContain('data-session-worktree-move-action="cancel"');
|
|
expect(markup).toContain('autofocus=""');
|
|
expect(markup).toContain('2');
|
|
expect(markup).toContain('data-session-worktree-move-staged-warning="true"');
|
|
});
|
|
|
|
test('omits the staged warning when no staged files are present', () => {
|
|
const markup = renderToStaticMarkup(
|
|
<I18nProvider>
|
|
<SessionWorktreeMoveConfirmDialog
|
|
value={{
|
|
intent: makeExistingIntent(),
|
|
dirtyFileCount: 3,
|
|
stagedFileCount: 0,
|
|
}}
|
|
onMoveSessionOnly={() => {}}
|
|
onMoveAllChanges={() => {}}
|
|
onCancel={() => {}}
|
|
/>
|
|
</I18nProvider>,
|
|
);
|
|
|
|
expect(markup).not.toContain('data-session-worktree-move-staged-warning="true"');
|
|
});
|
|
});
|