feat(sessions): confirm dirty-source worktree moves
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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',
|
||||
});
|
||||
|
||||
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"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import type { SessionTreeMoveConfirmation } from '@/lib/worktrees/sessionWorktreeMove';
|
||||
|
||||
export type SessionWorktreeMoveConfirmDialogProps = {
|
||||
value: SessionTreeMoveConfirmation | null;
|
||||
onMoveSessionOnly: () => void;
|
||||
onMoveAllChanges: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export function SessionWorktreeMoveConfirmDialog(props: SessionWorktreeMoveConfirmDialogProps): React.ReactNode {
|
||||
const { t } = useI18n();
|
||||
const { value, onMoveSessionOnly, onMoveAllChanges, onCancel } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(value)} onOpenChange={(open) => { if (!open) onCancel(); }}>
|
||||
<DialogContent
|
||||
id="session-worktree-move-confirm-dialog"
|
||||
showCloseButton={false}
|
||||
className="max-w-md gap-5"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('sessions.sidebar.session.moveToWorktree.confirm.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('sessions.sidebar.session.moveToWorktree.confirm.changedFiles', {
|
||||
count: value?.dirtyFileCount ?? 0,
|
||||
})}{' '}
|
||||
{t('sessions.sidebar.session.moveToWorktree.confirm.ownership')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 typography-ui-label text-muted-foreground">
|
||||
<p>{t('sessions.sidebar.session.moveToWorktree.confirm.sessionOnlyHelp')}</p>
|
||||
<p>{t('sessions.sidebar.session.moveToWorktree.confirm.allChangesHelp')}</p>
|
||||
{value && value.stagedFileCount > 0 ? (
|
||||
<p data-session-worktree-move-staged-warning="true">
|
||||
{t('sessions.sidebar.session.moveToWorktree.confirm.stagedWarning')}
|
||||
</p>
|
||||
) : null}
|
||||
<p>{t('sessions.sidebar.session.moveToWorktree.confirm.baseWarning')}</p>
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="neutral"
|
||||
data-session-worktree-move-action="cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('sessions.sidebar.session.moveToWorktree.confirm.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
data-session-worktree-move-action="all-changes"
|
||||
onClick={onMoveAllChanges}
|
||||
>
|
||||
{t('sessions.sidebar.session.moveToWorktree.confirm.allChanges')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
autoFocus
|
||||
data-session-worktree-move-action="session-only"
|
||||
onClick={onMoveSessionOnly}
|
||||
>
|
||||
{t('sessions.sidebar.session.moveToWorktree.confirm.sessionOnly')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user