Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
261b579
test: add meaningful coverage for AI flows, dashboard, login, alunos,…
EmiyaKiritsugu3 Jul 3, 2026
0e0e431
remove: padding UI tests that add no quality value
EmiyaKiritsugu3 Jul 3, 2026
58d1e41
remove: card tests add no quality value
EmiyaKiritsugu3 Jul 3, 2026
6eb0cd2
revert: remove padding tests from combobox, columns, form
EmiyaKiritsugu3 Jul 3, 2026
4e3855b
chore: apply prettier formatting to test files
EmiyaKiritsugu3 Jul 3, 2026
31d714c
fix: add missing beforeEach import and ActionResult data types
EmiyaKiritsugu3 Jul 3, 2026
a54bb55
fix(ci): use npx playwright test directly to avoid .env.test overridi…
EmiyaKiritsugu3 Jul 3, 2026
e606289
fix(e2e): flush session cookie before redirect, increase login timeou…
EmiyaKiritsugu3 Jul 3, 2026
c43d9c3
test: add getSession mock to auth tests after server action fix
EmiyaKiritsugu3 Jul 3, 2026
13ccb8e
test: add getSession mock to auth test helper
EmiyaKiritsugu3 Jul 3, 2026
0c5fcb2
fix(e2e): use authData.session directly instead of calling getSession
EmiyaKiritsugu3 Jul 3, 2026
4b34900
refactor(auth): use client-side routing instead of server-side redirect
EmiyaKiritsugu3 Jul 3, 2026
b836c8b
fix(login): narrow LoginResult union before accessing error
EmiyaKiritsugu3 Jul 3, 2026
d60535c
revert: restore original auth action with server redirect
EmiyaKiritsugu3 Jul 3, 2026
9eb1992
fix(e2e): solve login redirect timeout with revalidatePath + remove d…
EmiyaKiritsugu3 Jul 3, 2026
65a05d4
fix(e2e): flush session cookie before redirect + remove dotenv overri…
EmiyaKiritsugu3 Jul 3, 2026
45c7d13
fix(e2e): client-side redirect for staff login (like ALUNO)
EmiyaKiritsugu3 Jul 3, 2026
78e10a8
fix(e2e): client-side auth with browser client for staff login
EmiyaKiritsugu3 Jul 3, 2026
a088163
fix(e2e): postgrest grants, client-side browser auth, dotenv override
EmiyaKiritsugu3 Jul 3, 2026
f1dad2c
fix(e2e): defer RLS policy to after prisma db push (undefined_table)
EmiyaKiritsugu3 Jul 3, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ jobs:
SUPABASE_SERVICE_ROLE_KEY: ${{ env.SUPABASE_LOCAL_SERVICE_ROLE_KEY }}
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
PLAYWRIGHT_BASE_URL: http://localhost:3333
run: npm run e2e
# Use npx directly, not npm run e2e: dotenv -e .env.test would override
# dynamic Supabase anon key with the static hardcoded key from .env.test.
run: npx playwright test

- name: Upload Playwright report
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,4 @@ graphify-out/
.nanostack/
qa/
.sisyphus/evidence/
.omo/
6 changes: 4 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';

