Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { CreatedFilesList } from '../../../../../renderer/components/InlineWizard/DocumentGenerationView/components';
import { mockTheme } from '../../../../helpers/mockTheme';

const firstDoc = {
filename: 'Phase-01-Setup.md',
content: '# Setup\n\nFirst paragraph.\n\n- [ ] Task',
taskCount: 1,
};

const secondDoc = {
filename: 'Phase-02-Build.md',
content: '# Build\n\nSecond paragraph.\n\n- [ ] Task',
taskCount: 1,
};

function getDescriptionPanel(text: string): HTMLElement {
const description = screen.getByText(text);
const panel = description.parentElement;
if (!panel) throw new Error('Missing description panel');
return panel;
}

describe('CreatedFilesList', () => {
it('auto-expands the newest file when it is added', () => {
const { rerender } = render(<CreatedFilesList documents={[firstDoc]} theme={mockTheme} />);

rerender(<CreatedFilesList documents={[firstDoc, secondDoc]} theme={mockTheme} />);

expect(getDescriptionPanel('Second paragraph.')).toHaveStyle({ maxHeight: '120px' });
});

it('preserves a user-expanded file when a newer file appears', () => {
const { rerender } = render(<CreatedFilesList documents={[firstDoc]} theme={mockTheme} />);

fireEvent.click(screen.getByRole('button', { name: /phase-01-setup/i }));
rerender(<CreatedFilesList documents={[firstDoc, secondDoc]} theme={mockTheme} />);

expect(getDescriptionPanel('First paragraph.')).toHaveStyle({ maxHeight: '120px' });
expect(getDescriptionPanel('Second paragraph.')).toHaveStyle({ maxHeight: '120px' });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { fireEvent, render, screen } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { DocumentGenerationView } from '../../../../../renderer/components/InlineWizard/DocumentGenerationView';
import { mockTheme } from '../../../../helpers/mockTheme';

const generatedDoc = {
filename: 'Phase-01-Setup.md',
content: '# Setup\n\nPlan the setup.\n\n- [ ] Create project\n- [x] Verify project',
taskCount: 2,
};

function renderView(overrides: Partial<ComponentProps<typeof DocumentGenerationView>> = {}) {
return render(
<DocumentGenerationView
theme={mockTheme}
documents={[]}
currentDocumentIndex={0}
isGenerating={false}
onComplete={vi.fn()}
onDocumentSelect={vi.fn()}
{...overrides}
/>
);
}

describe('DocumentGenerationView', () => {
it('renders the empty state and cancel action', () => {
const onCancel = vi.fn();
renderView({ onCancel });

expect(screen.getByText('No documents generated yet.')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCancel).toHaveBeenCalledTimes(1);
});

it('renders the generating state with cancel action', () => {
const onCancel = vi.fn();
renderView({ isGenerating: true, onCancel });

expect(screen.getByText('Generating Auto Run Documents...')).toBeInTheDocument();
expect(screen.getByText(/This may take a while/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCancel).toHaveBeenCalledTimes(1);
});

it('renders the complete state with task totals and saved location', () => {
renderView({
documents: [generatedDoc],
subfolderName: 'Generated-Plan',
});

expect(screen.getByText('Documentation generation complete.')).toBeInTheDocument();
expect(screen.getByText('Generated-Plan/')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
expect(screen.getByText('Tasks Planned')).toBeInTheDocument();
expect(screen.getByText('Work Plans Drafted (1)')).toBeInTheDocument();
});

it('calls completion actions', () => {
const onComplete = vi.fn();
const onCompleteAndStartAutoRun = vi.fn();
renderView({
documents: [generatedDoc],
onComplete,
onCompleteAndStartAutoRun,
});

fireEvent.click(screen.getByRole('button', { name: 'Exit Wizard' }));
fireEvent.click(screen.getByRole('button', { name: 'Start Auto Run' }));

expect(onComplete).toHaveBeenCalledTimes(1);
expect(onCompleteAndStartAutoRun).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useElapsedGenerationTime } from '../../../../../renderer/components/InlineWizard/DocumentGenerationView/hooks/useElapsedGenerationTime';

describe('useElapsedGenerationTime', () => {
afterEach(() => {
vi.useRealTimers();
});

it('uses the persisted start timestamp while generating', () => {
vi.useFakeTimers();
vi.setSystemTime(10_000);

const { result } = renderHook(() => useElapsedGenerationTime(true, 7_000));

expect(result.current).toBe(3_000);

act(() => {
vi.advanceTimersByTime(1_000);
});

expect(result.current).toBe(4_000);
});

it('does not start an interval after generation is complete', () => {
vi.useFakeTimers();
vi.setSystemTime(10_000);

const { result } = renderHook(() => useElapsedGenerationTime(false, 7_000));

act(() => {
vi.advanceTimersByTime(5_000);
});

expect(result.current).toBe(3_000);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import {
countTasks,
countTotalTasks,
extractDocumentDescription,
} from '../../../../../renderer/components/InlineWizard/DocumentGenerationView/utils/documentStats';

describe('DocumentGenerationView document stats', () => {
it('counts simple markdown task rows', () => {
expect(countTasks('- [ ] One\n- [x] Two\n- [X] ignored by legacy UI regex')).toBe(2);
});

it('counts total tasks across documents', () => {
expect(
countTotalTasks([
{ filename: 'a.md', content: '- [ ] One', taskCount: 1 },
{ filename: 'b.md', content: '- [ ] Two\n- [x] Three', taskCount: 2 },
])
).toBe(3);
});

it('extracts the first non-heading non-list paragraph', () => {
expect(extractDocumentDescription('# Title\n\n- [ ] Task\n\nFirst useful paragraph.')).toBe(
'First useful paragraph.'
);
});

it('truncates long descriptions', () => {
const description = extractDocumentDescription('a'.repeat(160));
expect(description).toHaveLength(150);
expect(description?.endsWith('...')).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { render, screen } from '@testing-library/react';
import { createRef } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { mockTheme } from '../../../../helpers/mockTheme';

vi.mock('../../../../../renderer/components/Wizard/shared/DocumentSelector', () => ({
DocumentSelector: ({
className,
showTaskCounts,
selectedIndex,
}: {
className?: string;
showTaskCounts?: boolean;
selectedIndex: number;
}) => (
<div
data-testid="shared-selector"
data-class-name={className}
data-show-task-counts={String(showTaskCounts)}
data-selected-index={selectedIndex}
/>
),
}));

vi.mock('../../../../../renderer/components/Wizard/shared/DocumentEditor', () => ({
DocumentEditor: ({
folderPath,
selectedFile,
showHeader,
proseClassPrefix,
}: {
folderPath: string;
selectedFile: string;
showHeader: boolean;
proseClassPrefix: string;
}) => (
<div
data-testid="shared-editor"
data-folder-path={folderPath}
data-selected-file={selectedFile}
data-show-header={String(showHeader)}
data-prose-class-prefix={proseClassPrefix}
/>
),
}));

import {
DocumentEditor,
DocumentSelector,
} from '../../../../../renderer/components/InlineWizard/DocumentGenerationView';

describe('DocumentGenerationView legacy wrappers', () => {
it('delegates DocumentSelector to the shared selector with task counts enabled', () => {
render(
<DocumentSelector
documents={[{ filename: 'Phase.md', content: '- [ ] Task', taskCount: 1 }]}
selectedIndex={0}
onSelect={vi.fn()}
theme={mockTheme}
/>
);

const selector = screen.getByTestId('shared-selector');
expect(selector).toHaveAttribute('data-class-name', 'flex-1 min-w-0');
expect(selector).toHaveAttribute('data-show-task-counts', 'true');
expect(selector).toHaveAttribute('data-selected-index', '0');
});

it('delegates DocumentEditor to the shared editor with hidden header defaults', () => {
render(
<DocumentEditor
content="# Plan"
onContentChange={vi.fn()}
mode="edit"
onModeChange={vi.fn()}
folderPath="/docs"
selectedFile="Phase.md"
attachments={[]}
onAddAttachment={vi.fn()}
onRemoveAttachment={vi.fn()}
theme={mockTheme}
isLocked={false}
textareaRef={createRef<HTMLTextAreaElement>()}
previewRef={createRef<HTMLDivElement>()}
/>
);

const editor = screen.getByTestId('shared-editor');
expect(editor).toHaveAttribute('data-folder-path', '/docs');
expect(editor).toHaveAttribute('data-selected-file', 'Phase.md');
expect(editor).toHaveAttribute('data-show-header', 'false');
expect(editor).toHaveAttribute('data-prose-class-prefix', 'doc-gen-view');
});
});
91 changes: 91 additions & 0 deletions src/__tests__/renderer/hooks/batch/inlineWizard/documents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
fetchHistoryFilePath,
hasExistingDocuments,
listExistingDocuments,
loadDocumentContents,
resolveAutoRunFolderPath,
} from '../../../../../renderer/hooks/batch/inlineWizard/documents';

describe('inline wizard document helpers', () => {
beforeEach(() => {
vi.clearAllMocks();
(window.maestro as any).history = {
getFilePath: vi.fn(),
};
});

it('prefers configured Auto Run folder path', () => {
expect(resolveAutoRunFolderPath('/repo', '/custom/playbooks')).toBe('/custom/playbooks');
});

it('falls back to the default playbooks path', () => {
expect(resolveAutoRunFolderPath('/repo')).toBe('/repo/.maestro/playbooks');
});

it('detects whether existing documents are present', async () => {
vi.mocked(window.maestro.autorun.listDocs).mockResolvedValueOnce({
success: true,
files: ['phase-1'],
});

await expect(hasExistingDocuments('/repo/.maestro/playbooks')).resolves.toBe(true);
expect(window.maestro.autorun.listDocs).toHaveBeenCalledWith('/repo/.maestro/playbooks');
});

it('treats list errors as no existing documents', async () => {
vi.mocked(window.maestro.autorun.listDocs).mockRejectedValueOnce(new Error('missing'));

await expect(hasExistingDocuments('/missing')).resolves.toBe(false);
});

it('maps existing document names to markdown paths', async () => {
vi.mocked(window.maestro.autorun.listDocs).mockResolvedValueOnce({
success: true,
files: ['phase-1', 'phase-2'],
});

await expect(listExistingDocuments('/docs')).resolves.toEqual([
{ name: 'phase-1', filename: 'phase-1.md', path: '/docs/phase-1.md' },
{ name: 'phase-2', filename: 'phase-2.md', path: '/docs/phase-2.md' },
]);
});

it('loads document contents and preserves unreadable docs with placeholder content', async () => {
vi.mocked(window.maestro.autorun.readDoc)
.mockResolvedValueOnce({ success: true, content: '# One' })
.mockRejectedValueOnce(new Error('read failed'));

await expect(
loadDocumentContents(
[
{ name: 'phase-1', filename: 'phase-1.md', path: '/docs/phase-1.md' },
{ name: 'phase-2', filename: 'phase-2.md', path: '/docs/phase-2.md' },
],
'/docs'
)
).resolves.toEqual([
{ name: 'phase-1', filename: 'phase-1.md', path: '/docs/phase-1.md', content: '# One' },
{
name: 'phase-2',
filename: 'phase-2.md',
path: '/docs/phase-2.md',
content: '(Failed to load content)',
},
]);
});

it('fetches local history file paths', async () => {
vi.mocked(window.maestro.history.getFilePath).mockResolvedValueOnce('/history/session.jsonl');

await expect(fetchHistoryFilePath('session-1')).resolves.toBe('/history/session.jsonl');
expect(window.maestro.history.getFilePath).toHaveBeenCalledWith('session-1');
});

it('skips history file lookup for SSH sessions', async () => {
await expect(
fetchHistoryFilePath('session-1', { enabled: true, remoteId: 'remote-1' })
).resolves.toBeUndefined();
expect(window.maestro.history.getFilePath).not.toHaveBeenCalled();
});
});
Loading