Skip to content

feat: the data that should have been in Postgres, and the tables to hold it - #26

Merged
amielnoy merged 5 commits into
fix/ci-format-generatedfrom
feat/supabase-durable-data
Sep 1, 2026
Merged

feat: the data that should have been in Postgres, and the tables to hold it#26
amielnoy merged 5 commits into
fix/ci-format-generatedfrom
feat/supabase-durable-data

Conversation

@amielnoy

@amielnoy amielnoy commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Stacked on #25 — based on fix/ci-format-generated, so retarget to main once that merges.

Six things that either belong in the Supabase project and are not there, or are there and empty.

The one that is currently broken

lecture_examples is live and has zero rows, and eight decks fetch from it with .single() — so a missing row is an error, not a blank space, and all 38 worked-example slides render "Example content unavailable" over an empty panel. Nothing in the build, the types or the tests ever mentioned the table.

Content comes from two places, and they are not equally trustworthy:

  • Lectures 9 and 10 (20 examples) are recovered from the deck sources. Those slides still carry their content as render-time fallbacks. Pulled through the TypeScript AST rather than retyped — the strings are bilingual and a mangled Hebrew transcription is not something review catches.
  • Lectures 1–6 (18 examples) are newly written. That content exists nowhere in this repository or its history; those slides were created already fetching from Supabase. Written against each slide's manifest description, in the house style of decks 9/10. This is course content and wants a real read, not a diff read.

tests/unit/lectureExamples.spec.ts reads every fetchLectureExample(n) call site and checks it against the content and the seed, in both languages, with no database.

The rest

Purchases & rate limits The DDL was a Python string, and neither table existed in the project the API points at — which is why /api/entitlements/course answers 503 and the admin screen lists nobody. Now server/app/schema.sql, plus a purge for the otherwise-unbounded rate-limit table.
Learner progress Was localStorage, so per-browser. Now an account's, merged rather than replaced — see below.
Accounts There was no user record anywhere; identity existed only inside a signed cookie. A google_subject row is what finally lets a purchase made at checkout attach to the account that signs in later.
Login & AI usage The Prometheus counters answer "how many, right now" and vanish on restart. Rows carry the same HMAC of the email the metric carries as a label — no addresses, no prompts, no responses.
Test history The Pushgateway holds one push per grouping key, so "history" was one run deep. test_runs accumulates.

Two decisions worth arguing with

The progress merge is a union, not a replacement. Booleans OR, the answer counter takes the max, the id lists combine, and the response is the result. Last-write-wins would mean opening the site on a second device silently discards what the first recorded — precisely the thing an account is meant to fix.

Every new table gets RLS on and anon revoked. These land in the same project as the content tables, public is the schema PostgREST exposes, and Supabase grants anon broad access to new tables there by default. Without the revoke, course_purchases is customer emails and payment intent ids readable over HTTPS with a key that ships in the browser. Verified on the live project after applying:

lecture_examples   anon SELECT: t   rls: t     <- decks need this
academy_users      anon SELECT: f   rls: t
course_purchases   anon SELECT: f   rls: t
ai_usage_events    anon SELECT: f   rls: t
learner_progress   anon SELECT: f   rls: t
login_events       anon SELECT: f   rls: t
test_runs          anon SELECT: f   rls: t
api_rate_limits    anon SELECT: f   rls: t

Already applied to Supabase

Additively — no TRUNCATE was run. lecture_examples now holds 76 rows (38 × en/he) and all eight operational tables exist. The committed seed still contains its truncate, so pnpm --filter @workspace/scripts run seed:academy remains the reproducible path.

Needed outside this PR

  • A DATABASE_URL secret on the repo. ci.yml passes it to the publish step; without it the test-history storage prints that it is disabled and moves on — intended, not a failure.
  • Confirm the deployed API has DATABASE_URL set. The absence of course_purchases in this project suggests it has never booted against it.

Also worth a look

Lectures 3 and 4 disagree with themselves: the manifest puts their worked examples at positions 8/13/20 and 7/11/15, but the slides fetch 5/7/10 and 5/7/9. Rows are keyed on the fetch argument, since that is what queries the database, but one of the two numbers is wrong.

I left the Prometheus user label alone despite its cardinality, because removing it changes what the Grafana dashboards see.

Verification

195 Python · 312 unit · 216 component · 58 API · 88 contract · lint · format · workspace typecheck — all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NrZUXGffUKSQBUKQtiSbeL

amielnoy and others added 5 commits September 1, 2026 09:31
Every worked-example slide in eight decks calls `fetchLectureExample`, which
calls `.single()` — so a missing row is not a blank space, it is an error, and
all thirty-eight of them have been rendering "Example content unavailable" over
an empty panel. The table was live and empty: the decks were written against a
Supabase project that had the content, the content never reached this
repository, and no build, type or test ever mentioned the table.

The content comes from two places, and they are not equally trustworthy.
Lectures 9 and 10 still carry theirs as render-time fallbacks — `FALLBACK_ROWS`,
`defaultCode`, `defaultBullets`, the `?? t(en, he)` default behind every field,
and lecture 10's verdict badges — so those twenty are recovered from the decks
themselves rather than retyped; the strings are bilingual and a mangled Hebrew
transcription is not something review catches. The eighteen for lectures 1 to 6
are new, written against each slide's manifest description, because that content
exists nowhere in this repository or its history.

`lectureExamples.spec.ts` is the part that stops this recurring. Two numbers
decide whether a slide finds its row — `LECTURE_ITEM_ID` in the deck's client
and the position each slide passes — and both are literals sitting far from the
seed that has to match them. It reads the call sites and checks them against the
content and the seed, in both languages, with no database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrZUXGffUKSQBUKQtiSbeL
The DDL was a Python string holding two tables, and neither existed in the
Supabase project this API points at — which is why `/api/entitlements/course`
answers 503 and the admin screen lists nobody. Moving it to `schema.sql` next to
the module is not tidying: it is read far more often than it is run, and the
comments in it are the only documentation of what each table is for.

Six tables join them, each answering a question nothing could answer before:
who signed in, what they have finished, what they have asked the AI proxy for,
and what a completed test run found. All of them are new; none changes an
existing code path.

The security block at the bottom is the reason this file is worth reading. These
land in the same project as the content tables, `public` is the schema PostgREST
exposes, and Supabase grants the anon role broad access to new tables there by
default — so without it, `course_purchases` would be customer email addresses
and payment intent ids readable over HTTPS with a key that ships in the browser.
Row level security with no policy is a closed door to every role that is not the
owner, and the owner is the role this API connects as.

`api_rate_limits` also gets a purge. Rows are reused in place, so the table is
bounded by distinct callers rather than by traffic, which for IP-keyed buckets
is not bounded at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrZUXGffUKSQBUKQtiSbeL
…ng away

There was no user record anywhere. Identity existed only inside a signed cookie,
which is why entitlement is decided by an OR across email and subject on every
request, and why a purchase made at checkout by someone not signed in could
never be attached to the account that signed in afterwards. Signing in is the
moment those two can be connected, so that is where it now happens — and never
at the cost of a sign-in Google has already verified.

The Prometheus counters answer "how many, right now" and are gone at the next
restart. Both wanted the same four facts about a request, so `activity.py`
records the counter and the row from one call rather than from two that drift
apart. The row carries the same HMAC of the email the metric carries as a label,
so a spike on a dashboard and a row in a table can be matched up without either
holding an address; prompts, responses and keys are absent for the reason they
are absent from the labels.

Neither write can fail the request it describes. A request that succeeded and
then could not be written down still succeeded, and turning that into a 500
would make the record more important than the thing it records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrZUXGffUKSQBUKQtiSbeL
`ProgressContext` has tracked this since there was a client, in `localStorage` —
so clearing site data lost it, a phone and a laptop each held a different half
of it, and nothing on the server could see any of it. This is the same record
with an owner.

The merge is the whole design, not the write. Booleans are OR-ed, the answer
counter takes the larger value, the two id lists are unioned, and the response
is the result. Last-write-wins would mean that opening the site on a second
device silently discards what the first one recorded, which is precisely the
situation an account is supposed to fix. `lastTool` is the exception, because it
is a cursor rather than an achievement.

On the client the guard in `adopt` is what keeps that from looping: taking the
server's answer sets state, setting state re-runs the effect, and the next push
returns the same union — comparing before adopting makes the second round a
no-op instead of the next lap. A tab regaining focus re-reads rather than
merges, which is the other half of "follows you between devices": progress made
on a phone is already stored, and that is the moment the laptop is worth
telling.

`useOptionalAuth` exists because progress is tracked for everyone and synced
only for someone with an account, so a tree with no `AuthProvider` — which is
how the component tests mount it — is an ordinary state rather than the
programming error `useAuth` reports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrZUXGffUKSQBUKQtiSbeL
The Pushgateway holds one push per grouping key, so what the module called
"history" was one run deep — enough to say whether the latest run on a branch
was green, and unable to say when a suite started getting slower or which commit
first broke it. That is the question `test_runs` exists to answer, and it is the
one lecture 9 spends a slide teaching.

Both destinations now run, independently: a missing `PUSHGATEWAY_URL` or
`DATABASE_URL` disables that half and says which, and a failure in either is
reported rather than raised. This step runs after the suites have already
produced their verdict, so losing the record of a run must never be the reason
a build goes red.

Suite totals are zero-filled across every status on purpose. Without it, "this
suite had no failures" and "this suite did not report" are the same answer to a
query over the stored history — which is exactly the question the table is for.
A re-run claims its existing row rather than inserting a second one, because
otherwise re-running a job doubles every count against the same commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrZUXGffUKSQBUKQtiSbeL
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
learn-practice-work-ai-testing-academy Ready Ready Preview Sep 1, 2026 6:36am UTC

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Persist academy content, progress, activity, and test history

🐞 Bug fix ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Seeds 38 bilingual worked examples so lecture slides no longer render unavailable content.
• Persists accounts, merged learner progress, activity events, purchases, quotas, and test history.
• Adds progress contracts, generated clients, retention safeguards, CI publishing, and regression
 tests.
Diagram

graph TD
  Browser["Academy UI"] --> ProgressAPI["Progress API"] --> DB[("Supabase Postgres")]
  Auth["Auth routes"] --> Activity["Activity recorder"] --> DB
  CI["GitHub Actions"] --> History["Test history"] --> DB
  Seeds["Content seed"] --> DB
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Versioned database migrations
  • ➕ Provides an auditable schema history and deterministic upgrades
  • ➕ Supports future destructive or data-transforming changes safely
  • ➖ Adds migration tooling and operational process not currently present
  • ➖ Requires reconciling existing Supabase state before adoption
2. Server-authoritative progress replacement
  • ➕ Simplifies write semantics and preserves exact ordering
  • ➕ Allows explicit reset or uncomplete operations
  • ➖ Last-write-wins can discard progress from another device
  • ➖ Requires conflict versions or timestamps to avoid regressions
3. Queued activity ingestion
  • ➕ Removes database latency from login and AI request paths
  • ➕ Improves durability during temporary database outages
  • ➖ Introduces broker, worker, retry, and monitoring infrastructure
  • ➖ Disproportionate complexity for current event volume

Recommendation: Keep the union-based progress merge, best-effort activity writes, and repository-owned content generation in this PR. The main strategic follow-up should be adopting versioned migrations before schemas require destructive evolution; boot-time idempotent DDL is pragmatic now but does not provide upgrade history or rollback semantics.

Files changed (45) +4829 / -117

Enhancement (13) +805 / -97
AuthContext.tsxExpose optional authentication context +12/-0

Expose optional authentication context

• Adds a non-throwing auth hook for consumers such as progress tracking that function both inside and outside an AuthProvider.

artifacts/ai-testing-academy/src/context/AuthContext.tsx

ProgressContext.tsxSynchronize local progress across signed-in devices +98/-40

Synchronize local progress across signed-in devices

• Validates local state, debounces server merges, adopts returned unions, and refreshes progress when a tab becomes visible. Anonymous and offline behavior continues using localStorage.

artifacts/ai-testing-academy/src/context/ProgressContext.tsx

progressApi.tsAdd resilient progress API client +102/-0

Add resilient progress API client

• Defines bounded progress types and validation, plus failure-tolerant GET and PUT helpers that preserve local behavior when remote storage is unavailable.

artifacts/ai-testing-academy/src/lib/progressApi.ts

generate-academy-seed-sql.tsGenerate lecture example seed rows +60/-4

Generate lecture example seed rows

• Reads structured worked-example content, maps decks to seeded lecture IDs, serializes panels to JSONB, and updates generated sequence values.

scripts/src/generate-academy-seed-sql.ts

activity.pyUnify metrics and durable activity recording +57/-0

Unify metrics and durable activity recording

• Adds helpers that emit existing Prometheus observations and privacy-preserving login or AI usage rows from the same request facts.

server/app/activity.py

database.pyAdd durable application data operations +311/-35

Add durable application data operations

• Moves DDL into a SQL file and adds retention cleanup, account linking, monotonic progress merges, best-effort activity writes, and rerun-safe test-history persistence.

server/app/database.py

dependencies.pyWire the progress service dependency +8/-0

Wire the progress service dependency

• Connects database progress functions to a FastAPI-injected ProgressService type.

server/app/dependencies.py

progress.pyAdd learner progress service +55/-0

Add learner progress service

• Wraps progress load and merge operations and distinguishes unavailable storage from query failures.

server/app/progress.py

__init__.pyRegister progress routes +2/-1

Register progress routes

• Adds the progress router to the server's API router collection.

server/app/routes/init.py

ai.pyPersist AI request outcomes +13/-11

Persist AI request outcomes

• Routes successful, invalid, and throttled AI observations through the shared activity helper so metrics and durable rows remain aligned.

server/app/routes/ai.py

auth.pyPersist accounts and login outcomes +17/-6

Persist accounts and login outcomes

• Creates or refreshes account records after verified sign-in, links email-only purchases to Google subjects, and records every login outcome without failing authentication.

server/app/routes/auth.py

progress.pyExpose authenticated progress endpoints +35/-0

Expose authenticated progress endpoints

• Adds private GET and PUT routes, validates untrusted device state, and returns the server's merged progress union.

server/app/routes/progress.py

schemas.pyValidate bounded progress payloads +35/-0

Validate bounded progress payloads

• Adds strict progress request validation, including counter, list-size, identifier-length, and last-tool constraints.

server/app/schemas.py

Bug fix (1) +78 / -1
academy-seed.sqlSeed all bilingual worked examples +78/-1

Seed all bilingual worked examples

• Adds 76 language-specific rows covering 38 worked-example slides and updates truncation and identity sequence handling.

scripts/src/academy-seed.sql

Tests (5) +396 / -2
test_activity.pyTest durable activity recording behavior +85/-0

Test durable activity recording behavior

• Covers successful and rejected logins, throttled AI calls, privacy guarantees, and non-fatal database write failures.

server/tests/test_activity.py

test_progress.pyTest progress authorization and contracts +124/-0

Test progress authorization and contracts

• Covers private access, merged responses, absent and broken stores, and rejection of unknown or oversized input.

server/tests/test_progress.py

test_test_history.pyTest Postgres test-history preparation +51/-1

Test Postgres test-history preparation

• Verifies status zero-filling, optional database storage, and GitHub Actions run identity extraction.

