Files
openchamber/packages/ui/src/components/session/sidebar/SessionWorktreeMoveConfirmDialog.test.tsx
T
Bohdan Triapitsyn 4f53db17e6 fix(worktrees): protect user changes across ambiguous move failures
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
2026-08-28 12:03:55 +03:00

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"');
});
});