-
Notifications
You must be signed in to change notification settings - Fork 0
test: increase branch coverage to 78.68% (from 73.53%) #168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
261b579
0e0e431
58d1e41
6eb0cd2
4e3855b
31d714c
a54bb55
e606289
c43d9c3
13ccb8e
0c5fcb2
4b34900
b836c8b
d60535c
9eb1992
65a05d4
45c7d13
78e10a8
a088163
f1dad2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -160,3 +160,4 @@ graphify-out/ | |
| .nanostack/ | ||
| qa/ | ||
| .sisyphus/evidence/ | ||
| .omo/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,34 @@ | ||
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| import type { WorkoutFeedbackOutput } from '@/ai/flows/workout-feedback-flow'; | ||
|
|
||
| const { mockFeedbackPrompt, mockDefinePrompt, mockDefineFlow, mockSetAttribute } = vi.hoisted( | ||
| () => { | ||
| const mockFeedbackPrompt = vi.fn(); | ||
| const mockDefinePrompt = vi.fn((_config: unknown) => mockFeedbackPrompt); | ||
| const mockDefineFlow = vi.fn((_config: unknown, handler: unknown) => handler); | ||
| const mockSetAttribute = vi.fn(); | ||
| return { mockFeedbackPrompt, mockDefinePrompt, mockDefineFlow, mockSetAttribute }; | ||
| } | ||
| ); | ||
|
|
||
| vi.mock('@/ai/genkit', () => ({ | ||
| ai: { | ||
| definePrompt: vi.fn(), | ||
| defineFlow: vi.fn(), | ||
| definePrompt: mockDefinePrompt, | ||
| defineFlow: mockDefineFlow, | ||
| }, | ||
| })); | ||
|
|
||
| vi.mock('@genkit-ai/google-genai', () => ({ | ||
| googleAI: { model: vi.fn(() => 'gemini-2.5-flash') }, | ||
| })); | ||
|
|
||
| vi.mock('@/ai/flows/workout-feedback-flow', async () => { | ||
| return { | ||
| generateWorkoutFeedback: vi.fn(), | ||
| }; | ||
| }); | ||
| vi.mock('@sentry/nextjs', () => ({ | ||
| startSpan: vi.fn( | ||
| async ( | ||
| _config: unknown, | ||
| fn: (span: { setAttribute: typeof mockSetAttribute }) => Promise<unknown> | ||
| ) => fn({ setAttribute: mockSetAttribute }) | ||
| ), | ||
| })); | ||
|
|
||
| import { generateWorkoutFeedback } from '@/ai/flows/workout-feedback-flow'; | ||
|
|
||
|
|
@@ -26,25 +38,23 @@ describe('generateWorkoutFeedback', () => { | |
| }); | ||
|
|
||
| it('returns fallback when no exercises completed (no AI call)', async () => { | ||
| vi.mocked(generateWorkoutFeedback).mockResolvedValue({ | ||
| title: 'O primeiro passo é o mais importante!', | ||
| message: expect.any(String) as unknown as string, | ||
| }); | ||
|
|
||
| const result = await generateWorkoutFeedback({ | ||
| goal: 'Hipertrofia', | ||
| completedExercises: [], | ||
| totalExercises: 3, | ||
| }); | ||
|
|
||
| expect(result.title).toBe('O primeiro passo é o mais importante!'); | ||
| expect(result.message).toMatch(/nenhum exercício/i); | ||
| expect(mockFeedbackPrompt).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('returns AI feedback when exercises are completed', async () => { | ||
| vi.mocked(generateWorkoutFeedback).mockResolvedValue({ | ||
| it('returns AI feedback when exercises completed', async () => { | ||
| const aiResult = { | ||
| title: 'Mandou bem!', | ||
| message: 'Excelente sessão. Continue assim!', | ||
| }); | ||
| }; | ||
| mockFeedbackPrompt.mockResolvedValue({ output: aiResult }); | ||
|
|
||
| const result = await generateWorkoutFeedback({ | ||
| goal: 'Hipertrofia', | ||
|
|
@@ -53,30 +63,19 @@ describe('generateWorkoutFeedback', () => { | |
| }); | ||
|
|
||
| expect(result.title).toBe('Mandou bem!'); | ||
| expect(result.message).toBeTruthy(); | ||
| expect(result.message).toBe('Excelente sessão. Continue assim!'); | ||
| expect(mockFeedbackPrompt).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('propagates errors from AI call', async () => { | ||
| vi.mocked(generateWorkoutFeedback).mockRejectedValue(new Error('AI unavailable')); | ||
| it('throws when AI returns null output', async () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Pragmatic Test Coverage The updated test suite for Prompt for AI agents |
||
| mockFeedbackPrompt.mockResolvedValue({ output: null }); | ||
|
|
||
| await expect( | ||
| generateWorkoutFeedback({ | ||
| goal: 'Força', | ||
| completedExercises: ['Agachamento'], | ||
| goal: 'Hipertrofia', | ||
| completedExercises: ['Supino Reto'], | ||
| totalExercises: 1, | ||
| }) | ||
| ).rejects.toThrow('AI unavailable'); | ||
| }); | ||
| }); | ||
|
|
||
| // Integration contract: WorkoutSession must handle the rejection gracefully | ||
| describe('WorkoutSession feedback contract', () => { | ||
| it('fallback feedback shape is valid', () => { | ||
| const fallback: WorkoutFeedbackOutput = { | ||
| title: 'Treino Concluído!', | ||
| message: 'Continue assim!', | ||
| }; | ||
| expect(fallback.title).toBeTruthy(); | ||
| expect(fallback.message).toBeTruthy(); | ||
| ).rejects.toThrow('workoutFeedbackFlow: output is null'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,14 @@ | ||||||||||
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||||||||||
| import { z } from 'zod'; | ||||||||||
|
|
||||||||||
| const mockGenerate = vi.fn(); | ||||||||||
| const mockGenerateStream = vi.fn(); | ||||||||||
| const mockDefineFlow = vi.fn((_config: unknown, handler: unknown) => handler); | ||||||||||
| const mockSetAttribute = vi.fn(); | ||||||||||
| const mockSpan = { setAttribute: mockSetAttribute }; | ||||||||||
| const { mockGenerate, mockGenerateStream, mockDefineFlow, mockSpan } = vi.hoisted(() => { | ||||||||||
| const mockGenerate = vi.fn(); | ||||||||||
| const mockGenerateStream = vi.fn(); | ||||||||||
| const mockDefineFlow = vi.fn((_config: unknown, handler: unknown) => handler); | ||||||||||
| const mockSetAttribute = vi.fn(); | ||||||||||
| const mockSpan = { setAttribute: mockSetAttribute }; | ||||||||||
| return { mockGenerate, mockGenerateStream, mockDefineFlow, mockSetAttribute, mockSpan }; | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| vi.mock('@/ai/genkit', () => ({ | ||||||||||
| ai: { | ||||||||||
|
|
@@ -151,6 +154,122 @@ describe('streamWorkoutPlan', () => { | |||||||||
| )(validInput, { sendChunk }) | ||||||||||
| ).rejects.toThrow('A IA não retornou um plano de treino válido.'); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| it('uses observacoesAdicionais when provided', async () => { | ||||||||||
| mockGenerateStream.mockReturnValue({ | ||||||||||
| stream: (async function* () { | ||||||||||
| yield { output: { planName: 'Plano', workouts: [] } }; | ||||||||||
| })(), | ||||||||||
| response: Promise.resolve({ | ||||||||||
| output: { | ||||||||||
| planName: 'Plano', | ||||||||||
| workouts: [], | ||||||||||
| }, | ||||||||||
| usage: { inputTokens: 10, outputTokens: 20 }, | ||||||||||
| }), | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const { streamWorkoutPlan } = await import('@/ai/flows/workout-generator-flow'); | ||||||||||
| const sendChunk = vi.fn(); | ||||||||||
| const inputWithObs = { ...validInput, observacoesAdicionais: 'Lesão no joelho' }; | ||||||||||
|
|
||||||||||
| const result = await ( | ||||||||||
| streamWorkoutPlan as unknown as ( | ||||||||||
| input: typeof inputWithObs, | ||||||||||
| ctx: { sendChunk: (chunk: unknown) => void } | ||||||||||
| ) => Promise<unknown> | ||||||||||
| )(inputWithObs, { sendChunk }); | ||||||||||
|
|
||||||||||
| expect(result).toBeDefined(); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Pragmatic Test Coverage The test 'handles missing usage stats with nullish coalescing' adds branch coverage for the null usage path but does not actually verify the nullish coalescing behavior it claims to test. The implementation defaults missing usage stats to 0 via ?? 0, but the assertion only checks expect(result).toBeDefined(). Consider asserting that the span attributes received the correct default values (e.g., by verifying mockSetAttribute was called with the coalesced token values of 0), to validate the actual behavioral outcome rather than just that the function returned a value. Prompt for AI agents
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Pragmatic Test Coverage The test 'uses observacoesAdicionais when provided' only asserts that the function returned a defined value, without verifying that the observation was actually included in the AI prompt or affected the output. For a test named to suggest it validates the use of the observation, a meaningful assertion would be to check that Prompt for AI agents |
||||||||||
| }); | ||||||||||
|
|
||||||||||
| it('handles missing usage stats with nullish coalescing', async () => { | ||||||||||
| const chunkOutput = { | ||||||||||
| planName: 'Plano Teste', | ||||||||||
| workouts: [], | ||||||||||
| }; | ||||||||||
| mockGenerateStream.mockReturnValue({ | ||||||||||
| stream: (async function* () { | ||||||||||
| yield { output: chunkOutput }; | ||||||||||
| })(), | ||||||||||
| response: Promise.resolve({ | ||||||||||
| output: { | ||||||||||
| planName: 'Plano Teste', | ||||||||||
| workouts: [ | ||||||||||
| { | ||||||||||
| nome: 'Treino A', | ||||||||||
| objetivo: 'Hipertrofia', | ||||||||||
| diaSugerido: 1, | ||||||||||
| exercicios: [ | ||||||||||
| { | ||||||||||
| nomeExercicio: 'Supino Reto', | ||||||||||
| grupoMuscular: 'Peito', | ||||||||||
| series: 3, | ||||||||||
| repeticoes: '10-12', | ||||||||||
| observacoes: '', | ||||||||||
| }, | ||||||||||
| ], | ||||||||||
| }, | ||||||||||
| ], | ||||||||||
| }, | ||||||||||
| usage: null, | ||||||||||
| }), | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const { streamWorkoutPlan } = await import('@/ai/flows/workout-generator-flow'); | ||||||||||
| const sendChunk = vi.fn(); | ||||||||||
|
|
||||||||||
| const result = await ( | ||||||||||
| streamWorkoutPlan as unknown as ( | ||||||||||
| input: typeof validInput, | ||||||||||
| ctx: { sendChunk: (chunk: unknown) => void } | ||||||||||
| ) => Promise<unknown> | ||||||||||
| )(validInput, { sendChunk }); | ||||||||||
|
|
||||||||||
| expect(result).toBeDefined(); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| it('throws when AI output fails schema validation', async () => { | ||||||||||
| mockGenerateStream.mockReturnValue({ | ||||||||||
| stream: (async function* () { | ||||||||||
| yield {}; | ||||||||||
| })(), | ||||||||||
| response: Promise.resolve({ | ||||||||||
| output: { | ||||||||||
| planName: 123, | ||||||||||
| workouts: [ | ||||||||||
| { | ||||||||||
| nome: 'A', | ||||||||||
| objetivo: 'Força', | ||||||||||
| diaSugerido: 1, | ||||||||||
| exercicios: [ | ||||||||||
| { | ||||||||||
| nomeExercicio: 'Supino', | ||||||||||
| grupoMuscular: 'Peito', | ||||||||||
| series: 3, | ||||||||||
| repeticoes: '10', | ||||||||||
| observacoes: '', | ||||||||||
| }, | ||||||||||
| ], | ||||||||||
| }, | ||||||||||
| ], | ||||||||||
| }, | ||||||||||
| usage: { inputTokens: 10, outputTokens: 20 }, | ||||||||||
| }), | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const { streamWorkoutPlan } = await import('@/ai/flows/workout-generator-flow'); | ||||||||||
| const sendChunk = vi.fn(); | ||||||||||
|
|
||||||||||
| await expect( | ||||||||||
| ( | ||||||||||
| streamWorkoutPlan as unknown as ( | ||||||||||
| input: typeof validInput, | ||||||||||
| ctx: { sendChunk: (chunk: unknown) => void } | ||||||||||
| ) => Promise<unknown> | ||||||||||
| )(validInput, { sendChunk }) | ||||||||||
| ).rejects.toThrow('A IA não retornou um plano de treino válido.'); | ||||||||||
| }); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| describe('generateWorkoutPlan', () => { | ||||||||||
|
|
@@ -190,6 +309,18 @@ describe('generateWorkoutPlan', () => { | |||||||||
| expect(mockGenerate).toHaveBeenCalled(); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| it('handles undefined usage stats in generateWorkoutPlan', async () => { | ||||||||||
| mockGenerate.mockResolvedValue({ | ||||||||||
| output: null, | ||||||||||
| usage: null, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const { generateWorkoutPlan } = await import('@/ai/flows/workout-generator-flow'); | ||||||||||
| await expect(generateWorkoutPlan(validInput)).rejects.toThrow( | ||||||||||
| 'A IA não retornou um plano de treino válido.' | ||||||||||
| ); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| it('throws when output is null', async () => { | ||||||||||
| mockGenerate.mockResolvedValue({ | ||||||||||
| output: null, | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Removing
override: truefromdotenv.config()weakens E2E test isolation by allowing any preloaded shell or system environment variables to take precedence over.env.test. In local development, this means a developer with production or staging credentials in their shell (e.g., fromsource .env.local) could silently run E2E tests against the wrong backend, causing flaky failures or unintended data writes.Additionally,
tests/e2e/global-setup.tsstill loads.env.testwithoverride: true, whileplaywright.config.tsnow loads it without. BecausewebServer.envis evaluated at config load time—beforeglobalSetupmutatesprocess.env—the web server and the E2E seed script can end up with different environment values, creating a database-credential mismatch between the app and its seed data.Consider keeping
override: trueto maintain.env.testas the baseline, and selectively restoring only the CI dynamic Supabase keys afterward instead of removing override globally.Prompt for AI agents