// override: true ensures .env.test values win over any pre-loaded .env.local
dotenv.config({ path: '.env.test', override: true });
// Load .env.test as defaults so local dev matches CI.
// Do NOT use override: true — CI passes dynamic Supabase keys that must
// take priority over the hardcoded demo key in .env.test.
dotenv.config({ path: '.env.test' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Removing override: true from dotenv.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., from source .env.local) could silently run E2E tests against the wrong backend, causing flaky failures or unintended data writes.

Additionally, tests/e2e/global-setup.ts still loads .env.test with override: true, while playwright.config.ts now loads it without. Because webServer.env is evaluated at config load time—before globalSetup mutates process.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: true to maintain .env.test as the baseline, and selectively restoring only the CI dynamic Supabase keys afterward instead of removing override globally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At playwright.config.ts, line 7:

<comment>Removing `override: true` from `dotenv.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., from `source .env.local`) could silently run E2E tests against the wrong backend, causing flaky failures or unintended data writes.

Additionally, `tests/e2e/global-setup.ts` still loads `.env.test` with `override: true`, while `playwright.config.ts` now loads it without. Because `webServer.env` is evaluated at config load time—before `globalSetup` mutates `process.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: true` to maintain `.env.test` as the baseline, and selectively restoring only the CI dynamic Supabase keys afterward instead of removing override globally.</comment>

<file context>
@@ -1,8 +1,10 @@
+// Load .env.test as defaults so local dev matches CI.
+// Do NOT use override: true — CI passes dynamic Supabase keys that must
+// take priority over the hardcoded demo key in .env.test.
+dotenv.config({ path: '.env.test' });
 
 export default defineConfig({
</file context>


export default defineConfig({
testDir: './tests/e2e/specs',
Expand Down
67 changes: 33 additions & 34 deletions src/ai/flows/workout-feedback-flow.test.ts
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';

Expand All @@ -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',
Expand All @@ -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 () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Custom agent: Enforce Pragmatic Test Coverage

The updated test suite for generateWorkoutFeedback covers the null-output failure path but removes coverage for the case where the AI prompt itself throws (e.g., network failure or API error). In production, a failed GenAI API call is a realistic failure mode; the old test explicitly verified this by asserting a thrown error propagates. Consider adding a test that simulates mockFeedbackPrompt.mockRejectedValue(new Error(...)) and verifies the error propagates from generateWorkoutFeedback, so both failure modes — an AI call that throws and one that returns null — remain covered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ai/flows/workout-feedback-flow.test.ts, line 70:

<comment>The updated test suite for `generateWorkoutFeedback` covers the null-output failure path but removes coverage for the case where the AI prompt itself throws (e.g., network failure or API error). In production, a failed GenAI API call is a realistic failure mode; the old test explicitly verified this by asserting a thrown error propagates. Consider adding a test that simulates `mockFeedbackPrompt.mockRejectedValue(new Error(...))` and verifies the error propagates from `generateWorkoutFeedback`, so both failure modes — an AI call that throws and one that returns null — remain covered.</comment>

<file context>
@@ -53,30 +63,19 @@ describe('generateWorkoutFeedback', () => {
 
-  it('propagates errors from AI call', async () => {
-    vi.mocked(generateWorkoutFeedback).mockRejectedValue(new Error('AI unavailable'));
+  it('throws when AI returns null output', async () => {
+    mockFeedbackPrompt.mockResolvedValue({ output: null });
 
</file context>

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');
});
});
141 changes: 136 additions & 5 deletions src/ai/flows/workout-generator-flow.test.ts
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: {
Expand Down Expand Up @@ -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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At src/ai/flows/workout-generator-flow.test.ts, line 184:

<comment>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.</comment>

<file context>
@@ -151,6 +155,122 @@ describe('streamWorkoutPlan', () => {
+      ) => Promise<unknown>
+    )(inputWithObs, { sendChunk });
+
+    expect(result).toBeDefined();
+  });
+
</file context>
Suggested change
expect(result).toBeDefined();
expect(result).toBeDefined();
expect(mockSetAttribute).toHaveBeenCalledWith('gen_ai.usage.input_tokens', 0);
expect(mockSetAttribute).toHaveBeenCalledWith('gen_ai.usage.output_tokens', 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 mockGenerateStream was called with a prompt containing the observation text (e.g., 'Lesão no joelho'). Currently this test covers the code branch but doesn't confirm the behavior its title describes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ai/flows/workout-generator-flow.test.ts, line 184:

<comment>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 `mockGenerateStream` was called with a prompt containing the observation text (e.g., 'Lesão no joelho'). Currently this test covers the code branch but doesn't confirm the behavior its title describes.</comment>

<file context>
@@ -151,6 +155,122 @@ describe('streamWorkoutPlan', () => {
+      ) => Promise<unknown>
+    )(inputWithObs, { sendChunk });
+
+    expect(result).toBeDefined();
+  });
+
</file context>

});

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', () => {
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading