Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
Expand Up @@ -34,7 +34,7 @@ describe('T084 (rendered) — voice-mode image journey (STT → route → image
await h.voiceSend('draw a dog');

// The image generation ran...
await h.rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage.length).toBe(1); }, { timeout: 6000 });
await h.rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage).toHaveLength(1); }, { timeout: 6000 });
// ...and the generated image renders on screen (the terminal artifact the user sees).
await h.rtl.waitFor(() => { expect(h.view!.queryByTestId('generated-image')).not.toBeNull(); }, { timeout: 6000 });
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('T062 (voice + enhancement) — resend of an enhanced image request re-
// then the image is drawn.
h.boundary.litert.scriptTurn({ content: 'a photorealistic dog in a park' });
await h.voiceSend('draw a dog');
await h.rtl.waitFor(() => { expect(h.view!.queryAllByTestId('generated-image-content').length).toBe(1); }, { timeout: 6000 });
await h.rtl.waitFor(() => { expect(h.view!.queryAllByTestId('generated-image-content')).toHaveLength(1); }, { timeout: 6000 });

// RESEND via the real action menu (3-dots) on the image-result message → Retry. Regenerate REPLACES the
// reply, so a correct re-draw leaves one rendered image; a misroute-to-text would leave ZERO (the image
Expand All @@ -53,7 +53,7 @@ describe('T062 (voice + enhancement) — resend of an enhanced image request re-

// SPEC: the resend re-ran the IMAGE pipeline (a second generateImage) AND the user still sees a rendered
// image (not a text answer). RED (B33) would be zero rendered images + the text answer.
expect(h.boundary.diffusion.calls.generateImage.length).toBe(2);
expect(h.view!.queryAllByTestId('generated-image-content').length).toBe(1);
expect(h.boundary.diffusion.calls.generateImage).toHaveLength(2);
expect(h.view!.queryAllByTestId('generated-image-content')).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('T062 (voice-mode) — resend of an image request re-draws, not text (D
// Switch to Voice mode via the chat-input quick-settings, then VOICE-send "draw a dog" → IMAGE.
await h.enterVoiceMode();
await h.voiceSend('draw a dog');
await h.rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage.length).toBe(1); }, { timeout: 6000 });
await h.rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage).toHaveLength(1); }, { timeout: 6000 });

// RESEND via the real action menu (3-dots) on the image-result message → Retry. In audio mode the image
// result renders as the same core ChatMessage, so the action menu is identical to text mode.
Expand All @@ -52,7 +52,7 @@ describe('T062 (voice-mode) — resend of an image request re-draws, not text (D

// SPEC: resend re-runs the IMAGE pipeline → a SECOND generateImage; NO text answer leaked.
// RED (B33 in voice mode): resend goes to the text model → generateImage stays 1 + the scripted text renders.
expect(h.boundary.diffusion.calls.generateImage.length).toBe(2);
expect(h.boundary.diffusion.calls.generateImage).toHaveLength(2);
expect(h.view!.queryByText(/domestic animal/)).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('chat-mode STT is dictation-to-the-input-box on every engine (LiteRT to
expect(transcriptArgs.some(t => t.trim() === 'draw a dog')).toBe(true);
// And it is NOT dispatched as a voice-note attachment, nor auto-sent — the user reviews/edits then sends.
// RED before the fix: the transcript went to onAudioAttachment (a dispatched note), composer stayed empty.
expect(attachmentArgs.length).toBe(0);
expect(autoSendArgs.length).toBe(0);
expect(attachmentArgs).toHaveLength(0);
expect(autoSendArgs).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('T012 (rendered) — ModelsScreen reflects N downloaded models', () =>

// The count of downloaded marks the user sees on ModelsScreen must equal N.
await waitFor(() => {
expect(view.queryAllByTestId(/^model-card-\d+-downloaded$/).length).toBe(N);
expect(view.queryAllByTestId(/^model-card-\d+-downloaded$/)).toHaveLength(N);
}, { timeout: 4000 });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe.each(['ios', 'android'] as const)(
};

// Precondition: no native download in flight, and the row is a dead 'failed' (the screenshot state).
expect(boundary.download!.active().length).toBe(0);
expect(boundary.download!.active()).toHaveLength(0);
expect(useDownloadStore.getState().downloads[modelKey].status).toBe('failed');

// ACT: the retry/resume-finalize path for an all-bytes-present image download.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('rendered — iOS image staging purged: Retry recovers the failed card'
return btn;
});
expect(view.queryByText(/SDXL|coreml_apple/)).not.toBeNull();
expect(boundary.download!.active().length).toBe(0);
expect(boundary.download!.active()).toHaveLength(0);

// GESTURE: tap Retry, the way the user did on the device.
fireEvent.press(retry!);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('D4 — iOS interrupted download leaves no failed entry (red-flow)', ()

boundary.download!.seedActive({ downloadId: 'dl-txt', fileName: 'gemma-4b.gguf', modelId: 'gemma-4b', modelType: 'text', status: 'running', bytesDownloaded: 2 * 1024 ** 3, totalBytes: 6 * 1024 ** 3 });
await hydrateDownloadStore();
expect(Object.keys(useDownloadStore.getState().downloads).length).toBe(1); // precondition: shown while running
expect(Object.keys(useDownloadStore.getState().downloads)).toHaveLength(1); // precondition: shown while running

// iOS force-quit: URLSession drops the row entirely.
boundary.download!.simulateRelaunch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('iOS text retry (rendered, red-flow)', () => {
await waitFor(() => { expect(view.queryByText(/model\.gguf/)).not.toBeNull(); });

// Pre-condition: nothing is downloading at the native boundary yet (so a false green can't hide).
expect(boundary.download!.active().length).toBe(0);
expect(boundary.download!.active()).toHaveLength(0);

fireEvent.press(view.getByTestId('failed-retry-button'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe('queued downloads survive an app kill (red-flow)', () => {
// so a second kill right now would not lose them (the found=11 → found=1 loss).
await flush();
const persisted = await loadQueuedDownloads();
expect(persisted.length).toBe(3);
expect(persisted).toHaveLength(3);
});

it('no regression: an in-flight (started) download still hydrates via the native path', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe('iOS text retry re-issues a rehydrated failed download that lost its do
await waitFor(() => expect(getByText('Retry')).toBeTruthy(), { timeout: 4000 });

// No native download exists yet (the row was lost on the kill).
expect(boundary.download!.active().length).toBe(0);
expect(boundary.download!.active()).toHaveLength(0);

// Tap Retry.
await act(async () => { fireEvent.press(getByText('Retry')); });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('T071 (rendered) — prompt enhancement must not think (DEV-B30)', () =

h.boundary.llama!.scriptCompletion({ text: 'a photorealistic cat in a garden' }); // the rewritten prompt
await h.tapSend('draw a cat');
await h.rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage.length).toBe(1); }, { timeout: 6000 });
await h.rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage).toHaveLength(1); }, { timeout: 6000 });

// The enhancement completion reached the text engine.
const enhancementReq = h.boundary.llama!.calls.completion.map(c => c[0] as { enable_thinking?: boolean; messages?: Array<{ role: string; content?: string }> }).find(isEnhancementRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('T073 (rendered) — enhancement must stream / show live progress (DEV-
// Release the held stream so the turn completes cleanly (no dangling promise / open handle).
h.boundary.llama!.releaseStream();
await h.rtl.waitFor(
() => { expect(h.boundary.diffusion.calls.generateImage.length).toBe(1); },
() => { expect(h.boundary.diffusion.calls.generateImage).toHaveLength(1); },
{ timeout: 6000 },
);
});
Expand Down
4 changes: 2 additions & 2 deletions __tests__/integration/generation/generationFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describe('Generation Flow Integration', () => {
// Verify message was finalized
const chatState = getChatState();
expect(chatState.streamingMessage).toBe('');
expect(chatState.streamingForConversationId).toBe(null);
expect(chatState.streamingForConversationId).toBeNull();
expect(chatState.isStreaming).toBe(false);

// Verify assistant message was added
Expand Down Expand Up @@ -368,7 +368,7 @@ describe('Generation Flow Integration', () => {
// Verify streaming state was cleared
const chatState = getChatState();
expect(chatState.streamingMessage).toBe('');
expect(chatState.streamingForConversationId).toBe(null);
expect(chatState.streamingForConversationId).toBeNull();
expect(chatState.isStreaming).toBe(false);
});
});
Expand Down
90 changes: 35 additions & 55 deletions __tests__/integration/generation/imageGenerationFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ describe('Image Generation Flow Integration', () => {

// Should have: system + context messages + user enhance prompt
// system (1) + conversation messages (3) + user enhance (1) = 5
expect(enhancementMessages.length).toBe(5);
expect(enhancementMessages).toHaveLength(5);
expect(enhancementMessages[0].role).toBe('system');
expect(enhancementMessages[0].content).toContain('conversation history');
expect(enhancementMessages[1].content).toBe('Draw me a cat');
Expand All @@ -709,7 +709,7 @@ describe('Image Generation Flow Integration', () => {
const enhancementMessages = callArgs[0] as Message[];

// Should have: system + user enhance prompt only (no context)
expect(enhancementMessages.length).toBe(2);
expect(enhancementMessages).toHaveLength(2);
expect(enhancementMessages[0].role).toBe('system');
expect(enhancementMessages[0].content).not.toContain('conversation history');
expect(enhancementMessages[1].role).toBe('user');
Expand All @@ -736,7 +736,7 @@ describe('Image Generation Flow Integration', () => {
// The context message should be truncated to 500 chars
const contextMsg = enhancementMessages.find(m => m.id.startsWith('ctx-'));
expect(contextMsg).toBeDefined();
expect(contextMsg!.content.length).toBe(500);
expect(contextMsg!.content).toHaveLength(500);
});

it('should limit conversation context to last 10 messages', async () => {
Expand All @@ -761,7 +761,7 @@ describe('Image Generation Flow Integration', () => {
const enhancementMessages = callArgs[0] as Message[];

// system (1) + last 10 context messages + user enhance (1) = 12
expect(enhancementMessages.length).toBe(12);
expect(enhancementMessages).toHaveLength(12);
// First context message should be message 6 (index 5), not message 1
const firstContextMsg = enhancementMessages[1];
expect(firstContextMsg.content).toBe('Message 6');
Expand All @@ -786,7 +786,7 @@ describe('Image Generation Flow Integration', () => {
const enhancementMessages = callArgs[0] as Message[];

// system (1) + 2 context (user + assistant, system skipped) + user enhance (1) = 4
expect(enhancementMessages.length).toBe(4);
expect(enhancementMessages).toHaveLength(4);
const contextMessages = enhancementMessages.filter(m => m.id.startsWith('ctx-'));
expect(contextMessages).toHaveLength(2);
expect(contextMessages.every(m => m.role !== 'system')).toBe(true);
Expand Down Expand Up @@ -835,7 +835,7 @@ describe('Image Generation Flow Integration', () => {
const enhancementMessages = callArgs[0] as Message[];

// system + user enhance only (no context from empty conversation)
expect(enhancementMessages.length).toBe(2);
expect(enhancementMessages).toHaveLength(2);
expect(enhancementMessages[0].role).toBe('system');
expect(enhancementMessages[0].content).not.toContain('conversation history');
});
Expand Down Expand Up @@ -1180,62 +1180,42 @@ describe('Image Generation Flow Integration', () => {
mockLlmService.isCurrentlyGenerating.mockReturnValue(false);
};

it('should strip <think> tags from thinking model responses', async () => {
it.each([
{
name: 'should strip <think> tags from thinking model responses',
// Simulate a thinking model that wraps reasoning in <think> tags
inputPrompt: 'sunset over mountains',
enhancedResponse:
'<think>Let me enhance this prompt by adding artistic details...</think>A majestic sunset over mountains, golden hour lighting, oil painting style',
// The prompt passed to image generation should NOT contain <think> tags
expectedPrompt: 'A majestic sunset over mountains, golden hour lighting, oil painting style',
},
{
name: 'should handle thinking model response that is only a think block',
// Simulate a model that only outputs thinking with no actual response
inputPrompt: 'a cat',
enhancedResponse: '<think>I need to think about how to enhance this prompt...</think>',
// When stripping produces empty string, should fall back to original prompt
expectedPrompt: 'a cat',
},
{
name: 'should handle response without think tags normally',
// Non-thinking model returns plain enhanced prompt
inputPrompt: 'simple prompt',
enhancedResponse: 'A beautiful enhanced prompt with details',
expectedPrompt: 'A beautiful enhanced prompt with details',
},
])('$name', async ({ inputPrompt, enhancedResponse, expectedPrompt }) => {
setupThinkingModelEnhancement();
// Simulate a thinking model that wraps reasoning in <think> tags
mockLlmService.generateResponse.mockResolvedValue(
'<think>Let me enhance this prompt by adding artistic details...</think>A majestic sunset over mountains, golden hour lighting, oil painting style'
);

await imageGenerationService.generateImage({
prompt: 'sunset over mountains',
});

// The prompt passed to image generation should NOT contain <think> tags
expect(mockLocalDreamService.generateImage).toHaveBeenCalledWith(
expect.objectContaining({
prompt: 'A majestic sunset over mountains, golden hour lighting, oil painting style',
}),
expect.any(Function),
expect.any(Function),
);
});

it('should handle thinking model response that is only a think block', async () => {
setupThinkingModelEnhancement();
// Simulate a model that only outputs thinking with no actual response
mockLlmService.generateResponse.mockResolvedValue(
'<think>I need to think about how to enhance this prompt...</think>'
);

await imageGenerationService.generateImage({
prompt: 'a cat',
});

// When stripping produces empty string, should fall back to original prompt
expect(mockLocalDreamService.generateImage).toHaveBeenCalledWith(
expect.objectContaining({
prompt: 'a cat',
}),
expect.any(Function),
expect.any(Function),
);
});

it('should handle response without think tags normally', async () => {
setupThinkingModelEnhancement();
// Non-thinking model returns plain enhanced prompt
mockLlmService.generateResponse.mockResolvedValue(
'A beautiful enhanced prompt with details'
);
mockLlmService.generateResponse.mockResolvedValue(enhancedResponse);

await imageGenerationService.generateImage({
prompt: 'simple prompt',
prompt: inputPrompt,
});

expect(mockLocalDreamService.generateImage).toHaveBeenCalledWith(
expect.objectContaining({
prompt: 'A beautiful enhanced prompt with details',
prompt: expectedPrompt,
Comment on lines +1183 to +1218

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Delete this mockist test rather than repairing it.

As per coding guidelines for __tests__/**/*.{ts,tsx}, you should delete mockist tests that mock application code or assert toHaveBeenCalled rather than repairing or parameterizing them. Tests should instead be rendered integration tests that mount the screen and assert what the user sees.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/integration/generation/imageGenerationFlow.test.ts` around lines
1183 - 1218, Delete the parameterized mockist test block around the
thinking-model prompt cases, including its mocked generateResponse setup and
toHaveBeenCalled assertion. Do not replace or repair it; follow the project
guideline that tests under __tests__ should use rendered integration behavior
and user-visible assertions instead.

Source: Coding guidelines

}),
expect.any(Function),
expect.any(Function),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('#510 (rendered) — a queued force-image send preserves its force flag
await h.settle(50); // let handleSendFn enqueue

// No image generated yet — turn #2 is queued, turn #1 still holds.
expect(h.boundary.diffusion.calls.generateImage.length).toBe(0);
expect(h.boundary.diffusion.calls.generateImage).toHaveLength(0);

// ---- Release turn #1 → the queue drains and dispatches the queued force-image message. ----
h.boundary.llama!.releaseStream();
Expand All @@ -73,10 +73,10 @@ describe('#510 (rendered) — a queued force-image send preserves its force flag
// SPEC: the queued force-image send draws → the generated-image bubble renders on screen, and the
// scripted TEXT reply must NOT appear for turn #2.
// RED (#510): force lost → classified text → QUEUED_TEXT_LEAK renders again as a second reply, no image.
await rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage.length).toBe(1); }, { timeout: 4000 });
await rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage).toHaveLength(1); }, { timeout: 4000 });
expect(view.queryByTestId('generated-image')).not.toBeNull();
// Only turn #1's single text reply may exist — turn #2 must NOT have produced a second text reply.
expect(view.queryAllByText(new RegExp(QUEUED_TEXT_LEAK)).length).toBe(1);
expect(view.queryAllByText(new RegExp(QUEUED_TEXT_LEAK))).toHaveLength(1);
});

it('COALESCE (M16): two sends queue together and one forced image — the merged dispatch draws an image', async () => {
Expand All @@ -101,12 +101,12 @@ describe('#510 (rendered) — a queued force-image send preserves its force flag
await rtl.waitFor(() => { expect(view.queryByTestId('image-mode-force-badge')).not.toBeNull(); });
await h.tapSend('tell me about cats'); // force → queued (now 2 queued, one forced)
await h.settle(50);
expect(h.boundary.diffusion.calls.generateImage.length).toBe(0); // nothing drawn while #1 holds
expect(h.boundary.diffusion.calls.generateImage).toHaveLength(0); // nothing drawn while #1 holds

// Drain: the 2 queued messages coalesce; because one was force, the merged dispatch must draw.
h.boundary.llama!.releaseStream();
await h.settle(400);
await rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage.length).toBe(1); }, { timeout: 4000 });
await rtl.waitFor(() => { expect(h.boundary.diffusion.calls.generateImage).toHaveLength(1); }, { timeout: 4000 });
expect(view.queryByTestId('generated-image')).not.toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('BUG #29(b) — second send while generating surfaces a queued indicato
await handleSendFn(deps, { text: 'second message', startGeneration, setDebugInfo: jest.fn() });
await flushPromises();

expect(generationService.getState().queuedMessages.length).toBe(1);
expect(generationService.getState().queuedMessages).toHaveLength(1);
// The subscription the screen reads now reflects the queued message → QueueRow renders.
expect(queueCount).toBe(1);
expect(queuedTexts).toEqual(['second message']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('reload race — a send during the load window keeps thinking (device 2
expect(h.view!.queryByText(/seventeen has no divisors below its root/)).not.toBeNull();
// BOTH turns carry the thinking affordance (the block collapses to its preview after completion).
const blocks = h.view!.queryAllByTestId('thinking-block');
expect(blocks.length).toBe(2);
expect(blocks).toHaveLength(2);
// Expand the racing turn's block: the full reasoning renders in the block content.
// (walking-up press: the toggle's onPress lives on the composite above the testID host)
const toggles = h.view!.queryAllByTestId('thinking-block-toggle');
Expand Down
Loading
Loading