server/tests/test_test_history.py

contentSchema.spec.tsCover lecture examples in schema checks +5/-1

Cover lecture examples in schema checks

• Requires the new table in academy schema and seed validation and updates the expected RLS table count.

tests/unit/contentSchema.spec.ts

lectureExamples.spec.tsValidate every worked-example call site +131/-0

Validate every worked-example call site

• Discovers deck fetch calls and pinned lecture IDs, then verifies matching bilingual source and seed rows without requiring a database.

tests/unit/lectureExamples.spec.ts

Documentation (2) +2831 / -0
openapi.yamlSpecify the progress API contract +97/-0

Specify the progress API contract

• Documents authenticated progress reads and union-based writes, including response shapes and list and counter bounds.

lib/api-spec/openapi.yaml

lecture-examples.jsonAdd bilingual worked-example course content +2734/-0

Add bilingual worked-example course content

• Introduces the English and Hebrew source content for 38 examples across lectures 1–6, 9, and 10, including bullets, panels, and verdicts.

scripts/src/lecture-examples.json

Other (24) +719 / -17
ci.ymlPublish test history to Postgres and Prometheus +6/-1

Publish test history to Postgres and Prometheus

• Passes the database connection to the optional post-test publishing step and clarifies that both telemetry destinations are independent and non-blocking.

.github/workflows/ci.yml

api.schemas.tsGenerate progress client models +33/-0

Generate progress client models

• Adds generated progress, response, and last-tool TypeScript models with documented limits.

lib/api-client-react/src/generated/api.schemas.ts

api.tsGenerate progress query and mutation hooks +148/-0

Generate progress query and mutation hooks

• Adds generated GET and merge functions, React Query hooks, keys, and error types for the progress endpoint.

lib/api-client-react/src/generated/api.ts

api.tsGenerate progress request validators +100/-0

Generate progress request validators

• Adds generated Zod schemas for progress reads, merge bodies, and merged responses with contract limits.

lib/api-zod/src/generated/api.ts

index.tsExport generated progress types +3/-0

Export generated progress types

• Exports the new progress model, last-tool enum, and response type from the generated type barrel.

lib/api-zod/src/generated/types/index.ts

progress.tsDefine generated progress type +26/-0

Define generated progress type

• Adds the generated TypeScript interface for all learner progress fields and bounds.

lib/api-zod/src/generated/types/progress.ts

progressLastTool.tsDefine generated last-tool enum +18/-0

Define generated last-tool enum

• Adds the nullable generated enum for resume, interview, and practice cursors.

lib/api-zod/src/generated/types/progressLastTool.ts

progressResponse.tsDefine generated progress response +12/-0

Define generated progress response

• Adds the generated response wrapper containing the merged or stored progress record.

lib/api-zod/src/generated/types/progressResponse.ts

academy-schema.sqlCreate the public lecture examples table +32/-3

Create the public lecture examples table

• Adds the keyed bilingual worked-example table, JSON panel storage, public read grants, and row-level security policy.

scripts/src/academy-schema.sql

seed-chunk-00.sqlInclude lecture examples in seed reset +1/-1

Include lecture examples in seed reset

• Extends the first deployment seed chunk to truncate the lecture_examples table.

scripts/src/seed-chunk-00.sql

seed-chunk-36.sqlBegin generated English example inserts +5/-5

Begin generated English example inserts

• Replaces premature sequence resets with the first generated lecture example rows.

scripts/src/seed-chunk-36.sql

seed-chunk-37.sqlContinue English worked-example seed rows +8/-1

Continue English worked-example seed rows

• Adds generated English examples for lectures 2–5 and moves sequence handling later.

scripts/src/seed-chunk-37.sql

seed-chunk-38.sqlSeed English examples for lectures 5–9 +8/-0

Seed English examples for lectures 5–9

• Adds the next generated batch of English worked-example records.

scripts/src/seed-chunk-38.sql

seed-chunk-39.sqlSeed English analytics examples +8/-0

Seed English analytics examples

• Adds English worked examples covering coverage, security, performance, strategy, ROI, CI history, and scorecards.

scripts/src/seed-chunk-39.sql

seed-chunk-40.sqlSeed English strategy and operations examples +8/-0

Seed English strategy and operations examples

• Adds English examples for evaluation history, maturity, scorecards, rollout, drift, security, and latency.

scripts/src/seed-chunk-40.sql

seed-chunk-41.sqlFinish English and begin Hebrew examples +8/-0

Finish English and begin Hebrew examples

• Adds the final English cost example and starts the generated Hebrew worked-example rows.

scripts/src/seed-chunk-41.sql

seed-chunk-42.sqlContinue Hebrew worked-example seed rows +8/-0

Continue Hebrew worked-example seed rows

• Adds Hebrew examples for factuality, schema validation, UI testing, mocking, and edge cases.

scripts/src/seed-chunk-42.sql

seed-chunk-43.sqlSeed Hebrew CI and analytics examples +8/-0

Seed Hebrew CI and analytics examples

• Adds Hebrew examples for CI design and Supabase-backed testing analytics.

scripts/src/seed-chunk-43.sql

seed-chunk-44.sqlSeed Hebrew metrics and scorecard examples +8/-0

Seed Hebrew metrics and scorecard examples

• Adds Hebrew examples for performance, strategy metrics, ROI, CI history, quality, and evaluations.

scripts/src/seed-chunk-44.sql

seed-chunk-45.sqlFinish Hebrew worked-example inserts +8/-0

Finish Hebrew worked-example inserts

• Adds the remaining Hebrew scorecard, audit, rollout, drift, security, latency, and cost examples, then begins sequence resets.

scripts/src/seed-chunk-45.sql

seed-chunk-46.sqlFinalize generated seed sequences +6/-0

Finalize generated seed sequences

• Sets all content identity sequences, including lecture_examples, after chunked inserts complete.

scripts/src/seed-chunk-46.sql

main.pyPermit cross-origin progress writes +1/-1

Permit cross-origin progress writes

• Adds PUT to the CORS method allowlist for the new progress merge endpoint.

server/app/main.py

schema.sqlDefine secured durable application tables +171/-0

Define secured durable application tables

• Creates purchases, quotas, accounts, learner progress, activity events, and test-history tables with indexes. Enables RLS and revokes PostgREST roles from all private data.

server/app/schema.sql

test_history.pyAccumulate CI test history in Postgres +85/-5

Accumulate CI test history in Postgres

• Transforms Allure results into zero-filled per-suite totals and independently publishes them to Pushgateway and Postgres without failing CI.

server/app/test_history.py

@amielnoy
amielnoy merged commit bd82598 into fix/ci-format-generated Sep 1, 2026
9 checks passed
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Existing database boot fails 🐞 Bug ☼ Reliability
Description
Removing the compatibility ALTER TABLE means an existing course_purchases table without
retention_until is left unchanged by CREATE TABLE IF NOT EXISTS, after which schema
initialization tries to index the missing column and fails. Deployments using the prior schema
therefore cannot start against their existing database.
Code

server/app/database.py[L64-67]

-        connection.execute(
-            """ALTER TABLE course_purchases
-               ADD COLUMN IF NOT EXISTS retention_until timestamptz"""
-        )
Evidence
The new startup path executes the schema and immediately updates retention_until; the schema only
conditionally creates the table but unconditionally creates an index on that column. This proves an
older existing table without the column fails before initialization completes.

server/app/database.py[57-67]
server/app/schema.sql[18-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Existing databases may have `course_purchases` without `retention_until`, and `CREATE TABLE IF NOT EXISTS` does not add that column. Restore an idempotent migration before any index, update, or delete references it.

## Issue Context
The previous initialization explicitly added the column, while the new schema assumes it is already present.

## Fix Focus Areas
- server/app/database.py[57-67]
- server/app/schema.sql[18-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Progress crosses user accounts 🐞 Bug ≡ Correctness
Description
ProgressProvider keeps one global localStorage/state copy and pushes it whenever any user becomes
signed in, so signing out user A and signing in user B merges A's progress into B's server record.
Neither the auth transition nor the sync effect resets or namespaces progress by identity.
Code

artifacts/ai-testing-academy/src/context/ProgressContext.tsx[R107-112]

+  useEffect(() => {
+    if (!signedIn) return;
+    const controller = new AbortController();
+    const timer = window.setTimeout(() => {
+      void pushProgress(progress, controller.signal).then(adopt);
+    }, SYNC_DELAY_MS);
Evidence
The provider loads and writes one ata_progress_v1 key, remains mounted inside the auth provider,
and starts a PUT solely when signedIn is true. Login/logout only replace the auth user, while the
server scopes the resulting merge to whichever user owns the current session.

artifacts/ai-testing-academy/src/context/ProgressContext.tsx[24-42]
artifacts/ai-testing-academy/src/context/ProgressContext.tsx[72-117]
artifacts/ai-testing-academy/src/context/AuthContext.tsx[96-140]
artifacts/ai-testing-academy/src/App.tsx[33-46]
server/app/routes/progress.py[24-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Progress retained from one account is pushed under the next account's authenticated session. Partition or reset progress when the authenticated identity changes while preserving a deliberate anonymous-to-first-account merge.

## Issue Context
The provider remains mounted through logout/login and currently tracks only `user !== null`, not which user is active.

## Fix Focus Areas
- artifacts/ai-testing-academy/src/context/ProgressContext.tsx[24-42]
- artifacts/ai-testing-academy/src/context/ProgressContext.tsx[72-117]
- artifacts/ai-testing-academy/src/context/AuthContext.tsx[96-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Stale refresh erases progress 🐞 Bug ≡ Correctness
Description
adopt replaces current state and localStorage with every differing response, so a visibility GET
started before a local update can return the older server copy and erase that unsynced update. The
same replacement is also unsafe for delayed PUT responses because no request generation or
state-version check is applied.
Code

artifacts/ai-testing-academy/src/context/ProgressContext.tsx[R98-101]

+      if (sameProgress(current, remote)) return current;
+      writeValidated(localStorage, STORAGE_KEY, remote);
+      return remote;
+    });
Evidence
Local updates write the newest state, while adopt unconditionally installs a different remote
object. The visibility handler can fetch at any later visibility event and is not cancelled by
progress changes, establishing a direct stale-response data-loss path.

artifacts/ai-testing-academy/src/context/ProgressContext.tsx[79-101]
artifacts/ai-testing-academy/src/context/ProgressContext.tsx[119-136]
server/app/routes/progress.py[17-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Asynchronous GET/PUT responses can be older than the current local state, but `adopt` replaces that state outright. Preserve monotonic local achievements when adopting remote data and reject responses from stale request/state generations.

## Issue Context
The visibility GET is independent of progress changes, so its controller is not aborted when the user records new progress.

## Fix Focus Areas
- artifacts/ai-testing-academy/src/context/ProgressContext.tsx[95-136]
- artifacts/ai-testing-academy/src/lib/progressApi.ts[63-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Stage sequence remains reset 🐞 Bug ≡ Correctness
Description
The chunked seed removes the question_bank_stages_id_seq advancement and never restores it in the
new final chunk, although stages are inserted with explicit IDs. After applying the documented chunk
sequence, the next default-ID stage insert therefore starts at 1 and collides with an existing row.
Code

scripts/src/seed-chunk-36.sql[4]

-select setval('question_bank_stages_id_seq', 10);
Evidence
The full seed advances the stage sequence to 10, while the final chunk advances all listed sequences
except stages. Because the schema uses an identity primary key and the seed inserts explicit stage
IDs, the omitted sequence update causes the next generated ID to collide.

scripts/src/seed-chunk-46.sql[1-6]
scripts/src/academy-seed.sql[1-12]
scripts/src/academy-seed.sql[1027-1034]
scripts/src/academy-schema.sql[29-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The chunked seed no longer advances `question_bank_stages_id_seq` after inserting explicit stage IDs. Add the missing `setval` to the generated final chunk and ensure chunk generation retains every statement from the full seed.

## Issue Context
The full seed contains the correct stage sequence statement, but the committed chunk set does not.

## Fix Focus Areas
- scripts/src/seed-chunk-36.sql[1-8]
- scripts/src/seed-chunk-46.sql[1-6]
- scripts/src/academy-seed.sql[1027-1034]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Unbounded growth of merged progress arrays 🐞 Bug ≡ Correctness
Description
The SQL merge in _merge_progress unions the full stored practice_completed and lectures_viewed
arrays with incoming arrays capped at 500, but never truncates the merged result, allowing
authenticated clients syncing disjoint sets across repeated requests or devices to grow the
persisted arrays indefinitely and defeat the intended row-size bound. Client-side response
validation then truncates the arrays to 500, causing entries beyond the cutoff to disappear from
device state while the unrestricted database row continues growing.
Code

server/app/database.py[R399-406]

+                  practice_completed = COALESCE((
+                    SELECT array_agg(DISTINCT id) FROM unnest(
+                      learner_progress.practice_completed || EXCLUDED.practice_completed) AS id
+                  ), '{{}}')::text[],
+                  lectures_viewed = COALESCE((
+                    SELECT array_agg(DISTINCT id) FROM unnest(
+                      learner_progress.lectures_viewed || EXCLUDED.lectures_viewed) AS id
+                  ), '{{}}')::text[],
Evidence
The incoming lists are sliced to MAX_PROGRESS_IDS (500) in Python before being passed to the
upsert, but both ON CONFLICT expressions aggregate every distinct element from the full stored
arrays and the incoming arrays without applying a SQL LIMIT or array slice to the result. The
unrestricted text-array schema provides no database-level constraint, so a stored 500-item array
merged with a disjoint 500-item request can become 1,000 items and continue growing with subsequent
disjoint merges, while the client slices server responses back to 500.

server/app/database.py[295-299]
server/app/database.py[369-379]
server/app/database.py[382-409]
server/app/database.py[369-406]
server/app/schema.sql[70-80]
artifacts/ai-testing-academy/src/lib/progressApi.ts[41-59]
artifacts/ai-testing-academy/src/lib/progressApi.ts[81-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The intended 500-ID bound currently applies only to each incoming request, not to the accumulated SQL union. Enforce `MAX_PROGRESS_IDS` on the merged `practice_completed` and `lectures_viewed` results so persisted arrays cannot grow indefinitely, and keep client and server truncation behavior consistent.

## Issue Context
`MAX_PROGRESS_IDS` is intended to bound row size and is applied to incoming payloads through Python slicing before the upsert. However, the `ON CONFLICT DO UPDATE` expressions union the complete stored arrays with the incoming arrays and write every distinct value back without truncation; authenticated clients can therefore submit repeated disjoint batches, the array columns have no database constraint, and the client subsequently truncates server responses to 500.

## Fix Focus Areas
- server/app/database.py[295-299]
- server/app/database.py[369-406]
- server/app/schema.sql[70-80]
- artifacts/ai-testing-academy/src/lib/progressApi.ts[41-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. First CI test-history publish may fail 🐞 Bug ☼ Reliability
Description
test_history.store() inserts directly into test_runs/test_suite_results via
record_test_run() without ever calling initialize_database() (which runs schema.sql), so
against a fresh Supabase database where the API server has never started, the insert fails with a
missing-relation error; the failure is silently swallowed by main()'s try/except, so the run's
history is simply never recorded with no hard failure signal beyond a printed message.
Code

server/app/test_history.py[R59-76]

+def store(directory: Path) -> bool:
+    """Keep the run in Postgres. False when no database is configured."""
+    if not database_url():
+        print("DATABASE_URL is not configured; test-history storage is disabled.")
+        return False
+    counts, durations = read_allure_results(directory)
+    if not counts:
+        raise RuntimeError(f"No Allure test results found in {directory}")
+    identity = run_identity()
+    record_test_run(
+        repository=identity["repository"],
+        branch=identity["branch"],
+        commit_sha=identity["commit"],
+        run_id=identity["run_id"],
+        run_attempt=positive_int("GITHUB_RUN_ATTEMPT", 1),
+        suites=suite_totals(counts, durations),
+    )
+    return True
Evidence
store() (server/app/test_history.py:59-76) calls record_test_run() (server/app/database.py:472-520)
directly without ensuring schema exists; initialize_database() is only invoked from the FastAPI
lifespan hook in server/app/main.py:30-37. If CI's publish step runs before the API has ever started
against a given database, the INSERT into test_runs fails; main() catches and prints the exception
(server/app/test_history.py:163-177) rather than failing the build, so the loss is silent.

server/app/test_history.py[59-76]
server/app/database.py[472-520]
server/app/main.py[30-37]
server/app/test_history.py[163-177]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`test_history.store()` writes to `test_runs`/`test_suite_results` without ensuring the schema has been applied, so the very first CI run against a fresh database (before the API has started at least once) will fail to persist history, and the failure is silently swallowed.

## Issue Context
`initialize_database()` in server/app/database.py applies `schema.sql` (including `test_runs`/`test_suite_results`) but is only called from the FastAPI lifespan startup in server/app/main.py. The standalone CI script `python -m app.test_history` never triggers it.

## Fix Focus Areas
- server/app/test_history.py[59-76]
- server/app/database.py[472-520]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR spans multiple independent, high-risk paths—database schema and persistence, authentication/purchases, progress merging, AI usage, test history, API contracts, and substantial course content—creating a dense set of easy-to-miss defects beyond a single-pass review.

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread server/app/database.py
Comment on lines -64 to -67
connection.execute(
"""ALTER TABLE course_purchases
ADD COLUMN IF NOT EXISTS retention_until timestamptz"""
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Existing database boot fails 🐞 Bug ☼ Reliability

Removing the compatibility ALTER TABLE means an existing course_purchases table without
retention_until is left unchanged by CREATE TABLE IF NOT EXISTS, after which schema
initialization tries to index the missing column and fails. Deployments using the prior schema
therefore cannot start against their existing database.
Agent Prompt
## Issue description
Existing databases may have `course_purchases` without `retention_until`, and `CREATE TABLE IF NOT EXISTS` does not add that column. Restore an idempotent migration before any index, update, or delete references it.

## Issue Context
The previous initialization explicitly added the column, while the new schema assumes it is already present.

## Fix Focus Areas
- server/app/database.py[57-67]
- server/app/schema.sql[18-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +107 to +112
useEffect(() => {
if (!signedIn) return;
const controller = new AbortController();
const timer = window.setTimeout(() => {
void pushProgress(progress, controller.signal).then(adopt);
}, SYNC_DELAY_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Progress crosses user accounts 🐞 Bug ≡ Correctness

ProgressProvider keeps one global localStorage/state copy and pushes it whenever any user becomes
signed in, so signing out user A and signing in user B merges A's progress into B's server record.
Neither the auth transition nor the sync effect resets or namespaces progress by identity.
Agent Prompt
## Issue description
Progress retained from one account is pushed under the next account's authenticated session. Partition or reset progress when the authenticated identity changes while preserving a deliberate anonymous-to-first-account merge.

## Issue Context
The provider remains mounted through logout/login and currently tracks only `user !== null`, not which user is active.

## Fix Focus Areas
- artifacts/ai-testing-academy/src/context/ProgressContext.tsx[24-42]
- artifacts/ai-testing-academy/src/context/ProgressContext.tsx[72-117]
- artifacts/ai-testing-academy/src/context/AuthContext.tsx[96-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +98 to +101
if (sameProgress(current, remote)) return current;
writeValidated(localStorage, STORAGE_KEY, remote);
return remote;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Stale refresh erases progress 🐞 Bug ≡ Correctness

adopt replaces current state and localStorage with every differing response, so a visibility GET
started before a local update can return the older server copy and erase that unsynced update. The
same replacement is also unsafe for delayed PUT responses because no request generation or
state-version check is applied.
Agent Prompt
## Issue description
Asynchronous GET/PUT responses can be older than the current local state, but `adopt` replaces that state outright. Preserve monotonic local achievements when adopting remote data and reject responses from stale request/state generations.

## Issue Context
The visibility GET is independent of progress changes, so its controller is not aborted when the user records new progress.

## Fix Focus Areas
- artifacts/ai-testing-academy/src/context/ProgressContext.tsx[95-136]
- artifacts/ai-testing-academy/src/lib/progressApi.ts[63-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

insert into lecture_items (id, track_id, position, num, ready, title, description, url) values (38, 4, 7, 8, false, 'אבטחת מערכות ה-AI עצמן', 'הצד השני של המטבע — הגנה על מערכות ה-AI שלכם מפני prompt injection, גניבת מודלים, הרעלת נתונים וסיכוני שרשרת אספקה.', null);
insert into lecture_items (id, track_id, position, num, ready, title, description, url) values (39, 4, 8, 9, false, 'תגובה לאירועי אבטחה בסיוע AI', 'שימוש בעוזרי AI כדי להאיץ טריאז'', ניתוח שורש הבעיה ודיווח במהלך אירוע אבטחה חי.', null);
insert into lecture_items (id, track_id, position, num, ready, title, description, url) values (40, 4, 9, 10, false, 'בניית אסטרטגיית AI לאבטחת מידע', 'הכל ביחד — מפת דרכים מעשית לאימוץ AI על פני זיהוי, תגובה ומניעה בתוכנית האבטחה שלכם.', null);
select setval('question_bank_stages_id_seq', 10);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Stage sequence remains reset 🐞 Bug ≡ Correctness

The chunked seed removes the question_bank_stages_id_seq advancement and never restores it in the
new final chunk, although stages are inserted with explicit IDs. After applying the documented chunk
sequence, the next default-ID stage insert therefore starts at 1 and collides with an existing row.
Agent Prompt
## Issue description
The chunked seed no longer advances `question_bank_stages_id_seq` after inserting explicit stage IDs. Add the missing `setval` to the generated final chunk and ensure chunk generation retains every statement from the full seed.

## Issue Context
The full seed contains the correct stage sequence statement, but the committed chunk set does not.

## Fix Focus Areas
- scripts/src/seed-chunk-36.sql[1-8]
- scripts/src/seed-chunk-46.sql[1-6]
- scripts/src/academy-seed.sql[1027-1034]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread server/app/database.py
Comment on lines +399 to +406
practice_completed = COALESCE((
SELECT array_agg(DISTINCT id) FROM unnest(
learner_progress.practice_completed || EXCLUDED.practice_completed) AS id
), '{{}}')::text[],
lectures_viewed = COALESCE((
SELECT array_agg(DISTINCT id) FROM unnest(
learner_progress.lectures_viewed || EXCLUDED.lectures_viewed) AS id
), '{{}}')::text[],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Unbounded growth of merged progress arrays 🐞 Bug ≡ Correctness

The SQL merge in _merge_progress unions the full stored practice_completed and lectures_viewed
arrays with incoming arrays capped at 500, but never truncates the merged result, allowing
authenticated clients syncing disjoint sets across repeated requests or devices to grow the
persisted arrays indefinitely and defeat the intended row-size bound. Client-side response
validation then truncates the arrays to 500, causing entries beyond the cutoff to disappear from
device state while the unrestricted database row continues growing.
Agent Prompt
## Issue description
The intended 500-ID bound currently applies only to each incoming request, not to the accumulated SQL union. Enforce `MAX_PROGRESS_IDS` on the merged `practice_completed` and `lectures_viewed` results so persisted arrays cannot grow indefinitely, and keep client and server truncation behavior consistent.

## Issue Context
`MAX_PROGRESS_IDS` is intended to bound row size and is applied to incoming payloads through Python slicing before the upsert. However, the `ON CONFLICT DO UPDATE` expressions union the complete stored arrays with the incoming arrays and write every distinct value back without truncation; authenticated clients can therefore submit repeated disjoint batches, the array columns have no database constraint, and the client subsequently truncates server responses to 500.

## Fix Focus Areas
- server/app/database.py[295-299]
- server/app/database.py[369-406]
- server/app/schema.sql[70-80]
- artifacts/ai-testing-academy/src/lib/progressApi.ts[41-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +59 to +76
def store(directory: Path) -> bool:
"""Keep the run in Postgres. False when no database is configured."""
if not database_url():
print("DATABASE_URL is not configured; test-history storage is disabled.")
return False
counts, durations = read_allure_results(directory)
if not counts:
raise RuntimeError(f"No Allure test results found in {directory}")
identity = run_identity()
record_test_run(
repository=identity["repository"],
branch=identity["branch"],
commit_sha=identity["commit"],
run_id=identity["run_id"],
run_attempt=positive_int("GITHUB_RUN_ATTEMPT", 1),
suites=suite_totals(counts, durations),
)
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

6. First ci test-history publish may fail 🐞 Bug ☼ Reliability

test_history.store() inserts directly into test_runs/test_suite_results via
record_test_run() without ever calling initialize_database() (which runs schema.sql), so
against a fresh Supabase database where the API server has never started, the insert fails with a
missing-relation error; the failure is silently swallowed by main()'s try/except, so the run's
history is simply never recorded with no hard failure signal beyond a printed message.
Agent Prompt
## Issue description
`test_history.store()` writes to `test_runs`/`test_suite_results` without ensuring the schema has been applied, so the very first CI run against a fresh database (before the API has started at least once) will fail to persist history, and the failure is silently swallowed.

## Issue Context
`initialize_database()` in server/app/database.py applies `schema.sql` (including `test_runs`/`test_suite_results`) but is only called from the FastAPI lifespan startup in server/app/main.py. The standalone CI script `python -m app.test_history` never triggers it.

## Fix Focus Areas
- server/app/test_history.py[59-76]
- server/app/database.py[472-520]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant