feat(shapeai): stories 1.2 + 2.1 + 2.2 - Auth, Camera, Vision AI Pipeline#643
feat(shapeai): stories 1.2 + 2.1 + 2.2 - Auth, Camera, Vision AI Pipeline#643ryan09062004-dot wants to merge 31 commits intoSynkraAI:mainfrom
Conversation
Story 1.2 - User Authentication: - Supabase Auth with SecureStore adapter (JWT, never AsyncStorage) - Login screen with Google OAuth (PKCE via expo-web-browser) + Apple Sign In (iOS only) - signup.tsx, forgot-password.tsx created (were missing from filesystem) - auth.store.ts: signUp added, AUTH_ERROR_MAP for friendly PT-BR errors - onAuthStateChange returns subscription cleanup to prevent memory leak - 8 unit tests: initialize, signIn, signUp, signOut flows Story 2.1 - AR Camera Capture: - Two-step capture flow (front + back) with HumanSilhouette overlay - Back camera fix: facing dynamically set per step (was hardcoded 'front') - Real file size validation via expo-file-system (10 MB limit enforced) - startAnalysis → uploadPhoto x2 → triggerProcessing pipeline - SUBSCRIPTION_REQUIRED error handling → friendly paywall alert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…A PASS] New service: services/ai-engine (Python/FastAPI) - mediapipe_processor.py: MediaPipe Pose → normalized landmarks dict - score_calculator.py: 8 BodyScores (0-100) from landmark geometry - report_generator.py: Claude claude-sonnet-4-6 + cache_control:ephemeral → ReportSection[] with validation - plan_generator.py: Claude → 4-week WorkoutPlan with WorkoutWeek validation - s3_service.py: download + delete_both_photos() for LGPD compliance - db_service.py: get_analysis, mark_photos_deleted (atomic UPDATE), mark_failed - routers/analysis.py: POST /analyze — full pipeline, S3 delete before Claude, mark_failed on any error Migrations & tests: - 004_workout_plans.sql (no-op; table already in 003) - 5x pytest suites: score_calculator, mediapipe, s3, report_generator, pipeline - Jest: analysis.loading.test.tsx (5 scenarios: loading, completed nav, failed, timeout, retry) LGPD: photos deleted before report generation; photos_deleted_at in atomic UPDATE Security: x-internal-secret sent on callback; ai-engine behind private network Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Someone is attempting to deploy a commit to the SINKRA - AIOX Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new ShapeAI monorepo: documentation and PRD, shared types package, an Expo React Native mobile app (auth, camera, analysis UI, purchases, tests), a Fastify API gateway with DB migrations and S3 presigned flows, and a Python FastAPI AI engine implementing MediaPipe scoring and Claude-backed report/plan generation. ChangesRepository & Docs / Monorepo
Mobile app
API Gateway (Fastify)
AI Engine (FastAPI + pipeline)
Sequence DiagramsequenceDiagram
participant Mobile as Mobile App
participant Gateway as API Gateway
participant S3 as AWS S3
participant Engine as AI Engine
participant DB as PostgreSQL
Mobile->>Gateway: POST /analyses (start)
Gateway->>DB: INSERT analysis (status=processing)
Gateway->>S3: generate presigned upload URLs
Gateway-->>Mobile: return analysis_id + upload_urls
Mobile->>S3: PUT front photo
Mobile->>S3: PUT back photo
Mobile->>Gateway: POST /analyses/{id}/process
Gateway->>Engine: POST /analyze (analysis_id, user_id)
Engine->>S3: GET front photo
Engine->>Engine: MediaPipe pose detection
Engine->>Engine: calculate scores & generate report/workout
Engine->>S3: DELETE photos
Engine->>Gateway: POST /internal/analyses/{id}/complete (payload)
Gateway->>DB: UPDATE analysis (status=completed), UPSERT report/workout
Mobile->>Gateway: GET /analyses/{id} (polling)
Gateway->>DB: SELECT analysis + report + plan
Gateway-->>Mobile: return completed results
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested labels
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Welcome to aiox-core! Thanks for your first pull request.
What happens next?
- Automated checks will run on your PR
- A maintainer will review your changes
- Once approved, we'll merge your contribution!
PR Checklist:
- Tests pass (
npm test) - Linting passes (
npm run lint) - Commit messages follow Conventional Commits
Thanks for contributing!
There was a problem hiding this comment.
Actionable comments posted: 5
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟡 Minor comments (9)
docs/stories/2.1.ar-camera-capture.md-7-11 (1)
7-11:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix markdownlint violations in code fences and table spacing.
Line 7 and Line 82 fenced blocks are missing language tags, and the table around Line 175 should be surrounded by blank lines to satisfy MD040/MD058.
🛠️ Suggested doc lint fix
-``` +```yaml executor: "@dev" quality_gate: "@architect" quality_gate_tools: ["vitest", "jest"]-
+text
POST /analyses → cria análise + retorna presigned URLs → 201 { analysis_id, upload_urls: { front, back } }
POST /analyses/:id/process → dispara pipeline ai-engine (async) → 202 { status: 'processing' }
GET /analyses/:id → status e resultado → 200 { id, status, scores?, report?, workout_plan? }## Change Log + | Data | Versão | Descrição | Autor | |------|--------|-----------|-------| | Abr 2026 | 1.0 | Story criada | River (`@sm`) | | Abr 2026 | 1.1 | Corrigida para alinhar com arquitetura aprovada: 2 fotos (front+back), endpoint único POST /analyses, schema analyses sem s3_key, sem status pending | Dex (`@dev`) | | Abr 2026 | 1.2 | Validação `@po`: seção CodeRabbit Integration adicionada, status confirmado Ready — GO ✅ | Pax (`@po`) | | Abr 2026 | 1.3 | QA CONCERNS fixes: câmera traseira para step 'back', validação real de tamanho com expo-file-system, mock expo-file-system nos testes | Dex (`@dev`) | +Also applies to: 82-86, 175-180
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/stories/2.1.ar-camera-capture.md` around lines 7 - 11, Update the three Markdown blocks: add language tags to the fenced YAML block beginning with "executor: \"@dev\"" (use ```yaml) and to the API example block containing the POST/GET routes (use ```text), and ensure the table under the "Change Log" header is preceded and followed by a blank line so the table is separated from surrounding paragraphs; locate these by the YAML block with executor, the fenced block that starts with "POST /analyses", and the "Change Log" section/table and apply the language tag and spacing fixes consistently for the other occurrences referenced (lines ~82-86 and ~175-180).docs/shapeai/brief-shapeai.md-55-56 (1)
55-56:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign privacy statement with implemented architecture.
Line 55 says processing is on-device, but the implemented flow uploads to backend services for server-side analysis. Please update this wording to avoid privacy/compliance confusion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/shapeai/brief-shapeai.md` around lines 55 - 56, The phrasing in the table row with "**Privacy-first** | Processamento on-device para dados sensíveis" is inaccurate given the implemented flow uploads data to backend services; update that table cell to accurately state that sensitive data may be uploaded for server-side analysis (e.g., "Processamento remoto: uploads ao servidor para análise") or a neutral phrasing like "Dados sensíveis podem ser enviados ao backend para análise", and ensure the "**Privacy-first**" heading is adjusted if needed to avoid implying purely on-device processing.shapeai/apps/mobile/app/(app)/analysis/[id].tsx-14-27 (1)
14-27:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winNormalize
idbefore polling.
useLocalSearchParamsmay providestring | string[]; callingpollAnalysiswith an array can break request construction.Defensive normalization
- const { id } = useLocalSearchParams<{ id: string }>() + const { id } = useLocalSearchParams<{ id?: string | string[] }>() + const analysisId = Array.isArray(id) ? id[0] : id @@ - if (!id) return + if (!analysisId) return @@ - pollAnalysis(id) + pollAnalysis(analysisId) @@ - router.replace(`/(app)/analysis/${id}/report`) + router.replace(`/(app)/analysis/${analysisId}/report`) @@ - }, [id]) + }, [analysisId])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/analysis/[id].tsx around lines 14 - 27, useLocalSearchParams can return string|string[] so passing id directly to pollAnalysis may pass an array; inside the useEffect before calling pollAnalysis normalize the id (e.g., derive a single string like const normalizedId = Array.isArray(id) ? id[0] : id) and use that in the setInterval guard and in pollAnalysis; update any checks that use id to use normalizedId (referencing useLocalSearchParams, the id variable, and pollAnalysis) and early-return if normalizedId is falsy.docs/stories/1.2.user-authentication.md-7-11 (1)
7-11:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix markdownlint warnings in fenced blocks/table formatting.
Add fence languages (e.g.,
yaml,text) and ensure blank lines around tables to keep docs lint-clean.Also applies to: 150-162, 177-180
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/stories/1.2.user-authentication.md` around lines 7 - 11, The fenced code block containing the keys executor, quality_gate and quality_gate_tools should include a language tag (e.g., ```yaml) and any other fenced blocks in this file should get appropriate fence languages; also ensure there is an empty line before and after any Markdown tables to satisfy markdownlint (apply the same fixes to the other affected ranges noted: 150-162 and 177-180). Locate the block with the tokens executor, quality_gate, quality_gate_tools and update the fence to include the language (yaml or text) and add blank lines around tables throughout the document.docs/stories/1.2.user-authentication.md-125-130 (1)
125-130:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove
AsyncStoragefrom the secure-token reference snippet.The snippet imports
@react-native-async-storage/async-storagein a section that explicitly forbids it. This creates copy/paste risk in auth code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/stories/1.2.user-authentication.md` around lines 125 - 130, The snippet wrongly imports AsyncStorage even though the section mandates using SecureStore; remove the AsyncStorage import statement and any references to it so the secure-token example only imports and uses SecureStore (see the import line for AsyncStorage and the ExpoSecureStoreAdapter symbol) ensuring the example exports/uses SecureStore exclusively to prevent copy/paste of insecure storage.docs/shapeai/architecture-shapeai.md-500-510 (1)
500-510:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign documented ai-engine path with implemented structure
The architecture tree documents
services/ai-engine/src/..., but this PR’s implementation usesservices/ai-engine/app/.... Please align the doc to avoid onboarding/debug friction.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/shapeai/architecture-shapeai.md` around lines 500 - 510, The documented ai-engine directory tree is out of sync: the implementation uses services/ai-engine/app/... rather than services/ai-engine/src/; update the architecture-shapeai.md tree so all occurrences of "ai-engine/src/..." (including referenced subpaths like routers/analysis.py, pipeline/mediapipe_processor.py, pipeline/score_calculator.py, pipeline/report_generator.py, pipeline/plan_generator.py, and privacy/photo_cleaner.py) reflect "ai-engine/app/..." to match the actual repository layout.docs/stories/2.2.vision-ai-analysis.md-161-170 (1)
161-170:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winResolve migration description conflict for
004_workout_plans.sqlThis section shows
004_workout_plans.sqlcreatingworkout_plans, while Line 220 states004is a no-op because the table already exists in003. Please keep one canonical version to avoid implementation drift.Also applies to: 220-220
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/stories/2.2.vision-ai-analysis.md` around lines 161 - 170, The docs contain conflicting migration descriptions for migration "004_workout_plans.sql": one shows creating the workout_plans table (CREATE TABLE workout_plans ...) while another line (around the reference to migration 004) says it's a no-op because the table already exists from migration 003; pick a single canonical state and update the doc accordingly — either keep the CREATE TABLE definition for workout_plans (and remove the "no-op" note) or mark 004 as a no-op and remove the CREATE TABLE block; update the section that references migration 004 (Line ~220) to match the chosen canonical version and ensure the migration name/description and the table name workout_plans/analysis_id are consistent.docs/stories/2.2.vision-ai-analysis.md-7-11 (1)
7-11:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix markdownlint blockers in fenced code/table sections
Add a language to the fenced block (e.g.,
yaml) and ensure tables are surrounded by blank lines to avoid markdownlint CI failures.Also applies to: 206-210
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/stories/2.2.vision-ai-analysis.md` around lines 7 - 11, The fenced code block containing the YAML keys (executor: "@dev", quality_gate: "@architect", quality_gate_tools: ["pytest", "vitest"]) is missing a language tag and causes markdownlint failures; update that fenced block to start with ```yaml and also ensure any nearby Markdown tables (notably the table around lines 206-210) are preceded and followed by a blank line so the linter recognizes them as separate blocks.shapeai/services/ai-engine/app/pipeline/score_calculator.py-79-79 (1)
79-79:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRename ambiguous loop variable to satisfy lint and readability
The comprehension uses
las a variable name, which triggers RuffE741and may fail lint checks. Rename to explicit names likeleft_key/right_key.Suggested change
- diffs = [abs(_y(landmarks_front, l) - _y(landmarks_front, r)) for l, r in pairs] + diffs = [ + abs(_y(landmarks_front, left_key) - _y(landmarks_front, right_key)) + for left_key, right_key in pairs + ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/ai-engine/app/pipeline/score_calculator.py` at line 79, The list comprehension assigning diffs uses ambiguous loop variables `l` and `r`; rename them to descriptive names (e.g., `left_key` and `right_key`) to satisfy Ruff E741 and improve readability. Update the comprehension: replace `l, r` with `left_key, right_key` and use those names in the call to `_y(landmarks_front, ...)`, and ensure any other references to `l`/`r` within the same scope (including nearby comprehensions or loops using `pairs`) are similarly renamed to avoid shadowing and lint errors.
🧹 Nitpick comments (15)
.gitignore (1)
141-141: 💤 Low valueRedundant negation pattern.
The negation
!shapeai/apps/has no effect because the patternapps/on line 140 only ignores a root-levelapps/directory, notshapeai/apps/. Consider removing this line or adding a comment to clarify intent.♻️ Suggested simplification
-!shapeai/apps/Or, if documenting intent is desired:
+# shapeai/apps/ is tracked (not affected by apps/ ignore above) !shapeai/apps/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore at line 141, The negation pattern '!shapeai/apps/' is redundant because the existing 'apps/' ignore pattern already does not apply to 'shapeai/apps/', so remove the '!shapeai/apps/' line; if your intent was actually to unignore a nested directory, instead adjust the ignore rules (for example modify 'apps/' to a rooted '/apps/' or explicitly ignore 'shapeai/apps/' and then use '!shapeai/apps/') so the negation has an effect.shapeai/apps/mobile/tests/analysis/analysis.service.test.ts (2)
1-8: ⚡ Quick winUse absolute imports in this test module.
Lines 1, 3, and 8 are using relative imports; please convert them to the repo’s absolute import style.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/analysis/analysis.service.test.ts` around lines 1 - 8, Replace the relative imports in this test with the repository’s absolute import paths: change the import of pollAnalysis from '../../src/services/analysis.service' to the project's absolute module path for that service, and change the mocked import and subsequent real import of apiGet and apiPost from '../../src/services/api.client' to the absolute module path for the API client; keep the jest.mock wrapper and named imports (pollAnalysis, apiGet, apiPost) the same so tests and mocks continue to reference the same symbols.
10-11: Usejest.MockedFunction<typeof ...>to preserve type safety for mocked functions.Lines 10–11 currently use broad
jest.Mockcasts, which lose the function signatures forapiGet<T>(path: string): Promise<T>andapiPost<T>(path: string, body?: unknown): Promise<T>. Replace withjest.MockedFunction<typeof apiGet>andjest.MockedFunction<typeof apiPost>to maintain type enforcement for arguments and return types.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/analysis/analysis.service.test.ts` around lines 10 - 11, Replace the broad jest.Mock casts for mockGet and mockPost with typed mocked function types so the original signatures are preserved: change the casts from jest.Mock to jest.MockedFunction<typeof apiGet> for mockGet and jest.MockedFunction<typeof apiPost> for mockPost; this ensures apiGet<T>(path: string): Promise<T> and apiPost<T>(path: string, body?: unknown): Promise<T> remain type-safe when mocked.shapeai/apps/mobile/app/(app)/index.tsx (1)
3-3: ⚡ Quick winUse an absolute import for the auth store.
Line 3 uses a relative path import; please switch it to the project’s absolute import convention.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/index.tsx at line 3, Replace the relative import of the auth store in index.tsx (import { useAuthStore } from '../../src/stores/auth.store') with the project's absolute import path (e.g., import { useAuthStore } from 'src/stores/auth.store' or whatever root alias your project uses) so that useAuthStore is imported via the absolute module path consistent with the codebase convention.shapeai/apps/mobile/app/index.tsx (1)
2-2: ⚡ Quick winReplace relative auth-store import with absolute import.
Line 2 currently uses a relative path; align it with the repo’s absolute import rule.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/index.tsx` at line 2, Replace the relative import of the auth store in index.tsx by changing the import that references '../src/stores/auth.store' to the project's absolute import path (use the module path used elsewhere, e.g., 'stores/auth.store' or the repo's configured base import) so the useAuthStore import uses an absolute module specifier; update the import statement that references useAuthStore accordingly to match the repo's absolute-import convention.shapeai/apps/mobile/app/(app)/_layout.tsx (1)
2-2: ⚡ Quick winUse absolute import for auth store.
Line 2 currently imports
useAuthStorevia a relative path. Please convert it to the configured absolute alias.As per coding guidelines
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/_layout.tsx at line 2, Replace the relative import of useAuthStore with the configured absolute alias import; locate the import statement that reads "import { useAuthStore } from '../../src/stores/auth.store'" and change it to the project’s absolute alias form (e.g., "import { useAuthStore } from 'src/stores/auth.store'" or "import { useAuthStore } from '@/stores/auth.store'") so the symbol useAuthStore is imported via the configured absolute path.shapeai/apps/mobile/app/(auth)/signup.tsx (1)
4-4: ⚡ Quick winReplace relative store import with project absolute import.
Line 4 uses a relative path (
../../src/stores/auth.store). Please switch to the repo’s absolute import alias for consistency.As per coding guidelines
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(auth)/signup.tsx at line 4, In signup.tsx replace the relative import of useAuthStore ("../../src/stores/auth.store") with the project's absolute import alias (useAuthStore from the configured absolute path, e.g. the repo alias for src/stores/auth.store) so the file uses the standard absolute import style and matches other modules.shapeai/apps/mobile/src/services/supabase.client.ts (1)
10-21: ⚡ Quick winType the Supabase client with the generated DB schema.
Lines 10–21 create an untyped client, which weakens table/column inference and can leak broad
any-style typing across auth/data calls.As per coding guidelines
**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/src/services/supabase.client.ts` around lines 10 - 21, The supabase client is created without the generated DB types, causing weak typing; import your generated schema type (e.g., Database) and pass it as the generic to createClient (createClient<Database>(...)) so that the exported supabase constant is properly typed for table/column inference and auth/data calls; keep the same config (auth.storage = ExpoSecureStoreAdapter, autoRefreshToken, persistSession, detectSessionInUrl) and export the typed supabase variable.shapeai/apps/mobile/src/services/api.client.ts (2)
18-32: ⚡ Quick winHarden JSON typing instead of passing through implicit
any.Lines 18 and 32 return
res.json()directly asT;Response.json()is untyped (any) and bypasses TS guarantees. Parse asunknownfirst and validate/narrow per endpoint contract.As per coding guidelines
**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/src/services/api.client.ts` around lines 18 - 32, The apiPost function currently returns res.json() directly (untyped any) which bypasses TS guarantees; modify apiPost to first await res.json() as unknown, then validate/narrow that value to T before returning (use a runtime validator/schema or explicit type guards per endpoint contract), and throw if validation fails; update the error-path handling that reads res.json() similarly; reference apiPost, res.json(), and API_URL when locating the change.
1-1: ⚡ Quick winUse absolute import for Supabase client.
Line 1 should use the project’s absolute import alias instead of
./supabase.client.As per coding guidelines
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/src/services/api.client.ts` at line 1, In api.client.ts the relative import "import { supabase } from './supabase.client'" must be changed to use the project's absolute import alias; replace the './supabase.client' specifier with the configured absolute path (e.g., the project's alias root + '/supabase.client') so the import becomes an absolute import for the supabase client, verify the alias resolves in tsconfig/webpack and run the linter/build to confirm no import errors.shapeai/services/ai-engine/tests/test_s3_service.py (1)
31-37: ⚡ Quick winStrengthen delete assertions to verify bucket and keys.
Line 37 only checking
call_countwon’t catch wrong key extraction regressions. Assert the expectedBucket/Keycall args.Example assertion upgrade
+from unittest.mock import call @@ - assert mock_s3.delete_object.call_count == 2 + mock_s3.delete_object.assert_has_calls( + [ + call(Bucket="shapeai-photos", Key="uploads/u/a/front.jpg"), + call(Bucket="shapeai-photos", Key="uploads/u/a/back.jpg"), + ], + any_order=False, + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/ai-engine/tests/test_s3_service.py` around lines 31 - 37, Replace the weak call_count check with assertions that verify the actual S3 delete parameters: inspect mock_s3.delete_object.call_args_list after calling delete_both_photos and assert each call includes the expected Bucket and Key values (derived from the input URLs) so you validate correct key extraction; reference the mock object mock_s3 and the target function delete_both_photos and assert on mock_s3.delete_object.call_args_list entries for the two expected {"Bucket": "...", "Key": "..."} dictionaries.shapeai/apps/mobile/tests/auth/auth.store.test.ts (1)
29-30: ⚡ Quick winReplace
as nevercasts with concrete typed fixtures/mocks.
as neversuppresses useful type checks in a file that should validate TS contracts.As per coding guidelines, "
**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific."Also applies to: 42-45, 57-58, 68-69, 81-82, 95-96, 107-108, 122-122
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/auth/auth.store.test.ts` around lines 29 - 30, Replace the unsafe `as never` casts by creating concrete, properly-typed fixtures for the mocked session values (e.g. declare `const fakeSession: Session` or `const fakeSession: Partial<Session>` with the expected shape) and use those typed variables when calling `mockAuth.getSession.mockResolvedValue(...)`; update all occurrences referencing `fakeSession` and similar test fixtures (the lines around the other listed instances) so the mocks match the real Session type instead of suppressing TS checks with `as never`.shapeai/apps/mobile/src/services/analysis.service.ts (1)
12-13: ⚡ Quick winReplace
unknown[]with shared report/workout types
unknown[]weakens type safety on a core API response. Prefer shared contracts (ReportSection[],WorkoutWeek[]) to keep client-server payloads aligned.As per coding guidelines, "
**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific. Ensure exports are properly typed."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/src/services/analysis.service.ts` around lines 12 - 13, The report and workout_plan properties currently use unknown[] which reduces type safety; replace report?: { highlights: unknown[]; development_areas: unknown[] } and workout_plan?: { weeks: unknown[] } with concrete shared types such as report?: { highlights: ReportSection[]; development_areas: ReportSection[] } and workout_plan?: { weeks: WorkoutWeek[] }, and import or declare/export ReportSection and WorkoutWeek (or adjust names to existing shared contracts) so the compiler enforces the API contract.shapeai/packages/shared/src/types/analysis.types.ts (1)
91-100: ⚡ Quick winMake
MUSCLE_EMOJIkey-safe withkeyof BodyScores
Record<string, string>allows missing/extra keys silently. Typing it asRecord<keyof BodyScores, string>gives compile-time coverage for all score dimensions.As per coding guidelines, "
**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific. Ensure exports are properly typed."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/packages/shared/src/types/analysis.types.ts` around lines 91 - 100, Change the MUSCLE_EMOJI declaration to be keyed by BodyScores instead of generic string: replace Record<string, string> with Record<keyof BodyScores, string> (update any import/definition of BodyScores if needed), then ensure the MUSCLE_EMOJI object includes an entry for every key in the BodyScores type and remove any extra keys; fix any resulting type errors by adding missing keys or aligning names to the BodyScores property names so the compiler enforces full coverage.shapeai/services/api-gateway/src/routes/analyses.ts (1)
145-149: ⚡ Quick winTighten callback body types instead of
unknown[]
unknown[]forreport/workout_planweakens compile-time guarantees. Use sharedReportSection/WorkoutWeekcontracts (or local strict interfaces) for safer payload handling.As per coding guidelines, "
**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific. Ensure exports are properly typed."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 145 - 149, The handler's Body currently uses unknown[] for report highlights/development_areas and workout_plan weeks; define strict interfaces (e.g., ReportSection with fields like title:string, content:string | string[], metadata?:Record<string,unknown> and WorkoutWeek with fields like weekNumber:number, sessions:Session[] or similar) and replace unknown[] with ReportSection[] and WorkoutWeek[] in the app.post<{ Params:..., Body:... }> generic so the route's Body becomes scores:Record<string,number>, report:{ highlights:ReportSection[]; development_areas:ReportSection[] }, workout_plan:{ weeks:WorkoutWeek[] }; update any imports/exports or local type aliases to reuse these types across the module.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 316da79d-139e-4146-9576-fb31cd868c8c
⛔ Files ignored due to path filters (5)
shapeai/apps/mobile/assets/adaptive-icon.pngis excluded by!**/*.pngshapeai/apps/mobile/assets/favicon.pngis excluded by!**/*.pngshapeai/apps/mobile/assets/icon.pngis excluded by!**/*.pngshapeai/apps/mobile/assets/splash-icon.pngis excluded by!**/*.pngshapeai/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (78)
.gitignoredocs/qa/QA_FIX_REQUEST.mddocs/shapeai/architecture-shapeai.mddocs/shapeai/brief-shapeai.mddocs/shapeai/prd-shapeai.mddocs/stories/1.2.user-authentication.mddocs/stories/2.1.ar-camera-capture.mddocs/stories/2.2.vision-ai-analysis.mdshapeai/.env.exampleshapeai/apps/mobile/.gitignoreshapeai/apps/mobile/App.tsxshapeai/apps/mobile/app.jsonshapeai/apps/mobile/app/(app)/_layout.tsxshapeai/apps/mobile/app/(app)/analysis/[id].tsxshapeai/apps/mobile/app/(app)/camera.tsxshapeai/apps/mobile/app/(app)/history.tsxshapeai/apps/mobile/app/(app)/index.tsxshapeai/apps/mobile/app/(auth)/_layout.tsxshapeai/apps/mobile/app/(auth)/forgot-password.tsxshapeai/apps/mobile/app/(auth)/login.tsxshapeai/apps/mobile/app/(auth)/signup.tsxshapeai/apps/mobile/app/_layout.tsxshapeai/apps/mobile/app/index.tsxshapeai/apps/mobile/index.tsshapeai/apps/mobile/jest.config.jsshapeai/apps/mobile/package.jsonshapeai/apps/mobile/src/components/camera/HumanSilhouette.tsxshapeai/apps/mobile/src/services/analysis.service.tsshapeai/apps/mobile/src/services/api.client.tsshapeai/apps/mobile/src/services/supabase.client.tsshapeai/apps/mobile/src/stores/auth.store.tsshapeai/apps/mobile/tests/analysis/analysis.loading.test.tsxshapeai/apps/mobile/tests/analysis/analysis.service.test.tsshapeai/apps/mobile/tests/auth/auth.store.test.tsshapeai/apps/mobile/tests/camera/camera.screen.test.tsxshapeai/apps/mobile/tsconfig.jsonshapeai/package.jsonshapeai/packages/shared/package.jsonshapeai/packages/shared/src/index.tsshapeai/packages/shared/src/types/analysis.types.tsshapeai/packages/shared/src/types/user.types.tsshapeai/services/ai-engine/app/__init__.pyshapeai/services/ai-engine/app/main.pyshapeai/services/ai-engine/app/pipeline/__init__.pyshapeai/services/ai-engine/app/pipeline/mediapipe_processor.pyshapeai/services/ai-engine/app/pipeline/plan_generator.pyshapeai/services/ai-engine/app/pipeline/report_generator.pyshapeai/services/ai-engine/app/pipeline/score_calculator.pyshapeai/services/ai-engine/app/routers/__init__.pyshapeai/services/ai-engine/app/routers/analysis.pyshapeai/services/ai-engine/app/services/__init__.pyshapeai/services/ai-engine/app/services/db_service.pyshapeai/services/ai-engine/app/services/s3_service.pyshapeai/services/ai-engine/pytest.inishapeai/services/ai-engine/requirements.txtshapeai/services/ai-engine/tests/__init__.pyshapeai/services/ai-engine/tests/test_mediapipe_processor.pyshapeai/services/ai-engine/tests/test_pipeline.pyshapeai/services/ai-engine/tests/test_report_generator.pyshapeai/services/ai-engine/tests/test_s3_service.pyshapeai/services/ai-engine/tests/test_score_calculator.pyshapeai/services/api-gateway/package.jsonshapeai/services/api-gateway/src/db/client.tsshapeai/services/api-gateway/src/db/migrations/001_users.sqlshapeai/services/api-gateway/src/db/migrations/002_analyses.sqlshapeai/services/api-gateway/src/db/migrations/003_reports.sqlshapeai/services/api-gateway/src/db/migrations/004_workout_plans.sqlshapeai/services/api-gateway/src/middleware/auth.tsshapeai/services/api-gateway/src/routes/analyses.tsshapeai/services/api-gateway/src/routes/profile.tsshapeai/services/api-gateway/src/server.tsshapeai/services/api-gateway/src/services/freemium.service.tsshapeai/services/api-gateway/src/services/s3.service.tsshapeai/services/api-gateway/tests/analyses/analyses.routes.test.tsshapeai/services/api-gateway/tsconfig.jsonshapeai/services/api-gateway/vitest.config.tsshapeai/tsconfig.jsonshapeai/turbo.json
| "expo": "~54.0.33", | ||
| "expo-camera": "~17.0.10", | ||
| "expo-file-system": "~19.0.22", | ||
| "expo-image-picker": "~17.0.11", | ||
| "expo-router": "~6.0.23", | ||
| "expo-secure-store": "~15.0.8", | ||
| "expo-status-bar": "~3.0.9", | ||
| "react": "19.1.0", | ||
| "react-native": "0.81.5", | ||
| "react-native-safe-area-context": "~5.6.0", | ||
| "react-native-screens": "~4.16.0", | ||
| "react-native-svg": "15.12.1", | ||
| "zustand": "^5.0.12" | ||
| }, | ||
| "devDependencies": { | ||
| "@testing-library/react-native": "^13.3.3", | ||
| "@types/jest": "^30.0.0", | ||
| "@types/react": "~19.1.0", | ||
| "jest": "^30.3.0", | ||
| "jest-expo": "^55.0.16", | ||
| "typescript": "~5.9.2" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Expo SDK 54, what jest-expo version range is officially supported?
💡 Result:
For Expo SDK 54, the officially supported version range for jest-expo is ^54.0.0 (specifically, versions from 54.0.0 up to the latest patch release 54.0.17 as of the available data). This is determined by running npx expo install jest-expo, which installs the compatible version matching the installed Expo SDK version. Expo SDK packages, including jest-expo, follow semantic versioning aligned with the SDK major version, and patch releases within the 54.x.x range are supported and recommended for bug fixes.
Citations:
- 1: https://github.com/expo/expo/blob/68661df5/packages/jest-expo/CHANGELOG.md
- 2: https://www.npmjs.com/package/jest-expo?activeTab=versions
- 3: https://github.com/expo/expo/blob/main/packages/jest-expo/CHANGELOG.md
- 4: https://docs.expo.dev/versions/v54.0.0
- 5: https://www.npmjs.com/package/jest-expo
- 6: https://docs.expo.dev/develop/unit-testing
Use jest-expo ^54.0.x with Expo SDK 54.
Line 33 pins jest-expo to ^55.0.16, which requires Expo SDK 55. Expo SDK 54 (line 14) requires jest-expo ^54.x.x range. Update jest-expo to ^54.0.0 to match the Expo SDK version and prevent test runner failures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/package.json` around lines 14 - 34, The devDependency
"jest-expo" is set to ^55.0.16 while the project uses "expo": "~54.0.33"; change
the "jest-expo" entry in package.json devDependencies from ^55.0.16 to a ^54.0.0
range (e.g., "^54.0.0") so it matches Expo SDK 54, then reinstall dependencies
to apply the change.
| export interface Exercise { | ||
| name: string | ||
| muscle_group: string | ||
| sets: number | ||
| reps: string | ||
| rest_seconds: number | ||
| note: string | null | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Shared Exercise type =="
fd -i 'analysis.types.ts' | xargs -I{} rg -n -C3 'interface Exercise|note|notes' "{}"
echo "== API gateway usage =="
fd -i 'analyses.ts' | xargs -I{} rg -n -C3 'workout|exercise|note|notes' "{}"
echo "== AI engine generators =="
fd -i 'plan_generator.py' | xargs -I{} rg -n -C3 'note|notes|exercise|sessions|weeks' "{}"
echo "== Mobile rendering usage =="
rg -n --type=ts --type=tsx -C3 'exercise|note|notes|rest_seconds' shapeai/apps/mobile || trueRepository: SynkraAI/aiox-core
Length of output: 3270
Reconcile Exercise field naming: note vs notes causing silent data loss
The TypeScript interface defines note: string | null, but the AI engine's SYSTEM_PROMPT in plan_generator.py generates notes?: string (line 15). This mismatch causes the AI-generated notes value to be silently dropped during deserialization into the Exercise type.
Evidence of mismatch
shapeai/packages/shared/src/types/analysis.types.ts (line 52):
note: string | null
plan_generator.py (line 15 in SYSTEM_PROMPT):
"Każdy Exercise: { name: string, sets: number, reps: string, rest_seconds: number, notes?: string }\n"
Choose one convention (recommended: notes to match generator output), then update both the shared type definition and all consuming code (API gateway, mobile rendering).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/packages/shared/src/types/analysis.types.ts` around lines 46 - 53,
The Exercise interface and consumers must use the same field name for freeform
text—rename Exercise.note to Exercise.notes (string | null or optional string)
and update all code that constructs/deserializes Exercises to match;
specifically change the Exercise interface (Exercise) from note to notes, update
the SYSTEM_PROMPT usage in plan_generator.py (SYSTEM_PROMPT) and any places that
parse AI output, API gateway handlers, and mobile/rendering components that read
Exercise.note to read Exercise.notes so the AI-generated notes are preserved
end-to-end.
| [frontResult.url, backResult.url, analysisId] | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== generatePresignedUploadUrl implementation =="
fd -i 's3.service.ts' | xargs -I{} rg -n -C4 'generatePresignedUploadUrl|presign|presigned' "{}"
echo "== API route persistence points =="
fd -i 'analyses.ts' | xargs -I{} rg -n -C4 'photo_front_url|photo_back_url|upload_urls|/analyze' "{}"
echo "== ai-engine photo retrieval/deletion behavior =="
fd -i 's3_service.py' | xargs -I{} rg -n -C5 'download|delete|extract_key|photo_front_url|photo_back_url|requests|boto3' "{}"Repository: SynkraAI/aiox-core
Length of output: 4308
Extend presigned URL TTL or implement URL refresh mechanism
Presigned URLs are generated with only a 5-minute TTL, but the async flow to ai-engine may exceed this. If ai-engine's /analyze processing takes longer than 5 minutes, download_photo() will fail with 403 AccessDenied. Either:
- Increase
PRESIGNED_TTL_SECONDS(line 13, s3.service.ts) to match expected processing time, or - Implement URL refresh logic in ai-engine before download, or
- Pre-download photos in the api-gateway before async notification
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 42 - 43,
Presigned URLs used in the async flow can expire before ai-engine calls
download_photo(), so either increase the TTL or add refresh/download logic:
update PRESIGNED_TTL_SECONDS in s3.service.ts to a value that exceeds the max
ai-engine processing time, or implement URL refresh in ai-engine before invoking
download_photo() (call the API/gateway to request a fresh presigned URL for the
same object), or alternatively have api-gateway pre-download the photos and
include durable storage or the file contents instead of passing
frontResult.url/backResult.url in the analyses route; locate
PRESIGNED_TTL_SECONDS, download_photo(), and the code passing
frontResult.url/backResult.url (analyses.ts) to apply one of these fixes.
| const secret = request.headers['x-internal-secret'] | ||
| if (secret !== process.env.INTERNAL_SECRET) { | ||
| return reply.status(401).send({ error: 'Unauthorized' }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "analyses.ts" -path "*/api-gateway/*" | head -5Repository: SynkraAI/aiox-core
Length of output: 116
🏁 Script executed:
cat -n shapeai/services/api-gateway/src/routes/analyses.ts | head -160 | tail -30Repository: SynkraAI/aiox-core
Length of output: 1301
🏁 Script executed:
cat -n shapeai/services/api-gateway/src/routes/analyses.ts | head -20Repository: SynkraAI/aiox-core
Length of output: 1022
🏁 Script executed:
cat -n shapeai/services/api-gateway/src/routes/analyses.ts | sed -n '1,50p'Repository: SynkraAI/aiox-core
Length of output: 2032
🏁 Script executed:
cat -n shapeai/services/api-gateway/src/routes/analyses.ts | sed -n '145,160p'Repository: SynkraAI/aiox-core
Length of output: 754
🏁 Script executed:
wc -l shapeai/services/api-gateway/src/routes/analyses.tsRepository: SynkraAI/aiox-core
Length of output: 118
🏁 Script executed:
cat shapeai/services/api-gateway/src/routes/analyses.tsRepository: SynkraAI/aiox-core
Length of output: 6753
🏁 Script executed:
ls -la shapeai/services/api-gateway/src/ | head -20Repository: SynkraAI/aiox-core
Length of output: 458
🏁 Script executed:
cat shapeai/services/api-gateway/tsconfig.json 2>/dev/null || cat shapeai/services/api-gateway/package.json | head -40Repository: SynkraAI/aiox-core
Length of output: 440
🏁 Script executed:
#!/bin/bash
FILE="shapeai/services/api-gateway/src/routes/analyses.ts"
echo "=== File: $FILE ==="
cat "$FILE"
echo ""
echo "=== Checking tsconfig.json ==="
if [ -f "shapeai/services/api-gateway/tsconfig.json" ]; then
cat "shapeai/services/api-gateway/tsconfig.json"
else
echo "tsconfig.json not found in api-gateway"
fiRepository: SynkraAI/aiox-core
Length of output: 7229
Harden internal callback authentication to prevent bypass when INTERNAL_SECRET is unset
The authentication check at lines 152-154 has a critical flaw: if process.env.INTERNAL_SECRET is undefined and the request lacks an x-internal-secret header, both values are undefined, making the comparison undefined !== undefined return false, which allows the request to pass. This callback must require explicit secret validation.
Additionally, this file uses relative imports throughout (e.g., ../db/client, ../middleware/auth) which violates the guideline requiring absolute imports in TypeScript files.
Suggested authentication fix
- const secret = request.headers['x-internal-secret']
- if (secret !== process.env.INTERNAL_SECRET) {
+ const expectedSecret = process.env.INTERNAL_SECRET
+ if (!expectedSecret) {
+ request.log.error('INTERNAL_SECRET is not configured')
+ return reply.status(500).send({ error: 'Server misconfigured' })
+ }
+ const secret = request.headers['x-internal-secret']
+ if (typeof secret !== 'string' || secret.length === 0 || secret !== expectedSecret) {
return reply.status(401).send({ error: 'Unauthorized' })
}Also convert relative imports to absolute imports (requires configuring baseUrl or paths in tsconfig.json, or using appropriate module aliases).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const secret = request.headers['x-internal-secret'] | |
| if (secret !== process.env.INTERNAL_SECRET) { | |
| return reply.status(401).send({ error: 'Unauthorized' }) | |
| const expectedSecret = process.env.INTERNAL_SECRET | |
| if (!expectedSecret) { | |
| request.log.error('INTERNAL_SECRET is not configured') | |
| return reply.status(500).send({ error: 'Server misconfigured' }) | |
| } | |
| const secret = request.headers['x-internal-secret'] | |
| if (typeof secret !== 'string' || secret.length === 0 || secret !== expectedSecret) { | |
| return reply.status(401).send({ error: 'Unauthorized' }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 152 - 154,
The internal callback auth currently compares
request.headers['x-internal-secret'] to process.env.INTERNAL_SECRET and can be
bypassed when INTERNAL_SECRET is unset; update the check in analyses.ts to
require a non-empty configured secret and fail if process.env.INTERNAL_SECRET is
falsy (e.g., throw 500 or 401), then validate that
request.headers['x-internal-secret'] exists and exactly matches the configured
secret before proceeding (use the same header name and symbol names to find the
logic). Also replace all relative imports in this file (e.g., "../db/client",
"../middleware/auth") with the project’s configured absolute import paths (after
ensuring tsconfig baseUrl/paths or aliases are set) so the file uses absolute
imports consistently.
| const fields = Object.keys(updates) as (keyof ProfileBody)[] | ||
|
|
||
| if (fields.length === 0) return reply.status(400).send({ error: 'No fields to update' }) | ||
|
|
||
| const setClauses = fields.map((f, i) => `${f} = $${i + 2}`).join(', ') | ||
| const values = [userId, ...fields.map((f) => updates[f])] | ||
|
|
||
| const { rows } = await pool.query( | ||
| `UPDATE user_profiles SET ${setClauses}, updated_at = NOW() | ||
| WHERE user_id = $1 RETURNING *`, |
There was a problem hiding this comment.
PATCH /profile builds SQL from untrusted keys (injection risk).
Object.keys(request.body) is runtime input. Interpolating keys into SQL allows crafted keys to alter the query. Whitelist allowed columns before building setClauses and reject unknown fields.
Proposed fix
- const updates = request.body
- const fields = Object.keys(updates) as (keyof ProfileBody)[]
+ const updates = request.body
+ const allowedFields: (keyof ProfileBody)[] = [
+ 'height_cm',
+ 'weight_kg',
+ 'biological_sex',
+ 'primary_goal',
+ ]
+ const rawFields = Object.keys(updates) as string[]
+ const fields = rawFields.filter((f): f is keyof ProfileBody =>
+ allowedFields.includes(f as keyof ProfileBody)
+ )
+
+ if (rawFields.length !== fields.length) {
+ return reply.status(400).send({ error: 'Invalid fields in update payload' })
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/routes/profile.ts` around lines 44 - 53, The
code builds SQL SET clauses from untrusted keys (fields / updates / ProfileBody)
which allows injection; before constructing setClauses and values, validate and
whitelist the incoming keys against an allowed-columns list (e.g., const allowed
= ['display_name','bio',...]) and filter fields = fields.filter(f =>
allowed.includes(f as string)); if any input key is not allowed return a 400
error; then build setClauses and values only from the filtered/whitelisted
fields and use parameterized values in pool.query (function/method references:
fields, updates, setClauses, values, pool.query) to eliminate key injection.
- ReportScreen with ScoreGauge (react-native-svg), highlights/development areas (max 3 each), disclaimer, workout navigation - ReportSectionCard component with muscle emoji map, score badge, highlight/development variants - getAnalysisResult() service + AnalysisResult/ReportSection types in shared package - 10/10 tests passing (report-section-card + report.screen suites) - Fix Jest env: mock expo/src/winter, react singleton via moduleNameMapper (root 19.2.5), react-test-renderer@19.2.5 at workspace root - Fix analysis.loading.test.tsx: ambiguous regex replaced with exact text query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkoutPlanScreen with horizontal week tabs (ScrollView) and vertical day scroll - WorkoutDayCard component: day label, focus title, exercise list - ExerciseItem component: name, sets×reps, formatted rest, muscle emoji, note - profile.service.ts: GET /profile to fetch primary_goal for header - Header: goal label (PT-BR via GOAL_LABEL) + color-coded score badge + Nova Análise button - Reuses MUSCLE_EMOJI, formatRest, getScoreColor, calculateOverallScore from @shapeai/shared - 17 new tests (exercise-item + workout-plan screen); 47/47 total passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/report.tsx:
- Around line 12-13: Replace the deep relative imports in this file by using
absolute module paths: update the import of getAnalysisResult, AnalysisResult,
BodyScores to import from the absolute module "src/services/analysis.service"
and update ReportSectionCard to import from
"src/components/report/ReportSectionCard"; ensure the import specifiers match
your project's tsconfig/baseUrl or path alias configuration so the symbols get
resolved correctly and run a quick compile to confirm no remaining relative
'../../../../' paths remain.
- Around line 70-72: The effect in useEffect currently returns early when id is
falsy but never clears the loading state, causing an infinite spinner; update
the effect that calls getAnalysisResult(id) to ensure loading is set to false
when id is missing (e.g., call setLoading(false) before the early return or
handle the missing id by redirecting/setting error state), and keep the existing
getAnalysisResult(id) path unchanged so setLoading is still managed after the
fetch.
In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/workout.tsx:
- Around line 18-20: Replace the deep relative imports for getAnalysisResult,
getUserProfile and WorkoutDayCard with the configured absolute import paths;
locate the import lines importing '../../../../src/services/analysis.service',
'../../../../src/services/profile.service', and
'../../../../src/components/workout/WorkoutDayCard' and change them to use
absolute imports (e.g., import { getAnalysisResult } from
'src/services/analysis.service', import { getUserProfile } from
'src/services/profile.service', import WorkoutDayCard from
'src/components/workout/WorkoutDayCard' or whatever project absolute alias is
configured).
- Around line 31-33: The useEffect early-return when id is falsy leaves the
component's loading state true; in the useEffect that references id,
getAnalysisResult and getUserProfile, update the logic so that when id is
missing you explicitly set loading (or an equivalent state like isLoading) to
false (or set an error state) and avoid starting the Promise.all; also ensure
the Promise.all success and failure handlers in the effect set loading to false
and handle errors; locate the useEffect, id variable, and functions
getAnalysisResult/getUserProfile to implement these state updates.
- Line 35: The double-cast on setWeeks masks a real mismatch between the local
AnalysisResult and the shared WorkoutWeek schema; remove the cast and either (A)
update getAnalysisResult/AnalysisResult in analysis.service.ts to import and use
the shared WorkoutWeek/WorkoutSession/Exercise types so workout_plan.weeks
already matches WorkoutWeek[], or (B) map/transform analysis.workout_plan.weeks
before calling setWeeks to produce objects with the expected fields (day, focus,
exercises: Exercise[]) and types; target symbols: setWeeks,
analysis.workout_plan.weeks, getAnalysisResult, AnalysisResult, WorkoutWeek,
WorkoutSession in analysis.service.ts.
In `@shapeai/apps/mobile/src/services/analysis.service.ts`:
- Around line 1-2: The service file imports a UI-layer type ReportSection from a
component (coupling service contract to UI) and uses a relative import; change
the import to use the shared/domain type instead and switch to absolute import
paths. Replace the ReportSection import with the domain/shared type (e.g., a
ReportSection or Report DTO exported from your shared types module) and update
the import to the absolute path configured in tsconfig (instead of
'../components/report/ReportSection'); also ensure apiGet and apiPost imports
remain correct (from './api.client' or switch to absolute if required) so the
analysis.service.ts only depends on domain/shared types, not UI components.
- Around line 13-14: The AnalysisStatusResponse type currently uses unknown[]
for report and workout_plan which loses type safety; define concrete interfaces
(e.g., ReportSection with fields for highlights/development_area items and
WorkoutWeek for week entries) and replace report?: { highlights: unknown[];
development_areas: unknown[] } and workout_plan?: { weeks: unknown[] } with
report?: { highlights: ReportSection[]; development_areas: ReportSection[] } and
workout_plan?: { weeks: WorkoutWeek[] } (or equivalent named types), export
these new interfaces, and scan for any lingering any usages to tighten types
across consumers of AnalysisStatusResponse.
- Around line 30-33: The exported WorkoutSession interface is too simple (name:
string, exercises: string[]) and doesn't match consumers that expect fields like
day and focus and structured exercise objects; update the WorkoutSession type to
include day: string (or enum), focus: string, and exercises: Array of a proper
Exercise type (e.g., Exercise with id/name/reps/sets or the fields used by
consuming code), add/export the Exercise interface, and replace any usages or
casts that assume string[] so callers use the correct typed shape (search for
WorkoutSession, exercises, day, focus in current diffs to locate affected code).
In `@shapeai/apps/mobile/src/services/profile.service.ts`:
- Line 1: Replace the relative import of apiGet in profile.service.ts (import {
apiGet } from './api.client') with the project’s configured absolute alias
import (use the tsconfig/webpack alias for the API client) so that apiGet is
imported via the absolute path; update the import statement to reference the
alias and run type-check/build to ensure the path resolves correctly.
In `@shapeai/apps/mobile/tests/analysis/analysis.loading.test.tsx`:
- Around line 9-10: Update the relative imports in the test so they use the
project's absolute import alias instead of "../../..." — specifically change the
jest.mock target '../../src/services/analysis.service' to the absolute path
(e.g. 'src/services/analysis.service' or your repo's alias) and do the same for
the other relative import used in the file (the one referencing the same service
in lines importing pollAnalysis); keep the mocked symbol name pollAnalysis and
ensure Jest still finds the module under the absolute alias.
In `@shapeai/apps/mobile/tests/report/report-section-card.test.tsx`:
- Line 3: Replace the relative import of the ReportSectionCard component with
the project's configured absolute alias; locate the import statement that
references ReportSectionCard and change it to use the absolute path (e.g., the
project's alias for the src/components/report module) so tests import
ReportSectionCard via the absolute import rather than
"../../src/components/report/ReportSectionCard".
In `@shapeai/apps/mobile/tests/report/report.screen.test.tsx`:
- Around line 9-10: Replace the relative import paths in the test so they use
the project's absolute alias imports: update the jest.mock call that references
'../../src/services/analysis.service' to use the absolute alias for the services
folder, and change any other relative imports in this file (the import(s) around
lines that reference getAnalysisResult and the other imports near lines 24-25)
to their corresponding absolute alias paths per the project's tsconfig/jsconfig
aliases; ensure the mocked symbol getAnalysisResult and any test imports still
resolve correctly after switching to aliases.
In `@shapeai/apps/mobile/tests/workout/exercise-item.test.tsx`:
- Line 3: Replace the relative import of the ExerciseItem component in the test
with the app's absolute import alias: change the import of ExerciseItem
(currently '../../src/components/workout/ExerciseItem') to the absolute path
used by the project (e.g., src/components/workout/ExerciseItem or the app alias
configured in tsconfig/webpack) so the test uses the canonical absolute import
style for ExerciseItem.
In `@shapeai/apps/mobile/tests/workout/workout-plan.screen.test.tsx`:
- Around line 9-10: In workout-plan.screen.test.tsx replace all relative import
strings with the project's absolute alias imports: update the jest.mock call
that references getAnalysisResult (currently mocking
'../../src/services/analysis.service') and the other relative imports on the
same file to use the configured absolute aliases (the ones your tsconfig/webpack
uses) so the mock target and any imported modules resolve via absolute paths
instead of '../../...'; ensure the jest.mock target string and any import
statements that referenced '../../...' are updated consistently to the absolute
path form.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8ba1226f-30fd-4aed-879b-aa5b4421843b
⛔ Files ignored due to path filters (1)
shapeai/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
shapeai/apps/mobile/__mocks__/expo-import-meta-registry.jsshapeai/apps/mobile/__mocks__/expo-winter.jsshapeai/apps/mobile/app/(app)/analysis/[id]/report.tsxshapeai/apps/mobile/app/(app)/analysis/[id]/workout.tsxshapeai/apps/mobile/jest.config.jsshapeai/apps/mobile/jest.setup.jsshapeai/apps/mobile/src/components/report/ReportSectionCard.tsxshapeai/apps/mobile/src/components/workout/ExerciseItem.tsxshapeai/apps/mobile/src/components/workout/WorkoutDayCard.tsxshapeai/apps/mobile/src/services/analysis.service.tsshapeai/apps/mobile/src/services/profile.service.tsshapeai/apps/mobile/tests/analysis/analysis.loading.test.tsxshapeai/apps/mobile/tests/report/report-section-card.test.tsxshapeai/apps/mobile/tests/report/report.screen.test.tsxshapeai/apps/mobile/tests/workout/exercise-item.test.tsxshapeai/apps/mobile/tests/workout/workout-plan.screen.test.tsxshapeai/package.json
✅ Files skipped from review due to trivial changes (5)
- shapeai/apps/mobile/mocks/expo-winter.js
- shapeai/apps/mobile/mocks/expo-import-meta-registry.js
- shapeai/apps/mobile/src/components/workout/WorkoutDayCard.tsx
- shapeai/apps/mobile/jest.setup.js
- shapeai/apps/mobile/src/components/report/ReportSectionCard.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- shapeai/package.json
- shapeai/apps/mobile/jest.config.js
| import { getAnalysisResult, AnalysisResult, BodyScores } from '../../../../src/services/analysis.service' | ||
| import ReportSectionCard from '../../../../src/components/report/ReportSectionCard' |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Replace relative imports with absolute imports.
Line 12 and Line 13 use deep relative paths, which violates the project import rule and makes refactors brittle.
As per coding guidelines, "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/report.tsx around lines 12 - 13,
Replace the deep relative imports in this file by using absolute module paths:
update the import of getAnalysisResult, AnalysisResult, BodyScores to import
from the absolute module "src/services/analysis.service" and update
ReportSectionCard to import from "src/components/report/ReportSectionCard";
ensure the import specifiers match your project's tsconfig/baseUrl or path alias
configuration so the symbols get resolved correctly and run a quick compile to
confirm no remaining relative '../../../../' paths remain.
| import { getAnalysisResult } from '../../../../src/services/analysis.service' | ||
| import { getUserProfile } from '../../../../src/services/profile.service' | ||
| import WorkoutDayCard from '../../../../src/components/workout/WorkoutDayCard' |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use absolute imports instead of deep relative imports.
Line 18 to Line 20 currently use relative paths and should be switched to the configured absolute import style.
As per coding guidelines, "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/workout.tsx around lines 18 -
20, Replace the deep relative imports for getAnalysisResult, getUserProfile and
WorkoutDayCard with the configured absolute import paths; locate the import
lines importing '../../../../src/services/analysis.service',
'../../../../src/services/profile.service', and
'../../../../src/components/workout/WorkoutDayCard' and change them to use
absolute imports (e.g., import { getAnalysisResult } from
'src/services/analysis.service', import { getUserProfile } from
'src/services/profile.service', import WorkoutDayCard from
'src/components/workout/WorkoutDayCard' or whatever project absolute alias is
configured).
| useEffect(() => { | ||
| if (!id) return | ||
| Promise.all([getAnalysisResult(id), getUserProfile()]) |
There was a problem hiding this comment.
Handle missing route id to prevent a stuck loading state.
At Line 32, returning early without updating state can leave loading true forever.
💡 Suggested fix
useEffect(() => {
- if (!id) return
+ if (!id) {
+ setError('Plano de treino não disponível.')
+ setLoading(false)
+ return
+ }
Promise.all([getAnalysisResult(id), getUserProfile()])
.then(([analysis, profile]) => {
setWeeks(analysis.workout_plan.weeks as unknown as WorkoutWeek[])
setOverallScore(calculateOverallScore(analysis.scores))
setGoal(profile.primary_goal)
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (!id) return | |
| Promise.all([getAnalysisResult(id), getUserProfile()]) | |
| useEffect(() => { | |
| if (!id) { | |
| setError('Plano de treino não disponível.') | |
| setLoading(false) | |
| return | |
| } | |
| Promise.all([getAnalysisResult(id), getUserProfile()]) | |
| .then(([analysis, profile]) => { | |
| setWeeks(analysis.workout_plan.weeks as unknown as WorkoutWeek[]) | |
| setOverallScore(calculateOverallScore(analysis.scores)) | |
| setGoal(profile.primary_goal) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/workout.tsx around lines 31 -
33, The useEffect early-return when id is falsy leaves the component's loading
state true; in the useEffect that references id, getAnalysisResult and
getUserProfile, update the logic so that when id is missing you explicitly set
loading (or an equivalent state like isLoading) to false (or set an error state)
and avoid starting the Promise.all; also ensure the Promise.all success and
failure handlers in the effect set loading to false and handle errors; locate
the useEffect, id variable, and functions getAnalysisResult/getUserProfile to
implement these state updates.
| jest.mock('../../src/services/analysis.service', () => ({ | ||
| pollAnalysis: jest.fn(), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Convert relative imports to absolute aliases.
Lines 9-10 and Lines 13-14 use relative paths; please migrate to absolute imports for consistency with repo standards.
As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.
Also applies to: 13-14
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/analysis/analysis.loading.test.tsx` around lines 9
- 10, Update the relative imports in the test so they use the project's absolute
import alias instead of "../../..." — specifically change the jest.mock target
'../../src/services/analysis.service' to the absolute path (e.g.
'src/services/analysis.service' or your repo's alias) and do the same for the
other relative import used in the file (the one referencing the same service in
lines importing pollAnalysis); keep the mocked symbol name pollAnalysis and
ensure Jest still finds the module under the absolute alias.
| @@ -0,0 +1,42 @@ | |||
| import React from 'react' | |||
| import { render } from '@testing-library/react-native' | |||
| import ReportSectionCard from '../../src/components/report/ReportSectionCard' | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Replace relative component import with absolute alias.
Line 3 should use the configured absolute import path instead of ../../....
As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/report/report-section-card.test.tsx` at line 3,
Replace the relative import of the ReportSectionCard component with the
project's configured absolute alias; locate the import statement that references
ReportSectionCard and change it to use the absolute path (e.g., the project's
alias for the src/components/report module) so tests import ReportSectionCard
via the absolute import rather than
"../../src/components/report/ReportSectionCard".
| jest.mock('../../src/services/analysis.service', () => ({ | ||
| getAnalysisResult: jest.fn(), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Switch test module imports to absolute aliases.
Lines 9-10 and Lines 24-25 use ../../... relative paths; please use the project absolute import aliases consistently.
As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.
Also applies to: 24-25
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/report/report.screen.test.tsx` around lines 9 - 10,
Replace the relative import paths in the test so they use the project's absolute
alias imports: update the jest.mock call that references
'../../src/services/analysis.service' to use the absolute alias for the services
folder, and change any other relative imports in this file (the import(s) around
lines that reference getAnalysisResult and the other imports near lines 24-25)
to their corresponding absolute alias paths per the project's tsconfig/jsconfig
aliases; ensure the mocked symbol getAnalysisResult and any test imports still
resolve correctly after switching to aliases.
| @@ -0,0 +1,74 @@ | |||
| import React from 'react' | |||
| import { render } from '@testing-library/react-native' | |||
| import ExerciseItem from '../../src/components/workout/ExerciseItem' | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use absolute import path in this test file.
Line 3 currently uses a relative path (../../...); switch to the app alias import convention.
As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/workout/exercise-item.test.tsx` at line 3, Replace
the relative import of the ExerciseItem component in the test with the app's
absolute import alias: change the import of ExerciseItem (currently
'../../src/components/workout/ExerciseItem') to the absolute path used by the
project (e.g., src/components/workout/ExerciseItem or the app alias configured
in tsconfig/webpack) so the test uses the canonical absolute import style for
ExerciseItem.
| jest.mock('../../src/services/analysis.service', () => ({ | ||
| getAnalysisResult: jest.fn(), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use absolute imports throughout this test file.
Lines 9-10, 13-14, and 17-19 should use absolute alias paths instead of relative ../../... imports.
As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.
Also applies to: 13-14, 17-19
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/workout/workout-plan.screen.test.tsx` around lines
9 - 10, In workout-plan.screen.test.tsx replace all relative import strings with
the project's absolute alias imports: update the jest.mock call that references
getAnalysisResult (currently mocking '../../src/services/analysis.service') and
the other relative imports on the same file to use the configured absolute
aliases (the ones your tsconfig/webpack uses) so the mock target and any
imported modules resolve via absolute paths instead of '../../...'; ensure the
jest.mock target string and any import statements that referenced '../../...'
are updated consistently to the absolute path form.
- Paywall screen with monthly/annual plans and RevenueCat integration - useSubscription hook with post-purchase polling - purchases.service.ts wrapping RevenueCat SDK - API Gateway: GET /subscription/status + POST /subscription/webhook - Camera redirects to paywall on 402 SUBSCRIPTION_REQUIRED - Home badge Free/Pro reflects live subscription status - 26 new tests (18 mobile + 8 backend), 65 total passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HistoryScreen: FlatList com paginação load-more, pull-to-refresh e estado vazio - AnalysisHistoryItem: data DD/MM/YYYY, badge Atual, top 2 development_areas - GET /analyses: paginação limit/offset + top_development_areas via LEFT JOIN reports - AnalysisSummary type adicionado ao shared package - listAnalyses() com suporte a paginação em analysis.service.ts - 12 novos testes mobile + 3 backend, 93 total passando Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rors
- PURCHASE_CANCELLED → PURCHASE_CANCELLED_ERROR (matches react-native-purchases types)
- Remove { size: true } from FileSystem.getInfoAsync (not in InfoOptions type)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (13)
shapeai/apps/mobile/tests/history/history.screen.test.tsx (1)
13-19: ⚡ Quick winUse absolute imports in this test file.
The relative paths in the mock and test imports violate the repo guideline for
**/*.{js,jsx,ts,tsx}files. Please switch these to the project’s absolute aliases for consistency.As per coding guidelines,
Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/history/history.screen.test.tsx` around lines 13 - 19, The test uses relative imports for the mocked service and HistoryScreen; update the jest.mock module string and the import statements to use the project's absolute import aliases instead of relative paths—replace the '../../src/services/analysis.service' mock target and the '../../app/(app)/history' and '../../src/services/analysis.service' import strings with the repository's absolute module aliases used elsewhere so that listAnalyses, HistoryScreen and the mocked analysis.service resolve via absolute imports (keep the existing jest.mock call and the named import listAnalyses unchanged).shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx (1)
1-4: ⚡ Quick winUse absolute imports here as well.
AnalysisHistoryItemis imported through a relative path, which conflicts with the repo guideline for TypeScript/TSX files. Please switch to the appropriate absolute alias.As per coding guidelines,
Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx` around lines 1 - 4, The test imports AnalysisHistoryItem via a relative path; change that to use the project's TypeScript path alias (use the same absolute import pattern used elsewhere in the test suite) so the import of AnalysisHistoryItem uses the repo's absolute alias instead of '../../src/components/history/AnalysisHistoryItem'; update the import statement in AnalysisHistoryItem.test.tsx to reference the component by its configured absolute alias (matching other files) and run the tests to confirm resolution.shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx (1)
18-30: ⚡ Quick winUse absolute imports in the new test too.
These mocks and imports still use
../../...paths, which conflicts with the repo's TS/TSX import rule.As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx` around lines 18 - 30, Update the test to use absolute imports instead of relative paths: change the jest.mock calls that reference '../../src/services/analysis.service' and '../../src/components/camera/HumanSilhouette' to their absolute module paths, and update the import of CameraScreen and startAnalysis to use the same absolute import roots; keep the mocked symbols startAnalysis, uploadPhoto, triggerProcessing and the HumanSilhouette mock and ensure CameraScreen import still refers to the default export from the app camera module.shapeai/apps/mobile/app/(app)/camera.tsx (1)
1-15: ⚡ Quick winUse absolute imports here.
This file still uses relative paths for local modules, which conflicts with the repo rule for
**/*.{js,jsx,ts,tsx}and makes the new screen inconsistent with the rest of the app.As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/camera.tsx around lines 1 - 15, This file uses relative imports for local modules (e.g., startAnalysis, uploadPhoto, triggerProcessing from ../../src/services/analysis.service and HumanSilhouette from ../../src/components/camera/HumanSilhouette); update these to use the project's absolute import paths (replace ../../src/... with the configured absolute root paths used across the repo) so the CameraView screen follows the coding guideline for **/*.{js,jsx,ts,tsx}; ensure you update all occurrences in this file (startAnalysis, uploadPhoto, triggerProcessing, HumanSilhouette) to the correct absolute module specifiers and verify TypeScript resolves them.shapeai/apps/mobile/tests/auth/auth.store.test.ts (1)
1-21: ⚡ Quick winUse absolute imports in the auth store test.
The test still relies on
../../...paths for app code, which violates the repo’s import rule.As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/auth/auth.store.test.ts` around lines 1 - 21, Replace the relative import paths in the test with the project's absolute-module imports: update the mocked module imports for purchases.service and supabase.client and the imports for useAuthStore and supabase to use the repo's configured absolute aliases (instead of '../../src/services/purchases.service', '../../src/services/supabase.client', '../../src/stores/auth.store'); ensure the jest.mock calls and the import statements reference the same absolute module names so the mocks apply correctly to useAuthStore and supabase in auth.store.test.ts.shapeai/apps/mobile/src/stores/auth.store.ts (1)
1-4: ⚡ Quick winUse absolute imports in the auth store.
This new store still pulls local modules through relative paths, which violates the repo's TS import rule and makes the app code inconsistent.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/src/stores/auth.store.ts` around lines 1 - 4, The auth store uses relative imports; update the top-of-file imports in auth.store.ts to use the project's absolute import paths for the local modules (replace imports for supabase and purchases.service that currently reference '../services/supabase.client' and '../services/purchases.service' with their absolute equivalents) while leaving third-party imports (create from 'zustand' and Session from '@supabase/supabase-js') unchanged; ensure the named symbols supabase, purchasesLogIn, and purchasesLogOut are imported via the absolute paths so the file conforms to the repository TS import rule.shapeai/apps/mobile/app/_layout.tsx (1)
1-5: ⚡ Quick winUse absolute imports in the root layout.
This file still uses relative paths for local modules, which breaks the repo’s TS/TSX import convention.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/_layout.tsx` around lines 1 - 5, The root layout is using relative imports for local modules (useAuthStore, configurePurchases); change those to absolute module specifiers per project convention (e.g., import { useAuthStore } from 'src/stores/auth.store' and import { configurePurchases } from 'src/services/purchases.service') while leaving external imports (react, expo-router, expo-status-bar) unchanged so the file uses absolute imports for local code.shapeai/apps/mobile/tests/subscription/home.screen.test.tsx (1)
4-17: ⚡ Quick winUse absolute imports in the home screen test.
This test still uses relative paths for the mocked app modules and the screen under test, which conflicts with the repo import rule.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/subscription/home.screen.test.tsx` around lines 4 - 17, The test uses relative module imports for the HomeScreen and mocked modules; update the jest.mock and import statements that reference useSubscription, useAuthStore and HomeScreen to use the project's absolute import aliases instead of relative paths (e.g., change the mocked module specifiers and the HomeScreen import to the repo's absolute module names), ensuring the mocked symbols (useSubscription, useAuthStore, and the exported HomeScreen component) are still correctly referenced.shapeai/apps/mobile/app/(app)/paywall.tsx (1)
1-11: 💤 Low valueSwitch these module specifiers to absolute imports.
The imports still use
../../...paths.As per coding guidelines, use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/paywall.tsx around lines 1 - 11, Replace the relative import specifiers in paywall.tsx with the project's absolute import paths: change "../../src/services/purchases.service" imports (getOfferings, purchasePackage, restorePurchases, PURCHASES_ERROR_CODE and PurchasesPackage) to the absolute module path your project uses for services, and change "../../src/hooks/useSubscription" to the absolute hooks path so that useSubscription is imported via the absolute alias; keep the existing symbol names (getOfferings, purchasePackage, restorePurchases, PURCHASES_ERROR_CODE, PurchasesPackage, useSubscription) unchanged so references in the file still resolve.shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx (1)
1-24: 💤 Low valueSwitch these module specifiers to absolute imports.
The mocks/imports still use
../../...paths.As per coding guidelines, use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx` around lines 1 - 24, Update the test to use absolute module specifiers: change the jest.mock calls that reference '../../src/services/purchases.service' and '../../src/hooks/useSubscription' to use their absolute paths (e.g., 'src/services/purchases.service' and 'src/hooks/useSubscription'), and update the PaywallScreen import from '../../app/(app)/paywall' to the absolute import (e.g., 'app/(app)/paywall'); keep the same mock implementations and the existing references to router, getOfferings, purchasePackage, restorePurchases, PURCHASES_ERROR_CODE, and useSubscription so only the module specifier strings are modified.shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts (1)
1-9: 💤 Low valueSwitch these module specifiers to absolute imports.
The mocks/imports still use
../../...paths.As per coding guidelines, use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts` around lines 1 - 9, The test currently uses relative module specifiers for the DB client mock/imports; update the vi.mock and import statements that reference '../../src/db/client' to use the project's absolute import path (e.g., 'src/db/client') so that vi.mock('../../src/db/client', ...) and import { pool } from '../../src/db/client' become vi.mock('src/db/client', ...) and import { pool } from 'src/db/client'; ensure the local alias mockPool (const mockPool = pool as unknown as { query: ReturnType<typeof vi.fn> }) and any references to pool/vi.mock remain unchanged except for the module specifier.shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts (1)
1-8: 💤 Low valueSwitch these module specifiers to absolute imports.
The mocks/imports still use
../../...paths.As per coding guidelines, use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts` around lines 1 - 8, Update the test to use absolute module specifiers instead of relative paths: replace imports and the vi.mock call that reference '../../src/db/client' and '../../src/services/subscription.service' with the project's absolute paths (e.g., 'src/db/client' and 'src/services/subscription.service') so the mocked module and the imported symbols pool and getSubscriptionStatus are resolved via absolute imports; update any other '../../...' specifiers in this file to their corresponding absolute 'src/...' equivalents.shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx (1)
1-30: 💤 Low valueSwitch these module specifiers to absolute imports.
The mocks/imports still use
../../...paths.As per coding guidelines, use absolute imports instead of relative imports in all code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` around lines 1 - 30, Update the test to use absolute module specifiers instead of relative paths: change all mocks and imports that reference '../../src/services/purchases.service', '../../src/hooks/useSubscription', and '../../app/(app)/paywall' to their absolute equivalents (e.g., 'src/services/purchases.service', 'src/hooks/useSubscription', and 'app/(app)/paywall' or your project's absolute module roots). Specifically update the jest.mock calls and the import statements for PaywallScreen, purchasePackage/restorePurchases/PURCHASES_ERROR_CODE, and useSubscription so they import from absolute module names matching the app's module resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@shapeai/.env.example`:
- Around line 7-15: The .env example uses S3_BUCKET_NAME but the S3 service
expects AWS_S3_BUCKET (see services/api-gateway/src/services/s3.service.ts),
causing an undefined bucket at runtime; update the .env.example to rename
S3_BUCKET_NAME to AWS_S3_BUCKET (keeping the same value) so the environment
variable matches the symbol referenced by the S3 code, or alternatively change
the s3.service.ts code to read S3_BUCKET_NAME—pick one consistent name and apply
it across the .env.example and the s3.service.ts configuration reference.
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 12-14: The useEffect currently calls initialize() but ignores its
returned unsubscribe; modify the effect so it returns the cleanup function from
initialize() (i.e., call initialize() and return its result) so the auth
listener is unsubscribed on unmount/remount; update the effect that references
initialize in the dependency array to return the unsubscribe instead of
discarding it.
In `@shapeai/apps/mobile/app/`(app)/paywall.tsx:
- Around line 133-136: The save badge text is hard-coded; compute the discount
from the pricing data returned by getOfferings() and render that instead of
"Economize 37%". Locate the pricing values used to display monthly and annual
plans in paywall.tsx (the code that calls getOfferings() and the variables
holding monthlyPrice and annualPrice or the offering objects) and calculate
percent = Math.round((1 - annualPrice / (monthlyPrice * 12)) * 100); replace the
static Text content inside the saveBadge (styles.saveBadgeText) with a string
built from that percent (e.g., `Economize ${percent}%`), and ensure you guard
against missing/zero prices by falling back to no badge or a safe default.
Ensure formatting and localization as needed when converting the number to a
string.
In `@shapeai/apps/mobile/src/hooks/useSubscription.ts`:
- Around line 24-45: pollUntilPro currently uses setInterval which can overlap
requests and resolves even when status never becomes 'pro'; replace it with a
sequential async loop that awaits apiGet('/subscription/status') each iteration,
calls setSubscription(status), and returns immediately when status.status ===
'pro'; after exhausting options.maxAttempts throw or reject (do not resolve) to
signal timeout; update the pollUntilPro implementation (and its Promise
signature) to be an async function that loops (for or while) with await on
apiGet, uses options.intervalMs via an awaited delay between attempts,
references pollUntilPro, apiGet, and setSubscription, and removes the
setInterval-based clearInterval logic.
In `@shapeai/apps/mobile/src/services/purchases.service.ts`:
- Around line 17-36: The paywall helpers (getOfferings, purchasePackage,
restorePurchases) call the RevenueCat SDK unconditionally and will crash when
EXPO_PUBLIC_REVENUECAT_KEY is missing; add the same early-exit guard used in
purchasesLogIn/purchasesLogOut to each of these functions: check
process.env.EXPO_PUBLIC_REVENUECAT_KEY at the top of getOfferings,
purchasePackage, and restorePurchases and short-circuit when missing—return a
safe no-op result (e.g., an empty/safe PurchasesOfferings for getOfferings and a
resolved no-op or a rejected Promise with a clear, descriptive error for
purchasePackage and restorePurchases) so the app won't blow up when RevenueCat
is disabled.
In `@shapeai/services/api-gateway/src/routes/analyses.ts`:
- Around line 136-137: Validate parsed pagination values from
request.query.limit and request.query.offset before using them: parse with radix
10, check isNaN and that offset >= 0 and limit is between 1 and 50; if invalid
return a 400 Bad Request (e.g., reply.code(400) / throw) or normalize by
clamping (limit = Math.min(Math.max(parsedLimit, 1), 50); offset =
Math.max(parsedOffset, 0)). Update the logic around the limit and offset
variables in analyses.ts so malformed values cannot reach the DB.
- Around line 186-213: The three DB writes (the UPDATE on analyses, INSERT into
reports, and INSERT into workout_plans) must be executed inside a single
transaction so the completion callback is atomic; obtain a client from pool (use
pool.connect()), BEGIN a transaction, run the three queries (the query that
updates analyses status/photo deletion, the INSERT ... ON CONFLICT for reports,
and the INSERT ... ON CONFLICT for workout_plans), then COMMIT; on any error
ROLLBACK and rethrow the error, finally release the client. Locate the calls to
pool.query that run the UPDATE analyses, INSERT INTO reports, and INSERT INTO
workout_plans and replace them with transactional client.query usage and proper
error handling/cleanup.
In `@shapeai/services/api-gateway/src/routes/subscription.ts`:
- Around line 18-28: The webhook handler in the app.post('/subscription/webhook'
...) currently casts request.body to { event: RevenueCatEvent } and destructures
body.event directly, which can throw on malformed payloads; add a runtime
validation (using an existing schema lib like zod or a simple guard) to check
that request.body && request.body.event is an object and that event.type is a
string (one of expected values) and event.expiration_at_ms is either a
number/null/undefined before destructuring or calling new
Date(...).toISOString(); if validation fails return a 400 with a clear error
instead of proceeding to DB logic in this handler (referencing request.body,
RevenueCatEvent, type, expiration_at_ms, and the
app.post('/subscription/webhook' route).
---
Nitpick comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 1-5: The root layout is using relative imports for local modules
(useAuthStore, configurePurchases); change those to absolute module specifiers
per project convention (e.g., import { useAuthStore } from
'src/stores/auth.store' and import { configurePurchases } from
'src/services/purchases.service') while leaving external imports (react,
expo-router, expo-status-bar) unchanged so the file uses absolute imports for
local code.
In `@shapeai/apps/mobile/app/`(app)/camera.tsx:
- Around line 1-15: This file uses relative imports for local modules (e.g.,
startAnalysis, uploadPhoto, triggerProcessing from
../../src/services/analysis.service and HumanSilhouette from
../../src/components/camera/HumanSilhouette); update these to use the project's
absolute import paths (replace ../../src/... with the configured absolute root
paths used across the repo) so the CameraView screen follows the coding
guideline for **/*.{js,jsx,ts,tsx}; ensure you update all occurrences in this
file (startAnalysis, uploadPhoto, triggerProcessing, HumanSilhouette) to the
correct absolute module specifiers and verify TypeScript resolves them.
In `@shapeai/apps/mobile/app/`(app)/paywall.tsx:
- Around line 1-11: Replace the relative import specifiers in paywall.tsx with
the project's absolute import paths: change
"../../src/services/purchases.service" imports (getOfferings, purchasePackage,
restorePurchases, PURCHASES_ERROR_CODE and PurchasesPackage) to the absolute
module path your project uses for services, and change
"../../src/hooks/useSubscription" to the absolute hooks path so that
useSubscription is imported via the absolute alias; keep the existing symbol
names (getOfferings, purchasePackage, restorePurchases, PURCHASES_ERROR_CODE,
PurchasesPackage, useSubscription) unchanged so references in the file still
resolve.
In `@shapeai/apps/mobile/src/stores/auth.store.ts`:
- Around line 1-4: The auth store uses relative imports; update the top-of-file
imports in auth.store.ts to use the project's absolute import paths for the
local modules (replace imports for supabase and purchases.service that currently
reference '../services/supabase.client' and '../services/purchases.service' with
their absolute equivalents) while leaving third-party imports (create from
'zustand' and Session from '@supabase/supabase-js') unchanged; ensure the named
symbols supabase, purchasesLogIn, and purchasesLogOut are imported via the
absolute paths so the file conforms to the repository TS import rule.
In `@shapeai/apps/mobile/tests/auth/auth.store.test.ts`:
- Around line 1-21: Replace the relative import paths in the test with the
project's absolute-module imports: update the mocked module imports for
purchases.service and supabase.client and the imports for useAuthStore and
supabase to use the repo's configured absolute aliases (instead of
'../../src/services/purchases.service', '../../src/services/supabase.client',
'../../src/stores/auth.store'); ensure the jest.mock calls and the import
statements reference the same absolute module names so the mocks apply correctly
to useAuthStore and supabase in auth.store.test.ts.
In `@shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx`:
- Around line 1-4: The test imports AnalysisHistoryItem via a relative path;
change that to use the project's TypeScript path alias (use the same absolute
import pattern used elsewhere in the test suite) so the import of
AnalysisHistoryItem uses the repo's absolute alias instead of
'../../src/components/history/AnalysisHistoryItem'; update the import statement
in AnalysisHistoryItem.test.tsx to reference the component by its configured
absolute alias (matching other files) and run the tests to confirm resolution.
In `@shapeai/apps/mobile/tests/history/history.screen.test.tsx`:
- Around line 13-19: The test uses relative imports for the mocked service and
HistoryScreen; update the jest.mock module string and the import statements to
use the project's absolute import aliases instead of relative paths—replace the
'../../src/services/analysis.service' mock target and the
'../../app/(app)/history' and '../../src/services/analysis.service' import
strings with the repository's absolute module aliases used elsewhere so that
listAnalyses, HistoryScreen and the mocked analysis.service resolve via absolute
imports (keep the existing jest.mock call and the named import listAnalyses
unchanged).
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx`:
- Around line 1-30: Update the test to use absolute module specifiers instead of
relative paths: change all mocks and imports that reference
'../../src/services/purchases.service', '../../src/hooks/useSubscription', and
'../../app/(app)/paywall' to their absolute equivalents (e.g.,
'src/services/purchases.service', 'src/hooks/useSubscription', and
'app/(app)/paywall' or your project's absolute module roots). Specifically
update the jest.mock calls and the import statements for PaywallScreen,
purchasePackage/restorePurchases/PURCHASES_ERROR_CODE, and useSubscription so
they import from absolute module names matching the app's module resolution.
In `@shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx`:
- Around line 1-24: Update the test to use absolute module specifiers: change
the jest.mock calls that reference '../../src/services/purchases.service' and
'../../src/hooks/useSubscription' to use their absolute paths (e.g.,
'src/services/purchases.service' and 'src/hooks/useSubscription'), and update
the PaywallScreen import from '../../app/(app)/paywall' to the absolute import
(e.g., 'app/(app)/paywall'); keep the same mock implementations and the existing
references to router, getOfferings, purchasePackage, restorePurchases,
PURCHASES_ERROR_CODE, and useSubscription so only the module specifier strings
are modified.
In `@shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx`:
- Around line 18-30: Update the test to use absolute imports instead of relative
paths: change the jest.mock calls that reference
'../../src/services/analysis.service' and
'../../src/components/camera/HumanSilhouette' to their absolute module paths,
and update the import of CameraScreen and startAnalysis to use the same absolute
import roots; keep the mocked symbols startAnalysis, uploadPhoto,
triggerProcessing and the HumanSilhouette mock and ensure CameraScreen import
still refers to the default export from the app camera module.
In `@shapeai/apps/mobile/tests/subscription/home.screen.test.tsx`:
- Around line 4-17: The test uses relative module imports for the HomeScreen and
mocked modules; update the jest.mock and import statements that reference
useSubscription, useAuthStore and HomeScreen to use the project's absolute
import aliases instead of relative paths (e.g., change the mocked module
specifiers and the HomeScreen import to the repo's absolute module names),
ensuring the mocked symbols (useSubscription, useAuthStore, and the exported
HomeScreen component) are still correctly referenced.
In `@shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts`:
- Around line 1-8: Update the test to use absolute module specifiers instead of
relative paths: replace imports and the vi.mock call that reference
'../../src/db/client' and '../../src/services/subscription.service' with the
project's absolute paths (e.g., 'src/db/client' and
'src/services/subscription.service') so the mocked module and the imported
symbols pool and getSubscriptionStatus are resolved via absolute imports; update
any other '../../...' specifiers in this file to their corresponding absolute
'src/...' equivalents.
In
`@shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts`:
- Around line 1-9: The test currently uses relative module specifiers for the DB
client mock/imports; update the vi.mock and import statements that reference
'../../src/db/client' to use the project's absolute import path (e.g.,
'src/db/client') so that vi.mock('../../src/db/client', ...) and import { pool }
from '../../src/db/client' become vi.mock('src/db/client', ...) and import {
pool } from 'src/db/client'; ensure the local alias mockPool (const mockPool =
pool as unknown as { query: ReturnType<typeof vi.fn> }) and any references to
pool/vi.mock remain unchanged except for the module specifier.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 62a06e8a-6770-46a2-9913-75408e44ee2a
⛔ Files ignored due to path filters (1)
shapeai/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (32)
shapeai/.env.exampleshapeai/apps/mobile/app.jsonshapeai/apps/mobile/app/(app)/_layout.tsxshapeai/apps/mobile/app/(app)/camera.tsxshapeai/apps/mobile/app/(app)/history.tsxshapeai/apps/mobile/app/(app)/index.tsxshapeai/apps/mobile/app/(app)/paywall.tsxshapeai/apps/mobile/app/_layout.tsxshapeai/apps/mobile/jest.config.jsshapeai/apps/mobile/package.jsonshapeai/apps/mobile/src/components/history/AnalysisHistoryItem.tsxshapeai/apps/mobile/src/hooks/useSubscription.tsshapeai/apps/mobile/src/services/analysis.service.tsshapeai/apps/mobile/src/services/purchases.service.tsshapeai/apps/mobile/src/stores/auth.store.tsshapeai/apps/mobile/tests/auth/auth.store.test.tsshapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsxshapeai/apps/mobile/tests/history/history.screen.test.tsxshapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsxshapeai/apps/mobile/tests/paywall/paywall.screen.test.tsxshapeai/apps/mobile/tests/subscription/camera.subscription.test.tsxshapeai/apps/mobile/tests/subscription/home.screen.test.tsxshapeai/apps/mobile/tsconfig.jsonshapeai/package.jsonshapeai/packages/shared/src/types/analysis.types.tsshapeai/services/api-gateway/src/routes/analyses.tsshapeai/services/api-gateway/src/routes/subscription.tsshapeai/services/api-gateway/src/server.tsshapeai/services/api-gateway/src/services/subscription.service.tsshapeai/services/api-gateway/tests/analyses/analyses.history.test.tsshapeai/services/api-gateway/tests/subscription/subscription.routes.test.tsshapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts
✅ Files skipped from review due to trivial changes (6)
- shapeai/apps/mobile/tsconfig.json
- shapeai/apps/mobile/jest.config.js
- shapeai/apps/mobile/package.json
- shapeai/apps/mobile/app.json
- shapeai/package.json
- shapeai/packages/shared/src/types/analysis.types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- shapeai/apps/mobile/app/(app)/index.tsx
- shapeai/services/api-gateway/src/server.ts
- shapeai/apps/mobile/app/(app)/history.tsx
- shapeai/apps/mobile/app/(app)/_layout.tsx
- shapeai/apps/mobile/src/services/analysis.service.ts
| # API Gateway (services/api-gateway/.env) | ||
| PORT=3000 | ||
| DATABASE_URL=postgresql://user:password@localhost:5432/shapeai | ||
| SUPABASE_JWT_SECRET=your-jwt-secret | ||
| AWS_ACCESS_KEY_ID=your-aws-key | ||
| AWS_SECRET_ACCESS_KEY=your-aws-secret | ||
| AWS_REGION=us-east-1 | ||
| S3_BUCKET_NAME=shapeai-uploads | ||
| REVENUECAT_WEBHOOK_SECRET=whsec_xxx |
There was a problem hiding this comment.
Rename the S3 bucket env var to match the gateway.
shapeai/services/api-gateway/src/services/s3.service.ts reads AWS_S3_BUCKET, but this example publishes S3_BUCKET_NAME. Anyone following this template will end up with an undefined bucket at runtime.
♻️ Proposed fix
- S3_BUCKET_NAME=shapeai-uploads
+ AWS_S3_BUCKET=shapeai-uploads📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # API Gateway (services/api-gateway/.env) | |
| PORT=3000 | |
| DATABASE_URL=postgresql://user:password@localhost:5432/shapeai | |
| SUPABASE_JWT_SECRET=your-jwt-secret | |
| AWS_ACCESS_KEY_ID=your-aws-key | |
| AWS_SECRET_ACCESS_KEY=your-aws-secret | |
| AWS_REGION=us-east-1 | |
| S3_BUCKET_NAME=shapeai-uploads | |
| REVENUECAT_WEBHOOK_SECRET=whsec_xxx | |
| # API Gateway (services/api-gateway/.env) | |
| PORT=3000 | |
| DATABASE_URL=postgresql://user:password@localhost:5432/shapeai | |
| SUPABASE_JWT_SECRET=your-jwt-secret | |
| AWS_ACCESS_KEY_ID=your-aws-key | |
| AWS_SECRET_ACCESS_KEY=your-aws-secret | |
| AWS_REGION=us-east-1 | |
| AWS_S3_BUCKET=shapeai-uploads | |
| REVENUECAT_WEBHOOK_SECRET=whsec_xxx |
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 9-9: [UnorderedKey] The DATABASE_URL key should go before the PORT key
(UnorderedKey)
[warning] 11-11: [UnorderedKey] The AWS_ACCESS_KEY_ID key should go before the DATABASE_URL key
(UnorderedKey)
[warning] 12-12: [UnorderedKey] The AWS_SECRET_ACCESS_KEY key should go before the DATABASE_URL key
(UnorderedKey)
[warning] 13-13: [UnorderedKey] The AWS_REGION key should go before the AWS_SECRET_ACCESS_KEY key
(UnorderedKey)
[warning] 14-14: [UnorderedKey] The S3_BUCKET_NAME key should go before the SUPABASE_JWT_SECRET key
(UnorderedKey)
[warning] 15-15: [UnorderedKey] The REVENUECAT_WEBHOOK_SECRET key should go before the S3_BUCKET_NAME key
(UnorderedKey)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/.env.example` around lines 7 - 15, The .env example uses
S3_BUCKET_NAME but the S3 service expects AWS_S3_BUCKET (see
services/api-gateway/src/services/s3.service.ts), causing an undefined bucket at
runtime; update the .env.example to rename S3_BUCKET_NAME to AWS_S3_BUCKET
(keeping the same value) so the environment variable matches the symbol
referenced by the S3 code, or alternatively change the s3.service.ts code to
read S3_BUCKET_NAME—pick one consistent name and apply it across the
.env.example and the s3.service.ts configuration reference.
| useEffect(() => { | ||
| initialize() | ||
| }, [initialize]) |
There was a problem hiding this comment.
Return the cleanup from initialize().
initialize() already returns an unsubscribe function, but the effect discards it, so the auth listener stays alive across remounts/HMR.
♻️ Proposed fix
useEffect(() => {
- initialize()
+ return initialize()
}, [initialize])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| initialize() | |
| }, [initialize]) | |
| useEffect(() => { | |
| return initialize() | |
| }, [initialize]) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/_layout.tsx` around lines 12 - 14, The useEffect
currently calls initialize() but ignores its returned unsubscribe; modify the
effect so it returns the cleanup function from initialize() (i.e., call
initialize() and return its result) so the auth listener is unsubscribed on
unmount/remount; update the effect that references initialize in the dependency
array to return the unsubscribe instead of discarding it.
| <Text style={styles.priceLabel}>Anual</Text> | ||
| <View style={styles.saveBadge}> | ||
| <Text style={styles.saveBadgeText}>Economize 37%</Text> | ||
| </View> |
There was a problem hiding this comment.
Derive the savings badge from the loaded pricing.
"Economize 37%" is hard-coded while this screen reads plan prices from getOfferings(). If RevenueCat changes the annual price, the copy becomes inaccurate and the UI will advertise the wrong discount.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/paywall.tsx around lines 133 - 136, The save
badge text is hard-coded; compute the discount from the pricing data returned by
getOfferings() and render that instead of "Economize 37%". Locate the pricing
values used to display monthly and annual plans in paywall.tsx (the code that
calls getOfferings() and the variables holding monthlyPrice and annualPrice or
the offering objects) and calculate percent = Math.round((1 - annualPrice /
(monthlyPrice * 12)) * 100); replace the static Text content inside the
saveBadge (styles.saveBadgeText) with a string built from that percent (e.g.,
`Economize ${percent}%`), and ensure you guard against missing/zero prices by
falling back to no badge or a safe default. Ensure formatting and localization
as needed when converting the number to a string.
| // Polling pós-compra: verifica a cada 2s até status pro ou esgotamento (10s) | ||
| const pollUntilPro = useCallback( | ||
| (options = { intervalMs: 2000, maxAttempts: 5 }): Promise<void> => | ||
| new Promise((resolve) => { | ||
| let attempts = 0 | ||
| const interval = setInterval(async () => { | ||
| attempts++ | ||
| try { | ||
| const status = await apiGet<SubscriptionStatus>('/subscription/status') | ||
| setSubscription(status) | ||
| if (status.status === 'pro' || attempts >= options.maxAttempts) { | ||
| clearInterval(interval) | ||
| resolve() | ||
| } | ||
| } catch { | ||
| if (attempts >= options.maxAttempts) { | ||
| clearInterval(interval) | ||
| resolve() | ||
| } | ||
| } | ||
| }, options.intervalMs) | ||
| }), |
There was a problem hiding this comment.
Don't resolve polling on timeout.
pollUntilPro() currently resolves after maxAttempts even when the status never becomes pro, so the paywall proceeds as if entitlement was confirmed. The setInterval callback can also overlap requests while one fetch is still in flight. Switch to a sequential loop and reject or throw when the limit is hit.
Suggested shape for the fix
- const pollUntilPro = useCallback(
- (options = { intervalMs: 2000, maxAttempts: 5 }): Promise<void> =>
- new Promise((resolve) => {
- let attempts = 0
- const interval = setInterval(async () => {
- attempts++
- try {
- const status = await apiGet<SubscriptionStatus>('/subscription/status')
- setSubscription(status)
- if (status.status === 'pro' || attempts >= options.maxAttempts) {
- clearInterval(interval)
- resolve()
- }
- } catch {
- if (attempts >= options.maxAttempts) {
- clearInterval(interval)
- resolve()
- }
- }
- }, options.intervalMs)
- }),
- []
- )
+ const pollUntilPro = useCallback(
+ async (options = { intervalMs: 2000, maxAttempts: 5 }): Promise<void> => {
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
+ try {
+ const status = await apiGet<SubscriptionStatus>('/subscription/status')
+ setSubscription(status)
+ if (status.status === 'pro') return
+ } catch {
+ // retry until the attempt budget is exhausted
+ }
+
+ if (attempt < options.maxAttempts) {
+ await new Promise(resolve => setTimeout(resolve, options.intervalMs))
+ }
+ }
+
+ throw new Error('Subscription did not become pro in time')
+ },
+ []
+ )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/src/hooks/useSubscription.ts` around lines 24 - 45,
pollUntilPro currently uses setInterval which can overlap requests and resolves
even when status never becomes 'pro'; replace it with a sequential async loop
that awaits apiGet('/subscription/status') each iteration, calls
setSubscription(status), and returns immediately when status.status === 'pro';
after exhausting options.maxAttempts throw or reject (do not resolve) to signal
timeout; update the pollUntilPro implementation (and its Promise signature) to
be an async function that loops (for or while) with await on apiGet, uses
options.intervalMs via an awaited delay between attempts, references
pollUntilPro, apiGet, and setSubscription, and removes the setInterval-based
clearInterval logic.
| export async function purchasesLogIn(userId: string): Promise<void> { | ||
| if (!process.env.EXPO_PUBLIC_REVENUECAT_KEY) return | ||
| await Purchases.logIn(userId) | ||
| } | ||
|
|
||
| export async function purchasesLogOut(): Promise<void> { | ||
| if (!process.env.EXPO_PUBLIC_REVENUECAT_KEY) return | ||
| await Purchases.logOut() | ||
| } | ||
|
|
||
| export async function getOfferings(): Promise<PurchasesOfferings> { | ||
| return Purchases.getOfferings() | ||
| } | ||
|
|
||
| export async function purchasePackage(pkg: PurchasesPackage) { | ||
| return Purchases.purchasePackage(pkg) | ||
| } | ||
|
|
||
| export async function restorePurchases() { | ||
| return Purchases.restorePurchases() |
There was a problem hiding this comment.
Keep the RevenueCat disabled path consistent.
The module opts out of RevenueCat when EXPO_PUBLIC_REVENUECAT_KEY is missing for config/auth sync, but the paywall-facing helpers still call the SDK unconditionally. That means a missing key will still blow up once the app tries to load offerings or complete a purchase.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/src/services/purchases.service.ts` around lines 17 - 36,
The paywall helpers (getOfferings, purchasePackage, restorePurchases) call the
RevenueCat SDK unconditionally and will crash when EXPO_PUBLIC_REVENUECAT_KEY is
missing; add the same early-exit guard used in purchasesLogIn/purchasesLogOut to
each of these functions: check process.env.EXPO_PUBLIC_REVENUECAT_KEY at the top
of getOfferings, purchasePackage, and restorePurchases and short-circuit when
missing—return a safe no-op result (e.g., an empty/safe PurchasesOfferings for
getOfferings and a resolved no-op or a rejected Promise with a clear,
descriptive error for purchasePackage and restorePurchases) so the app won't
blow up when RevenueCat is disabled.
| it('navega para /(app)/paywall ao receber erro SUBSCRIPTION_REQUIRED', async () => { | ||
| mockStart.mockRejectedValueOnce(new Error('SUBSCRIPTION_REQUIRED')) | ||
| render(<CameraScreen />) | ||
|
|
||
| // Disparamos handleUploadAndProcess diretamente via mock interno | ||
| // O teste verifica que o router.push foi configurado corretamente | ||
| expect(mockPush).toBeDefined() | ||
| }) | ||
|
|
||
| it('não navega para paywall em erros genéricos', () => { | ||
| mockStart.mockRejectedValueOnce(new Error('HTTP 500')) | ||
| render(<CameraScreen />) | ||
| expect(mockPush).not.toHaveBeenCalledWith('/(app)/paywall') | ||
| }) |
There was a problem hiding this comment.
This test never reaches the paywall branch.
Both cases stop after render(<CameraScreen />), so the queued mockRejectedValueOnce never drives startAnalysis and router.push('/(app)/paywall') is never asserted. Simulate the capture/confirm flow or invoke the upload path directly so this branch is covered.
| const limit = Math.min(parseInt(request.query.limit ?? '10', 10), 50) | ||
| const offset = parseInt(request.query.offset ?? '0', 10) |
There was a problem hiding this comment.
Validate pagination inputs before querying.
parseInt() can yield NaN, so malformed limit/offset values currently flow into LIMIT/OFFSET and can turn a bad request into a DB error. Clamp or reject invalid inputs up front.
Suggested fix
- const limit = Math.min(parseInt(request.query.limit ?? '10', 10), 50)
- const offset = parseInt(request.query.offset ?? '0', 10)
+ const parsedLimit = Number.parseInt(request.query.limit ?? '10', 10)
+ const parsedOffset = Number.parseInt(request.query.offset ?? '0', 10)
+ if (Number.isNaN(parsedLimit) || Number.isNaN(parsedOffset) || parsedLimit < 1 || parsedOffset < 0) {
+ return reply.status(400).send({ error: 'Invalid pagination parameters' })
+ }
+ const limit = Math.min(parsedLimit, 50)
+ const offset = parsedOffset🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 136 - 137,
Validate parsed pagination values from request.query.limit and
request.query.offset before using them: parse with radix 10, check isNaN and
that offset >= 0 and limit is between 1 and 50; if invalid return a 400 Bad
Request (e.g., reply.code(400) / throw) or normalize by clamping (limit =
Math.min(Math.max(parsedLimit, 1), 50); offset = Math.max(parsedOffset, 0)).
Update the logic around the limit and offset variables in analyses.ts so
malformed values cannot reach the DB.
| // Atualiza análise com scores e status completed | ||
| await pool.query( | ||
| `UPDATE analyses | ||
| SET status = 'completed', scores = $1, completed_at = NOW(), | ||
| photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW() | ||
| WHERE id = $2`, | ||
| [JSON.stringify(scores), id] | ||
| ) | ||
|
|
||
| // Insere relatório | ||
| await pool.query( | ||
| `INSERT INTO reports (analysis_id, highlights, development_areas) | ||
| VALUES ($1, $2, $3) | ||
| ON CONFLICT (analysis_id) DO UPDATE | ||
| SET highlights = EXCLUDED.highlights, | ||
| development_areas = EXCLUDED.development_areas`, | ||
| [id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)] | ||
| ) | ||
|
|
||
| // Insere plano de treino | ||
| const weeks = workout_plan.weeks | ||
| await pool.query( | ||
| `INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks) | ||
| VALUES ($1, $2, $3, $4, $5) | ||
| ON CONFLICT (analysis_id) DO UPDATE | ||
| SET weeks = EXCLUDED.weeks`, | ||
| [id, userId, 4, 4, JSON.stringify(weeks)] | ||
| ) |
There was a problem hiding this comment.
Make the completion callback atomic.
analyses, reports, and workout_plans are written independently, so a failure after status = 'completed' or photo deletion can leave the pipeline partially persisted. Wrap these writes in a transaction so the callback is all-or-nothing.
Suggested fix
- await pool.query(
+ const client = await pool.connect()
+ try {
+ await client.query('BEGIN')
+
+ await client.query(
`UPDATE analyses
SET status = 'completed', scores = $1, completed_at = NOW(),
photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW()
WHERE id = $2`,
[JSON.stringify(scores), id]
)
- // Insere relatório
- await pool.query(
+ // Insere relatório
+ await client.query(
`INSERT INTO reports (analysis_id, highlights, development_areas)
VALUES ($1, $2, $3)
ON CONFLICT (analysis_id) DO UPDATE
SET highlights = EXCLUDED.highlights,
development_areas = EXCLUDED.development_areas`,
[id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)]
)
- // Insere plano de treino
- const weeks = workout_plan.weeks
- await pool.query(
+ // Insere plano de treino
+ const weeks = workout_plan.weeks
+ await client.query(
`INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (analysis_id) DO UPDATE
SET weeks = EXCLUDED.weeks`,
[id, userId, 4, 4, JSON.stringify(weeks)]
)
+
+ await client.query('COMMIT')
+ } catch (error) {
+ await client.query('ROLLBACK')
+ throw error
+ } finally {
+ client.release()
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Atualiza análise com scores e status completed | |
| await pool.query( | |
| `UPDATE analyses | |
| SET status = 'completed', scores = $1, completed_at = NOW(), | |
| photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW() | |
| WHERE id = $2`, | |
| [JSON.stringify(scores), id] | |
| ) | |
| // Insere relatório | |
| await pool.query( | |
| `INSERT INTO reports (analysis_id, highlights, development_areas) | |
| VALUES ($1, $2, $3) | |
| ON CONFLICT (analysis_id) DO UPDATE | |
| SET highlights = EXCLUDED.highlights, | |
| development_areas = EXCLUDED.development_areas`, | |
| [id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)] | |
| ) | |
| // Insere plano de treino | |
| const weeks = workout_plan.weeks | |
| await pool.query( | |
| `INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks) | |
| VALUES ($1, $2, $3, $4, $5) | |
| ON CONFLICT (analysis_id) DO UPDATE | |
| SET weeks = EXCLUDED.weeks`, | |
| [id, userId, 4, 4, JSON.stringify(weeks)] | |
| ) | |
| const client = await pool.connect() | |
| try { | |
| await client.query('BEGIN') | |
| // Atualiza análise com scores e status completed | |
| await client.query( | |
| `UPDATE analyses | |
| SET status = 'completed', scores = $1, completed_at = NOW(), | |
| photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW() | |
| WHERE id = $2`, | |
| [JSON.stringify(scores), id] | |
| ) | |
| // Insere relatório | |
| await client.query( | |
| `INSERT INTO reports (analysis_id, highlights, development_areas) | |
| VALUES ($1, $2, $3) | |
| ON CONFLICT (analysis_id) DO UPDATE | |
| SET highlights = EXCLUDED.highlights, | |
| development_areas = EXCLUDED.development_areas`, | |
| [id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)] | |
| ) | |
| // Insere plano de treino | |
| const weeks = workout_plan.weeks | |
| await client.query( | |
| `INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks) | |
| VALUES ($1, $2, $3, $4, $5) | |
| ON CONFLICT (analysis_id) DO UPDATE | |
| SET weeks = EXCLUDED.weeks`, | |
| [id, userId, 4, 4, JSON.stringify(weeks)] | |
| ) | |
| await client.query('COMMIT') | |
| } catch (error) { | |
| await client.query('ROLLBACK') | |
| throw error | |
| } finally { | |
| client.release() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 186 - 213,
The three DB writes (the UPDATE on analyses, INSERT into reports, and INSERT
into workout_plans) must be executed inside a single transaction so the
completion callback is atomic; obtain a client from pool (use pool.connect()),
BEGIN a transaction, run the three queries (the query that updates analyses
status/photo deletion, the INSERT ... ON CONFLICT for reports, and the INSERT
... ON CONFLICT for workout_plans), then COMMIT; on any error ROLLBACK and
rethrow the error, finally release the client. Locate the calls to pool.query
that run the UPDATE analyses, INSERT INTO reports, and INSERT INTO workout_plans
and replace them with transactional client.query usage and proper error
handling/cleanup.
| app.post('/subscription/webhook', async (request, reply) => { | ||
| const secret = request.headers['x-revenuecat-secret'] | ||
| if (!secret || secret !== process.env.REVENUECAT_WEBHOOK_SECRET) { | ||
| return reply.status(401).send({ error: 'Unauthorized' }) | ||
| } | ||
|
|
||
| const body = request.body as { event: RevenueCatEvent } | ||
| const { type, app_user_id, expiration_at_ms } = body.event | ||
|
|
||
| if (type === 'INITIAL_PURCHASE' || type === 'RENEWAL') { | ||
| const expiresAt = expiration_at_ms ? new Date(expiration_at_ms).toISOString() : null |
There was a problem hiding this comment.
Validate the webhook payload before destructuring it.
request.body is only cast, so a malformed RevenueCat call can throw at body.event or toISOString() and surface as a 500 instead of a clean 4xx. Add a runtime schema/guard for event, type, and expiration_at_ms before touching the DB.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/routes/subscription.ts` around lines 18 -
28, The webhook handler in the app.post('/subscription/webhook' ...) currently
casts request.body to { event: RevenueCatEvent } and destructures body.event
directly, which can throw on malformed payloads; add a runtime validation (using
an existing schema lib like zod or a simple guard) to check that request.body &&
request.body.event is an object and that event.type is a string (one of expected
values) and event.expiration_at_ms is either a number/null/undefined before
destructuring or calling new Date(...).toISOString(); if validation fails return
a 400 with a clear error instead of proceeding to DB logic in this handler
(referencing request.body, RevenueCatEvent, type, expiration_at_ms, and the
app.post('/subscription/webhook' route).
- POST /analyses/compare: calls Claude API via axios, returns summary+improvements+needs_attention - HistoryScreen: selection mode UI (Comparar → select 2 → Ver comparativo → navigate) - AnalysisHistoryItem: isSelectMode/isSelected/onSelect props with checkbox overlay - CompareScreen: side-by-side dates+scores, muscle group delta bars (▲▼), O que mudou section - Share button: Pro users share, Free users get paywall alert - _layout.tsx: registered compare route with href: null - 13 new tests (history.compare, compare.screen, analyses.compare) — 106 total passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ations [QA PASS] - Mobile: expo-notifications permission request + registerPushToken() service - Mobile: addNotificationResponseReceivedListener in _layout.tsx → deep link to /(app)/camera - Mobile: ProfileScreen with notifications toggle → PATCH /profile - Mobile: apiPatch() added to api.client.ts - Backend: POST /push-tokens route with upsert (ON CONFLICT token) - Backend: notification.service.ts — getEligibleUsers() SQL, pickTemplate(), sendReanalysisNotifications() - Backend: DeviceNotRegistered handling removes expired token from push_tokens table - Backend: node-cron job at 0 9 * * * + POST /internal/notifications/trigger for manual testing - Backend: migration 004_notifications.sql — adds notifications_enabled to user_profiles + push_tokens table - fix(jest): moduleNameMapper react path → ../../node_modules/react (hoisted to monorepo root) - 16 new tests (94 mobile + 28 backend = 122 total passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
♻️ Duplicate comments (2)
shapeai/services/api-gateway/src/routes/profile.ts (1)
45-54:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winWhitelist PATCH fields before generating SQL
SETclauses.Line 45 derives SQL column names from runtime input; that allows crafted keys to alter the query text.
Proposed fix
const updates = request.body - const fields = Object.keys(updates) as (keyof ProfileBody)[] + const allowedFields: (keyof ProfileBody)[] = [ + 'height_cm', + 'weight_kg', + 'biological_sex', + 'primary_goal', + 'notifications_enabled', + ] + const rawFields = Object.keys(updates) as string[] + const fields = rawFields.filter((f): f is keyof ProfileBody => + allowedFields.includes(f as keyof ProfileBody) + ) + + if (rawFields.length !== fields.length) { + return reply.status(400).send({ error: 'Invalid fields in update payload' }) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/src/routes/profile.ts` around lines 45 - 54, The code builds SQL SET clauses directly from runtime keys (fields from updates) which allows injection via crafted keys; modify the PATCH handler so you first define an explicit whitelist of allowed profile columns (e.g., const ALLOWED_PROFILE_FIELDS = ['display_name','bio','avatar_url', ...] as const), then filter fields = Object.keys(updates).filter(f => ALLOWED_PROFILE_FIELDS.includes(f as any)) as (keyof ProfileBody)[]; if filtered fields is empty return 400; then build setClauses and values from this filtered list (use the actual column names from the whitelist if they differ from property names), and call pool.query with those sanitized setClauses and values (ensure updated_at is still set and WHERE user_id = $1 remains unchanged).shapeai/apps/mobile/app/_layout.tsx (1)
14-16:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReturn the auth cleanup from the Line 14 effect.
If
initialize()returns an unsubscribe/teardown, not returning it leaks listeners across remounts.Proposed fix
useEffect(() => { - initialize() + return initialize() }, [initialize])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/_layout.tsx` around lines 14 - 16, The useEffect currently calls initialize() but does not return its teardown/unsubscribe; change the effect so it returns the cleanup from initialize: if initialize() is synchronous and returns a function, simply use useEffect(() => initialize(), [initialize]); if initialize() is async, call it inside the effect, capture the returned unsubscribe (e.g. let unsub; initialize().then(fn => { unsub = fn });) and return a cleanup () => { if (typeof unsub === 'function') unsub(); } so the listener is properly removed on unmount/remount; reference the useEffect and initialize symbols when making this change.
🟡 Minor comments (3)
shapeai/apps/mobile/app/(app)/compare.tsx-30-33 (1)
30-33:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against empty score objects in
calcOverall.When
scoresis empty, division by zero returnsNaNand leaks into UI badges.Proposed fix
function calcOverall(scores: Record<string, number>): number { const vals = Object.values(scores) + if (vals.length === 0) return 0 return Math.round(vals.reduce((s, v) => s + v, 0) / vals.length) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 30 - 33, calcOverall currently divides by vals.length without checking for an empty scores object, which yields NaN; update the calcOverall(scores: Record<string, number>) function to guard against empty input by checking if Object.values(scores).length === 0 and return a safe default (e.g., 0) before performing the reduce/divide/Math.round, so UI badges never receive NaN.shapeai/apps/mobile/tests/profile/profile.screen.test.tsx-44-48 (1)
44-48:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRe-query the switch before asserting reverted value.
The test stores
toggleonce on Line 44 and then checkstoggle.props.valueafter state updates. Re-query insidewaitForto avoid stale-node flakiness.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx` around lines 44 - 48, The test is using a stale reference to the switch stored in the variable `toggle` (from `getByTestId('toggle-notifications')`) before state updates; re-query the DOM inside the `waitFor` callback instead of using `toggle.props.value` to avoid flakiness — call `getByTestId('toggle-notifications')` inside `waitFor` and assert the new `.props.value` (or prefer toAssert via matcher on the element) to verify the reverted value after `fireEvent(toggle, 'valueChange', false)`.shapeai/apps/mobile/tests/compare/compare.screen.test.tsx-72-83 (1)
72-83:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAssert actual Free vs Pro share behavior, not just button presence.
On Line 72 and Line 77 tests only verify visibility. They won’t catch regressions where both plans execute the same share action. Trigger the share press and assert different outcomes per plan.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx` around lines 72 - 83, Update the two tests in compare.screen.test.tsx to assert behavior instead of mere visibility: in the Free test (rendering <CompareScreen /> with mockUseSubscription returning free) call fireEvent.press(getByTestId('btn-compartilhar')) and assert the Pro alert was shown (e.g., expect(Alert.alert / mockAlert toHaveBeenCalledWith or whatever project-mocked alert you use); ensure mockUseSubscription returns the free subscription for that test). In the Pro test (mockUseSubscription returns { status: 'pro' }) also fireEvent.press(getByTestId('btn-compartilhar')) and assert the share flow runs (e.g., expect(yourShareHandler or navigator/share mock toHaveBeenCalled and that no Pro alert was triggered, or check for presence of shared UI), keeping the existing getByText('Compartilhar') assertion if desired; ensure you restore or reconfigure mockUseSubscription per test so each scenario is isolated.
🧹 Nitpick comments (12)
shapeai/services/api-gateway/src/routes/profile.ts (1)
5-11: ⚡ Quick winSplit create and patch DTOs to match actual route behavior.
ProfileBodyrequiresnotifications_enabled, but POST doesn’t consume it. Define separateCreateProfileBodyandPatchProfileBodyto keep route contracts accurate.As per coding guidelines, "
**/*.ts: Verify TypeScript types are properly defined."Also applies to: 23-24, 42-42
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/src/routes/profile.ts` around lines 5 - 11, ProfileBody currently mixes POST and PATCH inputs; split it into CreateProfileBody (used by the POST/create route) and PatchProfileBody (used by the PATCH/update route). CreateProfileBody should include required fields used on creation (height_cm, weight_kg, biological_sex, primary_goal) and omit notifications_enabled if POST doesn't consume it; PatchProfileBody should make fields optional and include notifications_enabled since updates may set it. Update any route handlers or references in this file from ProfileBody to the appropriate new type names (CreateProfileBody for create handler and PatchProfileBody for patch handler) and adjust imports/exports accordingly.shapeai/apps/mobile/app/(app)/compare.tsx (1)
83-84: ⚡ Quick winRemove unsafe
unknowncasts for analysis scores.Line 83-84 bypasses type safety. Prefer modeling
scoresinAnalysisSummary(or a local typed extension) instead of casting throughunknown.As per coding guidelines, "
**/*.ts: Verify TypeScript types are properly defined."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 83 - 84, The current unsafe casts of a1/a2 to unknown to access .scores should be removed by adding a proper type for analysis scores (e.g., add scores: Record<string, number> to AnalysisSummary or declare a local interface AnalysisWithScores) and then using that type where the comparator receives items (replace the unknown casts in the comparator that defines scores1/scores2). Update the comparator signature or map input items to the new typed shape (use a single safe "as AnalysisWithScores" only after defining the interface) so you can access .scores without bypassing TypeScript safety.shapeai/apps/mobile/tests/compare/compare.screen.test.tsx (1)
18-20: ⚡ Quick winSwitch Line 18–Line 20 imports to absolute paths.
These relative imports violate the TS/TSX import rule and make cross-package moves harder.
As per coding guidelines "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx` around lines 18 - 20, Replace the relative imports on the test file for CompareScreen, apiGet/apiPost and useSubscription with their absolute module paths; update the import statements that reference '../../app/(app)/compare', '../../src/services/api.client', and '../../src/hooks/useSubscription' to use the project's configured absolute paths (keeping the same imported symbols CompareScreen, apiGet, apiPost, and useSubscription) so the test follows the "use absolute imports" guideline.shapeai/apps/mobile/tests/notifications/notification.service.test.ts (1)
1-1: ⚡ Quick winNormalize Line 1, Line 12, and Line 17 to absolute module paths.
Please align test imports/mocks with the absolute import convention used for TS files.
As per coding guidelines "Use absolute imports instead of relative imports in all code".
Also applies to: 12-12, 17-17
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/notifications/notification.service.test.ts` at line 1, The test imports use relative paths; update them to absolute module paths to follow the project convention — replace the relative import for registerPushToken in notification.service.test.ts and any other relative imports/mocks referenced around lines 12 and 17 with their corresponding absolute module paths (e.g., use the package-root alias or tsconfig baseUrl paths for the service and mock modules), ensuring imports reference the same exported symbols (registerPushToken, and the mocks) but via absolute paths so TypeScript resolves them consistently.shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts (1)
7-7: ⚡ Quick winUse an absolute import on Line 7.
Please align with the TS import rule used across the repo.
As per coding guidelines "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts` at line 7, Replace the relative import of the DB client with an absolute import: change "import { pool } from '../../src/db/client'" to use the project's absolute path (e.g. "import { pool } from 'src/db/client'") so the symbol pool is imported via the repo's TS absolute import convention used across tests and source code.shapeai/apps/mobile/app/_layout.tsx (1)
6-7: ⚡ Quick winConvert Line 6–Line 7 imports to absolute paths.
Please align with the repository-wide TS import convention.
As per coding guidelines "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/_layout.tsx` around lines 6 - 7, The imports in _layout.tsx use relative paths for useAuthStore and configurePurchases; update them to the project’s absolute TS import style by replacing '../src/stores/auth.store' and '../src/services/purchases.service' with their absolute module paths (e.g., starting with the repo alias such as '@/stores/auth.store' and '@/services/purchases.service') so that useAuthStore and configurePurchases are imported via absolute imports consistent with the codebase convention.shapeai/apps/mobile/tests/profile/profile.screen.test.tsx (1)
13-14: ⚡ Quick winUse absolute imports on Line 13 and Line 14.
These relative imports break the repo import convention for TS/TSX code.
As per coding guidelines "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx` around lines 13 - 14, Replace the two relative imports at the top of the test that pull in ProfileScreen and apiGet/apiPatch with project-root absolute imports to follow the TS/TSX import convention; locate the import statements referencing '../../app/(app)/profile' (ProfileScreen) and '../../src/services/api.client' (apiGet, apiPatch) and update them to use the repository's absolute module paths (e.g., the app module path for ProfileScreen and the src/services api.client path) so the test uses absolute imports instead of relative paths.shapeai/apps/mobile/app/(app)/profile.tsx (1)
4-4: ⚡ Quick winReplace Line 4 with an absolute import.
../../src/services/api.clientshould follow the repo TS import convention.As per coding guidelines "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/profile.tsx at line 4, The import on line 4 uses a relative path; change it to the repo's absolute import form so the two symbols (apiGet, apiPatch) come from the absolute module instead of '../../src/services/api.client'. Open the file containing the import of apiGet and apiPatch and replace the module specifier with the project's absolute import (e.g., 'src/services/api.client' per TS import convention), keeping the imported identifiers unchanged; ensure TypeScript resolves the path via existing tsconfig paths if required.shapeai/apps/mobile/src/services/notification.service.ts (1)
3-3: ⚡ Quick winUse an absolute import on Line 3.
./api.clientshould follow the repo-wide absolute import convention.As per coding guidelines "Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/src/services/notification.service.ts` at line 3, Change the relative import in notification.service.ts from './api.client' to the repo's absolute import form used elsewhere (e.g., import { apiPost } from 'services/api.client' or the project's configured path alias), so update the import statement for apiPost in the notification service to use the project's absolute path convention and ensure it matches tsconfig/webpack path aliases if applicable.shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx (1)
18-18: ⚡ Quick winUse absolute module paths on Line 18, Line 24, and Line 30.
Please align these test imports/mocks with the TS/TSX absolute import convention.
As per coding guidelines "Use absolute imports instead of relative imports in all code".
Also applies to: 24-24, 30-30
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx` at line 18, The tests currently mock/import modules using relative paths (e.g., '../../src/stores/auth.store', '../../src/stores/user.store', '../../src/utils/notifications'); update these to use the project's TypeScript absolute import convention (replace '../../src/stores/auth.store' with the absolute path like 'src/stores/auth.store', do the same for the user store and notifications imports) so the jest.mock and import statements in notification.handler.test.tsx use absolute module paths consistent with the TS/TSX config.shapeai/services/api-gateway/src/routes/push-tokens.ts (1)
2-3: ⚡ Quick winUse absolute imports for internal modules.
Please replace these relative imports with the project’s absolute import style.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/services/api-gateway/src/routes/push-tokens.ts` around lines 2 - 3, The imports in this file use relative paths; replace the two relative imports ("import { pool } from '../db/client'" and "import { requireAuth } from '../middleware/auth'") with the project's absolute import style (use the configured path alias used across the repo) so they resolve via the project's tsconfig/module-alias setup; update the import specifiers for pool and requireAuth accordingly and verify the build/tsconfig path mapping supports the chosen absolute base.shapeai/apps/mobile/app/(app)/index.tsx (1)
3-4: ⚡ Quick winUse absolute imports instead of relative imports.
Please switch these imports to your configured absolute alias/path style to keep module resolution consistent across the app.
As per coding guidelines,
**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/app/`(app)/index.tsx around lines 3 - 4, The imports for useAuthStore and useSubscription are using relative paths; update them to use the project's configured absolute import alias (the alias set in your tsconfig/jsconfig, e.g. "@/..." or "src/...") so module resolution is consistent. Specifically, replace the relative import lines that reference '../../src/stores/auth.store' and '../../src/hooks/useSubscription' with their equivalent absolute-alias imports so useAuthStore and useSubscription are imported via the app's absolute path scheme.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 14-23: The startup flow calls initialize() but never invokes
registerPushToken(), so device tokens are never upserted; update the layout
startup/auth-ready flow to call registerPushToken() after initialize() (or once
auth is confirmed) and handle/await errors; specifically, inside the same
useEffect that calls initialize() (or a new effect that depends on
initialize/auth-ready state) call registerPushToken() and log or surface
failures, ensuring you don’t duplicate calls and that existing
Notifications.addNotificationResponseReceivedListener behavior is preserved.
- Around line 19-21: The notification handler registered with
Notifications.addNotificationResponseReceivedListener ignores the response and
always routes to '/(app)/camera'; update the listener callback to read the
response parameter (the NotificationResponse) and extract the deep-link route
from response.notification.request.content.data (or the relevant payload key),
then call router.push with that route (falling back to '/(app)/camera' if the
payload route is missing). Ensure you reference the existing listener setup
(Notifications.addNotificationResponseReceivedListener) and the router.push call
when implementing the change.
In `@shapeai/apps/mobile/app/`(app)/compare.tsx:
- Around line 4-5: Update the two relative imports in compare.tsx to use
absolute paths: replace the import of apiGet and apiPost from
'../../src/services/api.client' with the absolute module path that exports
apiGet and apiPost (referenced symbol names: apiGet, apiPost) and replace the
import of useSubscription from '../../src/hooks/useSubscription' with its
absolute path (symbol: useSubscription); keep the imported symbol names
unchanged so existing usages in the file still resolve.
- Around line 47-62: The useEffect chain currently sets setIsLoading(false) only
on successful apiGet calls so a failure leaves isLoading true; update the
Promise.all(...) call to ensure setIsLoading(false) runs on both success and
failure (e.g., add a .catch(err => { setResult(null); throw err }) before
continuing or move setIsLoading(false) into a .finally() directly after
Promise.all), and ensure setIsComparing is cleared in all paths (use .finally(()
=> setIsComparing(false)) for the compare request and also call
setIsComparing(false) if Promise.all fails) so that setIsLoading and
setIsComparing are always reset when apiGet or apiPost fail; refer to useEffect,
apiGet, apiPost('/analyses/compare'), setA1, setA2, setResult, setIsLoading, and
setIsComparing to locate the changes.
- Around line 36-50: Validate runtime types/values of id1 and id2 (from
useLocalSearchParams) before using them in API calls: inside the useEffect (and
before any apiPost payload construction), add a guard that rejects undefined or
array values (e.g., !id1 || !id2 || Array.isArray(id1) || Array.isArray(id2)),
handle the error by setting isLoading/isComparing appropriately and returning
early (or navigate/redirect), and only call
apiGet(`/analyses/${id1}`)/apiGet(`/analyses/${id2}`) and any apiPost that uses
id1/id2 after the guard passes so the requests never contain "undefined" or
comma-separated arrays.
In `@shapeai/apps/mobile/src/services/api.client.ts`:
- Line 3: The code currently silently falls back to 'http://localhost:3000' when
process.env.EXPO_PUBLIC_API_URL is missing; change the API_URL initialization so
it fails fast: if process.env.EXPO_PUBLIC_API_URL is undefined and the app is
not running in explicit dev mode (e.g., check __DEV__ or a specific DEV flag),
throw an error explaining the missing EXPO_PUBLIC_API_URL; only allow the
localhost fallback when explicitly running in local development (guarded by
__DEV__ or process.env.NODE_ENV === 'development') and reference the API_URL
constant to locate where to implement this check.
- Line 1: The import in api.client.ts uses a relative path ("./supabase.client")
— replace it with an absolute import for the supabase client (e.g.,
"services/supabase.client" or your project's configured alias) so the line
importing the supabase symbol uses the absolute module path; update the import
that brings in supabase and verify your tsconfig/webpack path aliases are
configured so the new absolute path resolves.
- Around line 18-19: apiGet, apiPost, and apiPatch call res.json() unguarded
which throws on 204 No Content or non-JSON responses; update each function
(apiGet, apiPost, apiPatch) to first check if res.status === 204 or the
Content-Type header does not include "application/json" (or if
res.headers.get("content-length") === "0") and return null (or an appropriate
empty value) in that case, otherwise call and return await res.json(); ensure
you still handle non-ok responses before parsing so error paths remain
unchanged.
In `@shapeai/apps/mobile/tests/history/history.compare.test.tsx`:
- Around line 12-18: Replace the relative imports in this test with the
project's absolute import paths: update the jest.mock target
'../../src/services/analysis.service' to the absolute module path used by the
app (the module that exports listAnalyses), change the import of HistoryScreen
from '../../app/(app)/history' to the app's absolute screen path, and adjust the
import of listAnalyses and any other relative imports (e.g.,
'../../src/services/analysis.service') to their corresponding absolute module
specifiers so the test uses the same absolute module resolution as the
app/router; keep the same symbols (HistoryScreen, router, listAnalyses) and
ensure jest.mock references the absolute service module name.
- Line 38: The test currently calls fireEvent.press inside waitFor (using
waitFor(() => fireEvent.press(getByTestId('btn-comparar')))), which causes
repeated presses; instead use waitFor with a query/assert (e.g., waitFor(() =>
expect(getByTestId('btn-comparar')).toBeTruthy() or getByTestId('btn-comparar')
returns) to wait for the element, then call
fireEvent.press(getByTestId('btn-comparar')) once outside the waitFor; apply
this same change to the other occurrences that currently wrap fireEvent.press
with waitFor (lines referencing waitFor, fireEvent.press, and getByTestId).
In `@shapeai/services/api-gateway/src/routes/profile.ts`:
- Around line 2-3: The imports in profile.ts use relative paths; change them to
the project's absolute import aliases (e.g., replace "import { pool } from
'../db/client'" and "import { requireAuth } from '../middleware/auth'" with the
corresponding absolute imports such as "import { pool } from 'db/client'" and
"import { requireAuth } from 'middleware/auth'"), and if necessary ensure the
project's tsconfig/webpack path alias is used so the module resolver recognizes
'db/client' and 'middleware/auth'.
In `@shapeai/services/api-gateway/src/routes/push-tokens.ts`:
- Around line 11-17: The route handler in push-tokens.ts uses relative imports
and lacks runtime schema validation for request.body, so change the imports
(e.g., the client and requireAuth) to absolute TypeScript module paths, and add
a Fastify route schema on the app.post call (use the route options object with
schema.body) that defines required properties token:string and platform: enum
['ios','android']; validate the body via Fastify before you destructure
request.body in the handler (remove or replace the ad-hoc presence check), and
keep using the existing symbols app.post, PushTokenBody (for typing only),
requireAuth, request.body and reply so the handler inserts only validated
platform values into the DB.
In `@shapeai/services/api-gateway/src/services/notification.service.ts`:
- Line 2: The import in notification.service.ts currently pulls the `pool`
symbol using a relative path; change that import to use the project's configured
absolute import (use the tsconfig/baseUrl or path mapping) so `pool` is imported
via the absolute module name instead of '../db/client', update the import
statement that references `pool` accordingly, and ensure any build/tsconfig path
aliases are respected so the service still compiles and tests pass.
- Around line 49-61: The per-user push send currently calls
axios.post(EXPO_PUSH_URL, ...) without a timeout and swallows all errors; update
the axios.post in notification.service.ts to include a reasonable timeout option
(e.g., 3–10s) and change the catch to capture the error object and log a concise
contextual message including the user.token and error details (error.message
and, if present, error.response.data) while still not throwing so the batch
continues; keep the existing DeviceNotRegistered check and pool.query deletion
logic intact.
In `@shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts`:
- Around line 3-11: The test uses a relative module path in the vi.mock call and
the import of pool (the string argument to vi.mock(...) and the import { pool }
statement) — update both to use the project's absolute-import pattern (replace
the relative '../../src/db/client' module specifiers with the repo's configured
absolute module path for the same module), keeping the mock shape (vi.mock(...,
{ pool: { query: vi.fn() } })) and import name unchanged so tests still
reference the same pool symbol.
- Around line 20-41: The helper simulateCompare duplicates route logic and omits
required request details; replace its usage in tests by calling the real POST
/analyses/compare route via fastify.inject so the actual handler executes.
Construct the injected request to include the same payload and headers the route
expects: set method POST, url '/analyses/compare', JSON body with model, system
and user messages (mirror the route's structured system prompt and messages),
include max_tokens: 1024, and set required headers like 'x-api-key' and
'anthropic-version'; use mockPool to prepare DB rows as before and assert
response status (e.g. reply.status === 403) and parsed JSON instead of parsing
mockAxios.raw content. Ensure you remove or stop using simulateCompare (and its
mockAxios call) and reference the actual route's handler behavior in your
assertions.
In
`@shapeai/services/api-gateway/tests/notifications/notification.service.test.ts`:
- Around line 3-14: The test currently imports/mocks internal modules using
relative paths ('../../src/db/client' and
'../../src/services/notification.service'); change these to absolute internal
module paths (e.g. 'src/db/client' and 'src/services/notification.service') in
both the vi.mock(...) and import lines so mocks and imports reference the same
absolute modules (retain the axios mock as-is), ensuring symbols like pool,
getEligibleUsers, pickTemplate, and sendReanalysisNotifications are imported
from the absolute paths.
In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts`:
- Around line 11-20: The tests are bypassing the real route by calling the
helper simulatePostPushToken which re-implements route logic; replace usages of
simulatePostPushToken and direct mockPool.query assertions with server-level
requests against the actual POST /push-tokens endpoint (e.g., use app.inject or
fastify.inject against your real app instance), supplying authentication headers
and JSON body to exercise route auth, schema, and handler wiring, then assert
response status/body and verify DB effects (mockPool or test DB) afterwards;
remove or deprecate the simulatePostPushToken helper to ensure future
regressions in the POST /push-tokens route are caught.
---
Minor comments:
In `@shapeai/apps/mobile/app/`(app)/compare.tsx:
- Around line 30-33: calcOverall currently divides by vals.length without
checking for an empty scores object, which yields NaN; update the
calcOverall(scores: Record<string, number>) function to guard against empty
input by checking if Object.values(scores).length === 0 and return a safe
default (e.g., 0) before performing the reduce/divide/Math.round, so UI badges
never receive NaN.
In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx`:
- Around line 72-83: Update the two tests in compare.screen.test.tsx to assert
behavior instead of mere visibility: in the Free test (rendering <CompareScreen
/> with mockUseSubscription returning free) call
fireEvent.press(getByTestId('btn-compartilhar')) and assert the Pro alert was
shown (e.g., expect(Alert.alert / mockAlert toHaveBeenCalledWith or whatever
project-mocked alert you use); ensure mockUseSubscription returns the free
subscription for that test). In the Pro test (mockUseSubscription returns {
status: 'pro' }) also fireEvent.press(getByTestId('btn-compartilhar')) and
assert the share flow runs (e.g., expect(yourShareHandler or navigator/share
mock toHaveBeenCalled and that no Pro alert was triggered, or check for presence
of shared UI), keeping the existing getByText('Compartilhar') assertion if
desired; ensure you restore or reconfigure mockUseSubscription per test so each
scenario is isolated.
In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx`:
- Around line 44-48: The test is using a stale reference to the switch stored in
the variable `toggle` (from `getByTestId('toggle-notifications')`) before state
updates; re-query the DOM inside the `waitFor` callback instead of using
`toggle.props.value` to avoid flakiness — call
`getByTestId('toggle-notifications')` inside `waitFor` and assert the new
`.props.value` (or prefer toAssert via matcher on the element) to verify the
reverted value after `fireEvent(toggle, 'valueChange', false)`.
---
Duplicate comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 14-16: The useEffect currently calls initialize() but does not
return its teardown/unsubscribe; change the effect so it returns the cleanup
from initialize: if initialize() is synchronous and returns a function, simply
use useEffect(() => initialize(), [initialize]); if initialize() is async, call
it inside the effect, capture the returned unsubscribe (e.g. let unsub;
initialize().then(fn => { unsub = fn });) and return a cleanup () => { if
(typeof unsub === 'function') unsub(); } so the listener is properly removed on
unmount/remount; reference the useEffect and initialize symbols when making this
change.
In `@shapeai/services/api-gateway/src/routes/profile.ts`:
- Around line 45-54: The code builds SQL SET clauses directly from runtime keys
(fields from updates) which allows injection via crafted keys; modify the PATCH
handler so you first define an explicit whitelist of allowed profile columns
(e.g., const ALLOWED_PROFILE_FIELDS = ['display_name','bio','avatar_url', ...]
as const), then filter fields = Object.keys(updates).filter(f =>
ALLOWED_PROFILE_FIELDS.includes(f as any)) as (keyof ProfileBody)[]; if filtered
fields is empty return 400; then build setClauses and values from this filtered
list (use the actual column names from the whitelist if they differ from
property names), and call pool.query with those sanitized setClauses and values
(ensure updated_at is still set and WHERE user_id = $1 remains unchanged).
---
Nitpick comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 6-7: The imports in _layout.tsx use relative paths for
useAuthStore and configurePurchases; update them to the project’s absolute TS
import style by replacing '../src/stores/auth.store' and
'../src/services/purchases.service' with their absolute module paths (e.g.,
starting with the repo alias such as '@/stores/auth.store' and
'@/services/purchases.service') so that useAuthStore and configurePurchases are
imported via absolute imports consistent with the codebase convention.
In `@shapeai/apps/mobile/app/`(app)/compare.tsx:
- Around line 83-84: The current unsafe casts of a1/a2 to unknown to access
.scores should be removed by adding a proper type for analysis scores (e.g., add
scores: Record<string, number> to AnalysisSummary or declare a local interface
AnalysisWithScores) and then using that type where the comparator receives items
(replace the unknown casts in the comparator that defines scores1/scores2).
Update the comparator signature or map input items to the new typed shape (use a
single safe "as AnalysisWithScores" only after defining the interface) so you
can access .scores without bypassing TypeScript safety.
In `@shapeai/apps/mobile/app/`(app)/index.tsx:
- Around line 3-4: The imports for useAuthStore and useSubscription are using
relative paths; update them to use the project's configured absolute import
alias (the alias set in your tsconfig/jsconfig, e.g. "@/..." or "src/...") so
module resolution is consistent. Specifically, replace the relative import lines
that reference '../../src/stores/auth.store' and
'../../src/hooks/useSubscription' with their equivalent absolute-alias imports
so useAuthStore and useSubscription are imported via the app's absolute path
scheme.
In `@shapeai/apps/mobile/app/`(app)/profile.tsx:
- Line 4: The import on line 4 uses a relative path; change it to the repo's
absolute import form so the two symbols (apiGet, apiPatch) come from the
absolute module instead of '../../src/services/api.client'. Open the file
containing the import of apiGet and apiPatch and replace the module specifier
with the project's absolute import (e.g., 'src/services/api.client' per TS
import convention), keeping the imported identifiers unchanged; ensure
TypeScript resolves the path via existing tsconfig paths if required.
In `@shapeai/apps/mobile/src/services/notification.service.ts`:
- Line 3: Change the relative import in notification.service.ts from
'./api.client' to the repo's absolute import form used elsewhere (e.g., import {
apiPost } from 'services/api.client' or the project's configured path alias), so
update the import statement for apiPost in the notification service to use the
project's absolute path convention and ensure it matches tsconfig/webpack path
aliases if applicable.
In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx`:
- Around line 18-20: Replace the relative imports on the test file for
CompareScreen, apiGet/apiPost and useSubscription with their absolute module
paths; update the import statements that reference '../../app/(app)/compare',
'../../src/services/api.client', and '../../src/hooks/useSubscription' to use
the project's configured absolute paths (keeping the same imported symbols
CompareScreen, apiGet, apiPost, and useSubscription) so the test follows the
"use absolute imports" guideline.
In `@shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx`:
- Line 18: The tests currently mock/import modules using relative paths (e.g.,
'../../src/stores/auth.store', '../../src/stores/user.store',
'../../src/utils/notifications'); update these to use the project's TypeScript
absolute import convention (replace '../../src/stores/auth.store' with the
absolute path like 'src/stores/auth.store', do the same for the user store and
notifications imports) so the jest.mock and import statements in
notification.handler.test.tsx use absolute module paths consistent with the
TS/TSX config.
In `@shapeai/apps/mobile/tests/notifications/notification.service.test.ts`:
- Line 1: The test imports use relative paths; update them to absolute module
paths to follow the project convention — replace the relative import for
registerPushToken in notification.service.test.ts and any other relative
imports/mocks referenced around lines 12 and 17 with their corresponding
absolute module paths (e.g., use the package-root alias or tsconfig baseUrl
paths for the service and mock modules), ensuring imports reference the same
exported symbols (registerPushToken, and the mocks) but via absolute paths so
TypeScript resolves them consistently.
In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx`:
- Around line 13-14: Replace the two relative imports at the top of the test
that pull in ProfileScreen and apiGet/apiPatch with project-root absolute
imports to follow the TS/TSX import convention; locate the import statements
referencing '../../app/(app)/profile' (ProfileScreen) and
'../../src/services/api.client' (apiGet, apiPatch) and update them to use the
repository's absolute module paths (e.g., the app module path for ProfileScreen
and the src/services api.client path) so the test uses absolute imports instead
of relative paths.
In `@shapeai/services/api-gateway/src/routes/profile.ts`:
- Around line 5-11: ProfileBody currently mixes POST and PATCH inputs; split it
into CreateProfileBody (used by the POST/create route) and PatchProfileBody
(used by the PATCH/update route). CreateProfileBody should include required
fields used on creation (height_cm, weight_kg, biological_sex, primary_goal) and
omit notifications_enabled if POST doesn't consume it; PatchProfileBody should
make fields optional and include notifications_enabled since updates may set it.
Update any route handlers or references in this file from ProfileBody to the
appropriate new type names (CreateProfileBody for create handler and
PatchProfileBody for patch handler) and adjust imports/exports accordingly.
In `@shapeai/services/api-gateway/src/routes/push-tokens.ts`:
- Around line 2-3: The imports in this file use relative paths; replace the two
relative imports ("import { pool } from '../db/client'" and "import {
requireAuth } from '../middleware/auth'") with the project's absolute import
style (use the configured path alias used across the repo) so they resolve via
the project's tsconfig/module-alias setup; update the import specifiers for pool
and requireAuth accordingly and verify the build/tsconfig path mapping supports
the chosen absolute base.
In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts`:
- Line 7: Replace the relative import of the DB client with an absolute import:
change "import { pool } from '../../src/db/client'" to use the project's
absolute path (e.g. "import { pool } from 'src/db/client'") so the symbol pool
is imported via the repo's TS absolute import convention used across tests and
source code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0d5af887-c6f5-4b92-bf9d-7693f8628bfe
⛔ Files ignored due to path filters (1)
shapeai/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (27)
shapeai/apps/mobile/app.jsonshapeai/apps/mobile/app/(app)/_layout.tsxshapeai/apps/mobile/app/(app)/compare.tsxshapeai/apps/mobile/app/(app)/history.tsxshapeai/apps/mobile/app/(app)/index.tsxshapeai/apps/mobile/app/(app)/profile.tsxshapeai/apps/mobile/app/_layout.tsxshapeai/apps/mobile/jest.config.jsshapeai/apps/mobile/package.jsonshapeai/apps/mobile/src/components/history/AnalysisHistoryItem.tsxshapeai/apps/mobile/src/services/api.client.tsshapeai/apps/mobile/src/services/notification.service.tsshapeai/apps/mobile/tests/compare/compare.screen.test.tsxshapeai/apps/mobile/tests/history/history.compare.test.tsxshapeai/apps/mobile/tests/notifications/notification.handler.test.tsxshapeai/apps/mobile/tests/notifications/notification.service.test.tsshapeai/apps/mobile/tests/profile/profile.screen.test.tsxshapeai/services/api-gateway/package.jsonshapeai/services/api-gateway/src/db/migrations/004_notifications.sqlshapeai/services/api-gateway/src/routes/analyses.tsshapeai/services/api-gateway/src/routes/profile.tsshapeai/services/api-gateway/src/routes/push-tokens.tsshapeai/services/api-gateway/src/server.tsshapeai/services/api-gateway/src/services/notification.service.tsshapeai/services/api-gateway/tests/analyses/analyses.compare.test.tsshapeai/services/api-gateway/tests/notifications/notification.service.test.tsshapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts
✅ Files skipped from review due to trivial changes (4)
- shapeai/services/api-gateway/src/db/migrations/004_notifications.sql
- shapeai/apps/mobile/package.json
- shapeai/apps/mobile/jest.config.js
- shapeai/services/api-gateway/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
- shapeai/apps/mobile/app/(app)/history.tsx
- shapeai/apps/mobile/src/components/history/AnalysisHistoryItem.tsx
- shapeai/services/api-gateway/src/server.ts
- shapeai/apps/mobile/app/(app)/_layout.tsx
- shapeai/services/api-gateway/src/routes/analyses.ts
| useEffect(() => { | ||
| initialize() | ||
| }, [initialize]) | ||
|
|
||
| useEffect(() => { | ||
| const sub = Notifications.addNotificationResponseReceivedListener(() => { | ||
| router.push('/(app)/camera') | ||
| }) | ||
| return () => sub.remove() | ||
| }, []) |
There was a problem hiding this comment.
Hook push-token registration into app startup/auth-ready flow.
This layout initializes auth and notification response handling, but never registers the device token. Without invoking registerPushToken(), backend token upsert won’t happen and push delivery for this device is likely broken.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/_layout.tsx` around lines 14 - 23, The startup flow
calls initialize() but never invokes registerPushToken(), so device tokens are
never upserted; update the layout startup/auth-ready flow to call
registerPushToken() after initialize() (or once auth is confirmed) and
handle/await errors; specifically, inside the same useEffect that calls
initialize() (or a new effect that depends on initialize/auth-ready state) call
registerPushToken() and log or surface failures, ensuring you don’t duplicate
calls and that existing Notifications.addNotificationResponseReceivedListener
behavior is preserved.
| const sub = Notifications.addNotificationResponseReceivedListener(() => { | ||
| router.push('/(app)/camera') | ||
| }) |
There was a problem hiding this comment.
Use notification payload route instead of hardcoding /(app)/camera.
Line 19 ignores the response object, so deep-link destinations from push data can’t be honored.
Proposed fix
- const sub = Notifications.addNotificationResponseReceivedListener(() => {
- router.push('/(app)/camera')
+ const sub = Notifications.addNotificationResponseReceivedListener((response) => {
+ const screen = response.notification.request.content.data?.screen
+ router.push(typeof screen === 'string' ? screen : '/(app)/camera')
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const sub = Notifications.addNotificationResponseReceivedListener(() => { | |
| router.push('/(app)/camera') | |
| }) | |
| const sub = Notifications.addNotificationResponseReceivedListener((response) => { | |
| const screen = response.notification.request.content.data?.screen | |
| router.push(typeof screen === 'string' ? screen : '/(app)/camera') | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/_layout.tsx` around lines 19 - 21, The notification
handler registered with Notifications.addNotificationResponseReceivedListener
ignores the response and always routes to '/(app)/camera'; update the listener
callback to read the response parameter (the NotificationResponse) and extract
the deep-link route from response.notification.request.content.data (or the
relevant payload key), then call router.push with that route (falling back to
'/(app)/camera' if the payload route is missing). Ensure you reference the
existing listener setup (Notifications.addNotificationResponseReceivedListener)
and the router.push call when implementing the change.
| import { apiGet, apiPost } from '../../src/services/api.client' | ||
| import { useSubscription } from '../../src/hooks/useSubscription' |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Replace relative imports with absolute imports.
Lines 4-5 use relative paths for app services/hooks.
As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 4 - 5, Update the two
relative imports in compare.tsx to use absolute paths: replace the import of
apiGet and apiPost from '../../src/services/api.client' with the absolute module
path that exports apiGet and apiPost (referenced symbol names: apiGet, apiPost)
and replace the import of useSubscription from '../../src/hooks/useSubscription'
with its absolute path (symbol: useSubscription); keep the imported symbol names
unchanged so existing usages in the file still resolve.
| const { id1, id2 } = useLocalSearchParams<{ id1: string; id2: string }>() | ||
| const { subscription } = useSubscription() | ||
| const isPro = subscription?.status === 'pro' | ||
| const compareRef = useRef<View>(null) | ||
|
|
||
| const [a1, setA1] = useState<AnalysisSummary | null>(null) | ||
| const [a2, setA2] = useState<AnalysisSummary | null>(null) | ||
| const [result, setResult] = useState<CompareResult | null>(null) | ||
| const [isLoading, setIsLoading] = useState(true) | ||
| const [isComparing, setIsComparing] = useState(false) | ||
|
|
||
| useEffect(() => { | ||
| Promise.all([ | ||
| apiGet<AnalysisSummary>(`/analyses/${id1}`), | ||
| apiGet<AnalysisSummary>(`/analyses/${id2}`), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "compare.tsx" | head -5Repository: SynkraAI/aiox-core
Length of output: 106
🏁 Script executed:
git ls-files | grep -E "compare\.tsx$"Repository: SynkraAI/aiox-core
Length of output: 104
🏁 Script executed:
find . -type f -path "*mobile*compare.tsx"Repository: SynkraAI/aiox-core
Length of output: 106
🏁 Script executed:
wc -l ./shapeai/apps/mobile/app/\(app\)/compare.tsxRepository: SynkraAI/aiox-core
Length of output: 110
🏁 Script executed:
cat -n ./shapeai/apps/mobile/app/\(app\)/compare.tsx | head -100Repository: SynkraAI/aiox-core
Length of output: 4258
🏁 Script executed:
rg -t ts -t tsx "useLocalSearchParams" --no-heading -A 1 -B 1 | head -50Repository: SynkraAI/aiox-core
Length of output: 90
🏁 Script executed:
fd -type f -name "*.d.ts" | xargs grep -l "useLocalSearchParams" 2>/dev/null | head -5Repository: SynkraAI/aiox-core
Length of output: 233
🏁 Script executed:
rg "useLocalSearchParams" -A 2 -B 1 | head -60Repository: SynkraAI/aiox-core
Length of output: 4803
🏁 Script executed:
rg "useLocalSearchParams" --stats | head -20Repository: SynkraAI/aiox-core
Length of output: 1555
🏁 Script executed:
find . -path "*/node_modules/expo-router*" -name "*.d.ts" 2>/dev/null | head -10Repository: SynkraAI/aiox-core
Length of output: 44
🏁 Script executed:
rg "useLocalSearchParams" -B 5 -A 5 "shapeai/apps/mobile/app/(app)/analysis/\[id\].tsx" | head -40Repository: SynkraAI/aiox-core
Length of output: 152
🌐 Web query:
expo-router useLocalSearchParams return type string array undefined
💡 Result:
The useLocalSearchParams hook from expo-router returns an object containing the current route's dynamic segments and query parameters as strings or string arrays (for catch-all routes [...slug]). Individual dynamic segments like [id] map to string values, while catch-all segments map to string[]. Optional query parameters can be string | undefined. Without generics, TypeScript infers { [key: string]: string | string[] | undefined }. With generics, you explicitly type it, e.g., useLocalSearchParams<{ user: string; everything: string[] }>. It can return undefined values for optional/missing parameters, or an empty object {} in certain contexts like unmatched routes or specific navigation scenarios (e.g., inside Stack.Screen options or layout files during navigation). Reserved names like params for catch-all routes cause bugs returning [object Object] instead of string[]. Examples from official docs: - /[user] -> { user: "evanbacon" } - /[user]?tab=projects -> { user: "bacon", tab: "projects" } or { user: "expo", tab: undefined } - /[...everything] -> { everything: ["evanbacon", "123"] } - /[...everything]?query=hello -> { everything: ["evanbacon", "123"], query: "hello" } Usage: const params = useLocalSearchParams<{ id: string; query?: string }>; // params.id: string (required route param) // params.query: string | undefined (optional query param)
Citations:
- 1: https://docs.expo.dev/router/reference/url-parameters/
- 2: https://docs.expo.dev/router/reference/url-parameters
- 3: https://docs.expo.dev/router/reference/typed-routes/
- 4: feat: add SearchParams types to Expo Router expo/expo#22380
- 5: [expo-router]: useLocalSearchParams returns [object Object] for catch-all route named [...params] expo/expo#43226
🏁 Script executed:
rg "compare" -A 2 -B 2 | grep -E "(push|replace|navigate|href)" | head -30Repository: SynkraAI/aiox-core
Length of output: 542
🏁 Script executed:
rg "router\.push.*compare" -A 1 -B 1Repository: SynkraAI/aiox-core
Length of output: 320
🏁 Script executed:
cat -n ./shapeai/apps/mobile/app/\(app\)/history.tsx | grep -A 20 -B 20 "selectedIds"Repository: SynkraAI/aiox-core
Length of output: 5930
Add runtime validation for id1 and id2 before making API calls.
useLocalSearchParams with generics only provides compile-time type hints; TypeScript generics are erased at runtime. Query parameters can arrive as string | string[] | undefined. Validating these values before constructing API paths prevents invalid requests like /analyses/undefined or /analyses/value1,value2.
Consider adding a guard like:
if (!id1 || !id2 || Array.isArray(id1) || Array.isArray(id2)) {
// Handle error or redirect
}This applies to lines 49–50 (API calls) and 57 (POST payload).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 36 - 50, Validate
runtime types/values of id1 and id2 (from useLocalSearchParams) before using
them in API calls: inside the useEffect (and before any apiPost payload
construction), add a guard that rejects undefined or array values (e.g., !id1 ||
!id2 || Array.isArray(id1) || Array.isArray(id2)), handle the error by setting
isLoading/isComparing appropriately and returning early (or navigate/redirect),
and only call apiGet(`/analyses/${id1}`)/apiGet(`/analyses/${id2}`) and any
apiPost that uses id1/id2 after the guard passes so the requests never contain
"undefined" or comma-separated arrays.
| useEffect(() => { | ||
| Promise.all([ | ||
| apiGet<AnalysisSummary>(`/analyses/${id1}`), | ||
| apiGet<AnalysisSummary>(`/analyses/${id2}`), | ||
| ]) | ||
| .then(([r1, r2]) => { | ||
| setA1(r1) | ||
| setA2(r2) | ||
| setIsLoading(false) | ||
| setIsComparing(true) | ||
| return apiPost<CompareResult>('/analyses/compare', { analysis_id_1: id1, analysis_id_2: id2 }) | ||
| }) | ||
| .then(setResult) | ||
| .catch(() => setResult(null)) | ||
| .finally(() => setIsComparing(false)) | ||
| }, [id1, id2]) |
There was a problem hiding this comment.
Clear isLoading on initial fetch failure to avoid infinite spinner.
If either apiGet fails, isLoading stays true, and the screen never leaves the loader state.
Proposed fix
- .catch(() => setResult(null))
+ .catch(() => {
+ setResult(null)
+ setIsLoading(false)
+ })
.finally(() => setIsComparing(false))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| Promise.all([ | |
| apiGet<AnalysisSummary>(`/analyses/${id1}`), | |
| apiGet<AnalysisSummary>(`/analyses/${id2}`), | |
| ]) | |
| .then(([r1, r2]) => { | |
| setA1(r1) | |
| setA2(r2) | |
| setIsLoading(false) | |
| setIsComparing(true) | |
| return apiPost<CompareResult>('/analyses/compare', { analysis_id_1: id1, analysis_id_2: id2 }) | |
| }) | |
| .then(setResult) | |
| .catch(() => setResult(null)) | |
| .finally(() => setIsComparing(false)) | |
| }, [id1, id2]) | |
| useEffect(() => { | |
| Promise.all([ | |
| apiGet<AnalysisSummary>(`/analyses/${id1}`), | |
| apiGet<AnalysisSummary>(`/analyses/${id2}`), | |
| ]) | |
| .then(([r1, r2]) => { | |
| setA1(r1) | |
| setA2(r2) | |
| setIsLoading(false) | |
| setIsComparing(true) | |
| return apiPost<CompareResult>('/analyses/compare', { analysis_id_1: id1, analysis_id_2: id2 }) | |
| }) | |
| .then(setResult) | |
| .catch(() => { | |
| setResult(null) | |
| setIsLoading(false) | |
| }) | |
| .finally(() => setIsComparing(false)) | |
| }, [id1, id2]) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 47 - 62, The
useEffect chain currently sets setIsLoading(false) only on successful apiGet
calls so a failure leaves isLoading true; update the Promise.all(...) call to
ensure setIsLoading(false) runs on both success and failure (e.g., add a
.catch(err => { setResult(null); throw err }) before continuing or move
setIsLoading(false) into a .finally() directly after Promise.all), and ensure
setIsComparing is cleared in all paths (use .finally(() =>
setIsComparing(false)) for the compare request and also call
setIsComparing(false) if Promise.all fails) so that setIsLoading and
setIsComparing are always reset when apiGet or apiPost fail; refer to useEffect,
apiGet, apiPost('/analyses/compare'), setA1, setA2, setResult, setIsLoading, and
setIsComparing to locate the changes.
| const res = await axios.post(EXPO_PUSH_URL, { | ||
| to: user.token, | ||
| title: 'ShapeAI', | ||
| body, | ||
| data: { screen: '/(app)/camera' }, | ||
| }) | ||
| const result = res.data?.data | ||
| if (result?.status === 'error' && result?.details?.error === 'DeviceNotRegistered') { | ||
| await pool.query('DELETE FROM push_tokens WHERE token = $1', [user.token]) | ||
| } | ||
| } catch { | ||
| // log per-user failures silently — don't abort the whole batch | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -a "notification.service.ts" --type fRepository: SynkraAI/aiox-core
Length of output: 223
🏁 Script executed:
cat -n /home/jailuser/git/shapeai/services/api-gateway/src/services/notification.service.ts | head -100Repository: SynkraAI/aiox-core
Length of output: 2445
🏁 Script executed:
cat -n /home/jailuser/git/shapeai/services/api-gateway/src/db/client.tsRepository: SynkraAI/aiox-core
Length of output: 404
🏁 Script executed:
rg -t ts "console\.(log|warn|error|info)" shapeai/services/api-gateway/src/ --max-count=10Repository: SynkraAI/aiox-core
Length of output: 668
🏁 Script executed:
cat -n /home/jailuser/git/shapeai/services/api-gateway/src/server.ts | head -80Repository: SynkraAI/aiox-core
Length of output: 2249
Add timeout and error logging to push notification handler.
The axios.post call (line 49) lacks a timeout, and the catch block (lines 59–61) silently swallows errors without any logging. This hides individual push delivery failures, making it difficult to detect and debug reliability issues. While the outer error handler in server.ts will catch total job failures, per-user errors are completely invisible.
Proposed fix
- const res = await axios.post(EXPO_PUSH_URL, {
+ const res = await axios.post(EXPO_PUSH_URL, {
to: user.token,
title: 'ShapeAI',
body,
data: { screen: '/(app)/camera' },
- })
+ }, { timeout: 10000 })- } catch {
- // log per-user failures silently — don't abort the whole batch
+ } catch (err) {
+ console.warn('push_send_failed', { userId: user.id, token: user.token, err })
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const res = await axios.post(EXPO_PUSH_URL, { | |
| to: user.token, | |
| title: 'ShapeAI', | |
| body, | |
| data: { screen: '/(app)/camera' }, | |
| }) | |
| const result = res.data?.data | |
| if (result?.status === 'error' && result?.details?.error === 'DeviceNotRegistered') { | |
| await pool.query('DELETE FROM push_tokens WHERE token = $1', [user.token]) | |
| } | |
| } catch { | |
| // log per-user failures silently — don't abort the whole batch | |
| } | |
| const res = await axios.post(EXPO_PUSH_URL, { | |
| to: user.token, | |
| title: 'ShapeAI', | |
| body, | |
| data: { screen: '/(app)/camera' }, | |
| }, { timeout: 10000 }) | |
| const result = res.data?.data | |
| if (result?.status === 'error' && result?.details?.error === 'DeviceNotRegistered') { | |
| await pool.query('DELETE FROM push_tokens WHERE token = $1', [user.token]) | |
| } | |
| } catch (err) { | |
| console.warn('push_send_failed', { userId: user.id, token: user.token, err }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/src/services/notification.service.ts` around
lines 49 - 61, The per-user push send currently calls axios.post(EXPO_PUSH_URL,
...) without a timeout and swallows all errors; update the axios.post in
notification.service.ts to include a reasonable timeout option (e.g., 3–10s) and
change the catch to capture the error object and log a concise contextual
message including the user.token and error details (error.message and, if
present, error.response.data) while still not throwing so the batch continues;
keep the existing DeviceNotRegistered check and pool.query deletion logic
intact.
| vi.mock('../../src/db/client', () => ({ | ||
| pool: { query: vi.fn() }, | ||
| })) | ||
|
|
||
| vi.mock('axios', () => ({ | ||
| default: { post: vi.fn() }, | ||
| })) | ||
|
|
||
| import { pool } from '../../src/db/client' |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use absolute module paths in test mocks/imports.
Line 3 and Line 11 use relative paths (../../src/...). Please switch these to the project’s absolute import pattern for consistency with repo rules.
As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts` around
lines 3 - 11, The test uses a relative module path in the vi.mock call and the
import of pool (the string argument to vi.mock(...) and the import { pool }
statement) — update both to use the project's absolute-import pattern (replace
the relative '../../src/db/client' module specifiers with the repo's configured
absolute module path for the same module), keeping the mock shape (vi.mock(...,
{ pool: { query: vi.fn() } })) and import name unchanged so tests still
reference the same pool symbol.
| async function simulateCompare(userId: string, id1: string, id2: string, authUserId = userId) { | ||
| const { rows } = await mockPool.query( | ||
| `SELECT id, scores FROM analyses WHERE id = ANY($1) AND user_id = $2 AND status = 'completed'`, | ||
| [[id1, id2], authUserId] | ||
| ) | ||
|
|
||
| if (rows.length !== 2) { | ||
| throw Object.assign(new Error('Forbidden'), { status: 403 }) | ||
| } | ||
|
|
||
| const a1 = rows.find((r: { id: string }) => r.id === id1) | ||
| const a2 = rows.find((r: { id: string }) => r.id === id2) | ||
|
|
||
| const claudeRes = await mockAxios.post('https://api.anthropic.com/v1/messages', { | ||
| model: 'claude-sonnet-4-6', | ||
| messages: [{ role: 'user', content: `${JSON.stringify(a1.scores)} ${JSON.stringify(a2.scores)}` }] | ||
| }, {}) | ||
|
|
||
| const raw: string = claudeRes.data.content[0].text | ||
| const clean = raw.replace(/```json|```/g, '').trim() | ||
| return JSON.parse(clean) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.ts" -path "*analyses*" | head -20Repository: SynkraAI/aiox-core
Length of output: 328
🏁 Script executed:
git ls-files | grep -E "(analyses|compare)" | grep -E "\.(ts|js)$"Repository: SynkraAI/aiox-core
Length of output: 320
🏁 Script executed:
fd -e ts -e js "analyses" | head -30Repository: SynkraAI/aiox-core
Length of output: 320
🏁 Script executed:
wc -l shapeai/services/api-gateway/tests/analyses/analyses.compare.test.tsRepository: SynkraAI/aiox-core
Length of output: 134
🏁 Script executed:
wc -l shapeai/services/api-gateway/src/routes/analyses.tsRepository: SynkraAI/aiox-core
Length of output: 118
🏁 Script executed:
cat -n shapeai/services/api-gateway/tests/analyses/analyses.compare.test.tsRepository: SynkraAI/aiox-core
Length of output: 3572
🏁 Script executed:
cat -n shapeai/services/api-gateway/src/routes/analyses.tsRepository: SynkraAI/aiox-core
Length of output: 11374
🏁 Script executed:
rg "fastify.*inject" shapeai/services/api-gateway/tests/ -A 2Repository: SynkraAI/aiox-core
Length of output: 44
🏁 Script executed:
rg "\.inject\(" shapeai/services/api-gateway/tests/ -B 2 -A 2Repository: SynkraAI/aiox-core
Length of output: 44
🏁 Script executed:
head -50 shapeai/services/api-gateway/tests/analyses/analyses.compare.test.tsRepository: SynkraAI/aiox-core
Length of output: 1856
🏁 Script executed:
fd "setup\|fixture\|vitest.config" shapeai/services/api-gateway/tests/ -o shapeai/services/api-gateway/Repository: SynkraAI/aiox-core
Length of output: 240
🏁 Script executed:
ls -la shapeai/services/api-gateway/tests/Repository: SynkraAI/aiox-core
Length of output: 411
🏁 Script executed:
cat shapeai/services/api-gateway/tests/analyses/analyses.routes.test.tsRepository: SynkraAI/aiox-core
Length of output: 4273
🏁 Script executed:
find shapeai/services/api-gateway -name "*.test.ts" -exec head -30 {} +Repository: SynkraAI/aiox-core
Length of output: 7845
Test the real route handler instead of duplicating compare logic in simulateCompare.
This helper mirrors backend behavior with critical differences: the route includes a detailed system prompt and structured message for Claude, passes required API headers (x-api-key, anthropic-version), sets max_tokens: 1024, and returns status codes via reply.status(403). Tests can pass while the actual route fails or behaves differently due to missing headers, incomplete prompts, or divergent error handling. Use fastify.inject against the registered POST /analyses/compare route to exercise real behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts` around
lines 20 - 41, The helper simulateCompare duplicates route logic and omits
required request details; replace its usage in tests by calling the real POST
/analyses/compare route via fastify.inject so the actual handler executes.
Construct the injected request to include the same payload and headers the route
expects: set method POST, url '/analyses/compare', JSON body with model, system
and user messages (mirror the route's structured system prompt and messages),
include max_tokens: 1024, and set required headers like 'x-api-key' and
'anthropic-version'; use mockPool to prepare DB rows as before and assert
response status (e.g. reply.status === 403) and parsed JSON instead of parsing
mockAxios.raw content. Ensure you remove or stop using simulateCompare (and its
mockAxios call) and reference the actual route's handler behavior in your
assertions.
| vi.mock('../../src/db/client', () => ({ | ||
| pool: { query: vi.fn() }, | ||
| })) | ||
|
|
||
| vi.mock('axios', () => ({ | ||
| default: { post: vi.fn() }, | ||
| })) | ||
|
|
||
| import { pool } from '../../src/db/client' | ||
| import axios from 'axios' | ||
| import { getEligibleUsers, pickTemplate, sendReanalysisNotifications } from '../../src/services/notification.service' | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use absolute internal module paths in this test.
The internal module references in mocks/imports are relative (../../src/...).
As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@shapeai/services/api-gateway/tests/notifications/notification.service.test.ts`
around lines 3 - 14, The test currently imports/mocks internal modules using
relative paths ('../../src/db/client' and
'../../src/services/notification.service'); change these to absolute internal
module paths (e.g. 'src/db/client' and 'src/services/notification.service') in
both the vi.mock(...) and import lines so mocks and imports reference the same
absolute modules (retain the axios mock as-is), ensuring symbols like pool,
getEligibleUsers, pickTemplate, and sendReanalysisNotifications are imported
from the absolute paths.
| async function simulatePostPushToken(userId: string, token: string, platform: string) { | ||
| if (!token || !platform) throw Object.assign(new Error('Bad Request'), { status: 400 }) | ||
|
|
||
| await mockPool.query( | ||
| `INSERT INTO push_tokens (user_id, token, platform) VALUES ($1, $2, $3) | ||
| ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, updated_at = NOW()`, | ||
| [userId, token, platform] | ||
| ) | ||
| return { ok: true } | ||
| } |
There was a problem hiding this comment.
This suite is not testing POST /push-tokens; it’s testing a local replica.
simulatePostPushToken re-implements route logic, so regressions in the actual Fastify route (auth, schema, handler wiring) won’t be caught. Replace helper-based assertions with server-level route tests (e.g., inject against the real app).
Also applies to: 25-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts`
around lines 11 - 20, The tests are bypassing the real route by calling the
helper simulatePostPushToken which re-implements route logic; replace usages of
simulatePostPushToken and direct mockPool.query assertions with server-level
requests against the actual POST /push-tokens endpoint (e.g., use app.inject or
fastify.inject against your real app instance), supplying authentication headers
and JSON body to exercise route auth, schema, and handler wiring, then assert
response status/body and verify DB effects (mockPool or test DB) afterwards;
remove or deprecate the simulatePostPushToken helper to ensure future
regressions in the POST /push-tokens route are caught.
Add missing test 4.4: technical purchase failure shows generic Alert. All 95 mobile tests + 28 api-gateway tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx (1)
68-81: ⚡ Quick winSuccessful-purchase test doesn't verify
pollUntilProwas called.The test correctly asserts that navigation occurs, but the subscription-polling step (
pollUntilPro) is a critical part of the happy path — it ensures the user's pro status is confirmed before redirecting. Without asserting it was awaited, a refactor that drops the poll and navigates immediately would still pass this test.♻️ Suggested addition
mockPurchase.mockResolvedValueOnce({}) + // Capture the pollUntilPro spy so we can assert it was called + const pollUntilProMock = jest.fn().mockResolvedValue(undefined) mockUseSubscription.mockReturnValue({ subscription: { status: 'free', expires_at: null }, isLoading: false, - pollUntilPro: jest.fn().mockResolvedValue(undefined), + pollUntilPro: pollUntilProMock, }) const { getByTestId } = render(<PaywallScreen />) await act(flushAllPromises) fireEvent.press(getByTestId('btn-assinar-pro')) await waitFor(() => { expect(mockReplace).toHaveBeenCalledWith('/(app)') }) + expect(pollUntilProMock).toHaveBeenCalledTimes(1) expect(mockPurchase).toHaveBeenCalledTimes(1)(The
mockReturnValueoverride can live inside the test or replace the one inbeforeEachfor this case.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` around lines 68 - 81, The test currently asserts navigation and purchase but omits verifying the subscription poll; update the 'navega para home após compra bem-sucedida' test in PaywallScreen (paywall.purchase.test.tsx) to also assert that the polling helper (pollUntilPro / its test double) was called and awaited: ensure the mock for pollUntilPro (or the exported helper used by PaywallScreen) is set to resolve (e.g., mockResolvedValueOnce) before firing the purchase, then add an expectation such as expect(mockPollUntilPro).toHaveBeenCalled() or toHaveBeenCalledTimes(1) (and/or check call order with mockReplace if needed) so the test fails if the component no longer waits for pollUntilPro before navigating.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx`:
- Line 12: The tests use relative module keys in jest.mock (e.g., the mock for
'../../src/services/purchases.service' and '../../src/hooks/useSubscription')
which can fail to intercept the real modules if the app uses absolute/path-alias
imports; change these mocks to use the same resolved module keys as the source
(e.g., the absolute/path-alias form used by paywall.tsx such as
'@/services/purchases.service' and '@/hooks/useSubscription'), update all five
mock locations accordingly, and verify the alias matches your tsconfig.json
paths / jest.config.js moduleNameMapper so Jest registers the mock under the
exact same module identity the component imports.
- Around line 63-65: The current test uses waitFor(() =>
expect(mockReplace).not.toHaveBeenCalled()) which is a false-positive because
waitFor only retries on thrown errors; change the test to first wait for a
positive observable signal that the async error path has completed (e.g. waitFor
the purchase button to become enabled/not disabled or for a loading indicator to
be removed using the same waitFor utility), then after that waitFor completes,
assert that mockReplace (the mocked router.replace) was not called; reference
the existing symbols mockReplace, purchasePackage (the rejected promise),
router.replace and the button/loading element to locate where to add the
positive waitFor and move the not.toHaveBeenCalled() assertion outside of
waitFor.
---
Nitpick comments:
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx`:
- Around line 68-81: The test currently asserts navigation and purchase but
omits verifying the subscription poll; update the 'navega para home após compra
bem-sucedida' test in PaywallScreen (paywall.purchase.test.tsx) to also assert
that the polling helper (pollUntilPro / its test double) was called and awaited:
ensure the mock for pollUntilPro (or the exported helper used by PaywallScreen)
is set to resolve (e.g., mockResolvedValueOnce) before firing the purchase, then
add an expectation such as expect(mockPollUntilPro).toHaveBeenCalled() or
toHaveBeenCalledTimes(1) (and/or check call order with mockReplace if needed) so
the test fails if the component no longer waits for pollUntilPro before
navigating.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 99fea957-a114-408f-b7cb-0529f5c52f83
📒 Files selected for processing (2)
shapeai/apps/mobile/app/(app)/index.tsxshapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx
✅ Files skipped from review due to trivial changes (1)
- shapeai/apps/mobile/app/(app)/index.tsx
| router: { replace: jest.fn(), push: jest.fn() }, | ||
| })) | ||
|
|
||
| jest.mock('../../src/services/purchases.service', () => ({ |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Relative imports violate project guidelines and risk breaking jest.mock interception.
All five locations use ../../… relative paths instead of absolute imports. Beyond the guideline violation, there's a functional risk: returning a falsy condition is not sufficient to trigger a retry — the callback must throw an error in order to retry is not the concern here, but the core issue is module identity. If paywall.tsx follows the project convention and imports via absolute paths or path aliases (e.g. @/services/purchases.service), then jest.mock('../../src/services/purchases.service', …) and jest.mock('../../src/hooks/useSubscription', …) may register against a different resolved key in Jest's module registry, meaning the mocks silently fail to intercept the source file's actual imports — and every test in this suite could be exercising the real implementations.
♻️ Proposed fix
-jest.mock('../../src/services/purchases.service', () => ({
+jest.mock('@/services/purchases.service', () => ({
getOfferings: jest.fn().mockResolvedValue({ ... }),
purchasePackage: jest.fn(),
restorePurchases: jest.fn(),
PURCHASES_ERROR_CODE: { PURCHASE_CANCELLED_ERROR: 'purchaseCancelled' },
}))
-jest.mock('../../src/hooks/useSubscription', () => ({
+jest.mock('@/hooks/useSubscription', () => ({
useSubscription: jest.fn(),
}))
-import PaywallScreen from '../../app/(app)/paywall'
+import PaywallScreen from '@/app/(app)/paywall'
import { router } from 'expo-router'
-import { purchasePackage, restorePurchases, PURCHASES_ERROR_CODE } from '../../src/services/purchases.service'
-import { useSubscription } from '../../src/hooks/useSubscription'
+import { purchasePackage, restorePurchases, PURCHASES_ERROR_CODE } from '@/services/purchases.service'
+import { useSubscription } from '@/hooks/useSubscription'(Alias prefix may differ — align with the paths config in tsconfig.json / jest.config.js moduleNameMapper.)
As per coding guidelines, "Use absolute imports instead of relative imports in all code."
Also applies to: 24-24, 28-31
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` at line 12, The
tests use relative module keys in jest.mock (e.g., the mock for
'../../src/services/purchases.service' and '../../src/hooks/useSubscription')
which can fail to intercept the real modules if the app uses absolute/path-alias
imports; change these mocks to use the same resolved module keys as the source
(e.g., the absolute/path-alias form used by paywall.tsx such as
'@/services/purchases.service' and '@/hooks/useSubscription'), update all five
mock locations accordingly, and verify the alias matches your tsconfig.json
paths / jest.config.js moduleNameMapper so Jest registers the mock under the
exact same module identity the component imports.
| await waitFor(() => { | ||
| expect(mockReplace).not.toHaveBeenCalled() | ||
| }) |
There was a problem hiding this comment.
waitFor(() => expect(not.toHaveBeenCalled())) is a false-positive trap.
waitFor retries only when the callback throws; returning a passing assertion never triggers a retry. Because expect(mockReplace).not.toHaveBeenCalled() passes immediately (on the very first poll, before the rejected promise from purchasePackage has even settled), this test will always be green — even if the component incorrectly calls router.replace after a cancelled purchase.
The fix is to first wait for a positive, observable signal that the async error path has completed (e.g. the button becoming pressable again, or a loading indicator disappearing), then assert no navigation occurred outside waitFor.
♻️ Suggested approach
fireEvent.press(getByTestId('btn-assinar-pro'))
- await waitFor(() => {
- expect(mockReplace).not.toHaveBeenCalled()
- })
+ // Wait for the UI to settle after the rejected purchase (e.g. button re-enabled)
+ await waitFor(() => {
+ expect(getByTestId('btn-assinar-pro')).not.toBeDisabled()
+ })
+ expect(mockReplace).not.toHaveBeenCalled()(Replace the not.toBeDisabled() sentinel with whatever observable state the component exposes after error handling completes.)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await waitFor(() => { | |
| expect(mockReplace).not.toHaveBeenCalled() | |
| }) | |
| fireEvent.press(getByTestId('btn-assinar-pro')) | |
| // Wait for the UI to settle after the rejected purchase (e.g. button re-enabled) | |
| await waitFor(() => { | |
| expect(getByTestId('btn-assinar-pro')).not.toBeDisabled() | |
| }) | |
| expect(mockReplace).not.toHaveBeenCalled() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` around lines 63
- 65, The current test uses waitFor(() =>
expect(mockReplace).not.toHaveBeenCalled()) which is a false-positive because
waitFor only retries on thrown errors; change the test to first wait for a
positive observable signal that the async error path has completed (e.g. waitFor
the purchase button to become enabled/not disabled or for a loading indicator to
be removed using the same waitFor utility), then after that waitFor completes,
assert that mockReplace (the mocked router.replace) was not called; reference
the existing symbols mockReplace, purchasePackage (the rejected promise),
router.replace and the button/loading element to locate where to add the
positive waitFor and move the not.toHaveBeenCalled() assertion outside of
waitFor.
… for Python 3.14 - mediapipe 0.10.14→0.10.35 (Python 3.14 compat), migrate to Tasks API - psycopg2-binary, pillow, numpy: pin relaxed to >= for forward compat - Add .gitignore to api-gateway and ai-engine services Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass size:true option and wrap in try/catch for forward compat. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…env var - Buffer.from(secret, 'base64') to correctly verify Supabase JWTs - Add AWS_S3_BUCKET to .env (s3.service.ts uses AWS_S3_BUCKET, not S3_BUCKET_NAME) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fastify rejects requests with Content-Type: application/json but empty body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…end-to-end - Migration 005: ADD COLUMN body_composition JSONB to reports table (applied) - GET /analyses/:id: include r.body_composition in SELECT and response - AnalysisResult: add BodyComposition interface and optional field - report.tsx: BodyCompositionCard showing fat%, body type, fat areas, coach assessment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tasks API calibrates confidence differently than Solutions API. Old API used min_detection_confidence=0.5 (Solutions); equivalent sensitivity in Tasks API requires ~0.3 to match prior behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If pose landmarks not detected, pipeline continues with neutral_scores() (all geometric metrics = 50, body_fat from BMI). Claude Vision still runs and generates report/plan from visual analysis. Analysis never fails due to pose detection alone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace single-attempt detection with 6-step fallback sequence: original → resize → contrast+1.3 → contrast+1.6 → resize+contrast → min confidence. Each step lowers confidence threshold and increases preprocessing aggressiveness. Also adds ImageOps.exif_transpose to fix phone camera rotation metadata. Tested: dark (0.3x), overexposed (2.5x), low quality (30% JPEG), portrait resize. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostgreSQL numeric columns return decimal.Decimal which breaks float arithmetic in score_calculator and vision_analyzer. Convert at source in analysis.py router and defensively in each consumer function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…3, 5.4) - POST /chat endpoint with Claude AI integration and context injection - 3 coach personas: Rafael (técnico), Marina (motivacional), Bruno (intenso) - Rate limiting: 20 msgs/day Free, unlimited Pro (chat_usage table) - GET /chat/usage for UI counter - coach.tsx chat screen with quick suggestions and paywall on limit - Persona selector in profile screen - coach_persona column in user_profiles (migration 006) - chat_usage table for daily rate limiting (migration 006) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useFocusEffect replaces useEffect so switching persona in profile is immediately reflected in the coach screen header name. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each message now includes the full session history so Claude maintains context across turns. History capped at 20 entries server-side to keep token usage bounded. Stateless between sessions (v1 spec). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- InlineText component parses **bold** patterns into fontWeight:bold spans - Chat context now includes full Week 1 exercise list (name, sets, reps, rest) so coach can explain, substitute and adjust the real plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove MediaPipe from pipeline entirely — no more geometric landmarks - Vision prompt now returns 9 muscle group scores (quadriceps, glutes, calves, biceps, triceps, chest, abs, traps, lats) each with a note, plus overall_score, strengths_summary, weaknesses_summary - report_generator simplified: builds highlights/dev_areas from muscle scores directly — eliminates a redundant Claude API call - plan_generator updated to use new muscle score priority list - Chat context updated to use new body_composition fields - New report UI: body composition → strengths/weaknesses summary → muscle breakdown with score bar + note per group - BodyScores types updated across mobile + shared package Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth: replace @fastify/jwt with jose JWKS for ES256 (Supabase) - s3: fix download to use GetObject instead of presigned PUT url - db: fix query to JOIN user_profiles (biological_sex, primary_goal) - ai-engine: add load_dotenv() to main.py - api-gateway: normalize INTERNAL_SECRET with ?? '' - api-client: send Content-Type only when body is present - plan_generator: max_tokens=8192, strip markdown fences, fallback plan - report_generator: strip markdown fences, fallback report - vision_analyzer: replace MediaPipe with Claude Vision muscle scoring - workout.tsx: make getUserProfile optional in Promise.all - onboarding: add onboarding screen flow - analysis: add _layout.tsx for nested routes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
eas.json estava sem seção env — variáveis do .env não eram injetadas no bundle, causando crash na inicialização do Supabase client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…th style - BodyScores interface (shared + mobile) estava sem o campo shoulders causando divisão incorreta por 10 em calculateOverallScore - heroWrapper tinha width duplicado (100% e 120%) — TS1117 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fechando como parte da triagem de manutenção do backlog de PRs abertos do AIOX-Core em 2026-05-07. Motivo: este PR ficou defasado ou está fora do escopo atual do core, com conflito/estado antigo ou conteúdo de produto/app/superfície legada que não deve entrar diretamente em main. Se ainda houver uma ideia útil aqui, o caminho correto é abrir um PR novo, pequeno e atualizado contra main, com story/critério de aceite e validação atual. Obrigado pela contribuição. |
Summary
Quality Gates
Test Plan
Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores