Skip to content

test(frontend): add Vitest + Testing Library and a frontend-test CI job - #158

Closed
lucastononro wants to merge 135 commits into
fix/118-vuln-scanfrom
fix/114-frontend-tests
Closed

test(frontend): add Vitest + Testing Library and a frontend-test CI job#158
lucastononro wants to merge 135 commits into
fix/118-vuln-scanfrom
fix/114-frontend-tests

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • frontend/package.json had no test runner (no jest/vitest/@testing-library/playwright) and no test script. Non-trivial client logic — the SSE dispatcher (useNotebookSSE.ts) and the fetch wrapper (api.ts) — shipped with zero coverage, so a single renamed event type or route path could silently break live updates with nothing to catch it.
  • Adds Vitest + jsdom + React Testing Library, a test script, two real test suites (20 cases), and a frontend-test CI job.

Changes

  • frontend/package.json: new test script (vitest run); devDependencies vitest, @vitejs/plugin-react, jsdom, @testing-library/react, @testing-library/jest-dom.
  • frontend/vitest.config.ts (new): jsdom environment, @/* alias matching tsconfig.json, globals on, setup file.
  • frontend/vitest.setup.ts (new): loads @testing-library/jest-dom/vitest matchers.
  • frontend/src/lib/notebook/useNotebookSSE.test.ts (new, 9 tests): a fake EventSource (jsdom has none) drives the hook — verifies event routing per notebook.* type, per-notebook filtering of cell-lifecycle events, that kernel/structure events bypass the filter, malformed-payload tolerance, that the stream opens scoped to sessionId and is skipped when disabled, closes on unmount, and that handler updates take effect without re-opening the underlying EventSource.
  • frontend/src/lib/api.test.ts (new, 11 tests): the fetchJSON-backed helpers (GET/POST bodies, conditional query-string building in listExperiments, the not-ok → Error path with status+text) and the raw-fetch FormData helpers (createExperiment's upload-failure path, attachData's webkitRelativePath-vs-plain-name fallback for folder uploads).
  • .github/workflows/ci.yml: new frontend-test job — npm ci && npm test.

Test plan

Existing

  • npx tsc --noEmit: clean.
  • npx next lint: no warnings or errors.
  • npx prettier --check 'src/**/*.{ts,tsx,css}': clean (ran prettier --write once to match house style before committing).
  • npm run build: succeeds unchanged (7 static/dynamic routes generated).

New

  • npm ci && npm test (the exact new CI command): 20 passed, 0 failed, 2 test files.

Caveats

  • Scope is the two modules the issue calls out by name (SSE dispatcher + api.ts); AppContext.tsx, the lineage graph, and notebook cell components remain untested — good next targets but out of scope for this PR.
  • No Playwright/e2e smoke test added (issue's "suggested fix" mentions it as an option); this PR sticks to unit/hook-level Vitest tests, which was enough to close the "zero tests" gap and land a real frontend-test CI gate.

Closes #114

🤖 Generated with Claude Code

Greptile Summary

This PR introduces the first testing infrastructure for the frontend: Vitest + jsdom + React Testing Library, a vitest.config.ts that mirrors the existing tsconfig alias, a vitest.setup.ts loader, and a frontend-test CI job that runs on every push alongside the existing lint and build jobs.

  • api.test.ts (11 cases): validates GET/POST bodies, conditional query-string construction in listExperiments, the fetchJSON error-throw path, sendMessage conditional field inclusion, and both raw-fetch FormData helpers including the webkitRelativePath fallback for folder uploads.
  • useNotebookSSE.test.ts (9 cases): a FakeEventSource drives the hook end-to-end — verifies session-scoped URL, disabled/null guards, per-notebook filtering, that kernel/structure/notebook.created events bypass the filter, filterRef and handlersRef live-update on re-render without re-opening the stream, malformed-payload tolerance, and EventSource.close() on unmount.
  • .github/workflows/ci.yml: the new job is consistent with the existing frontend-lint and frontend-build jobs in structure, Node version, and npm cache configuration.

Confidence Score: 5/5

Safe to merge — adds test infrastructure and new test files only; no production code is modified.

All changes are additive (test files, config, CI job). The test assertions were verified against the actual implementation in api.ts and useNotebookSSE.ts — URLs, method names, error messages, and filter logic all match. The CI job structure is consistent with the existing frontend jobs.

No files require special attention.

Important Files Changed

Filename Overview
.github/workflows/ci.yml Adds frontend-test job that mirrors the structure of the existing frontend-lint and frontend-build jobs — correct working-directory, cache-dependency-path, and Node 20 setup.
frontend/vitest.config.ts Configures jsdom environment, @/* alias matching tsconfig, globals, and the setup file. Config is minimal and correct.
frontend/package.json Adds test script (vitest run) and five testing devDependencies; all version pins look current and consistent with package-lock.json.
frontend/src/lib/api.test.ts 11 tests covering fetchJSON-backed helpers (GET/POST bodies, conditional query-string, error throw) and raw-fetch FormData helpers (createExperiment upload failure, attachData webkitRelativePath fallback); test assertions match the actual implementation.
frontend/src/lib/notebook/useNotebookSSE.test.ts 9 tests with a well-structured FakeEventSource. Covers session-scoped URL, disabled/null guard, filter behaviour, handler-ref updates, malformed payload tolerance, and unmount cleanup. Three cell-lifecycle event types (cell.stream, cell.display, cell.error) remain untested.

Reviews (2): Last reviewed commit: "fix(review): address Greptile findings o..." | Re-trigger Greptile

lucastononro and others added 27 commits May 12, 2026 20:29
Adds `GET /api/sessions/{sid}/download` and `GET /api/projects/{pid}/download`
that stream a self-contained zip of the agent's workspace: every file under
`/sessions/{sid}/` plus a synthetic `trainable_local.py` shim, a filtered
`requirements.txt`, and a runbook README. The shim lets downloaded scripts
that import `from trainable import log, log_image, ...` run on a vanilla
Python install — calls land in `./trainable_out/` instead of the Modal
volume.

The zip is streamed via `StreamingResponse` over an in-memory buffer
drained per write, so multi-hundred-MB sessions don't materialize on
disk or in RAM. A 2 GB uncompressed cap with a trailing `__truncated.txt`
marker keeps a runaway walk bounded.

Frontend ships two entry points:
- Download icon in the WorkspaceSidebar header → session zip
- Download icon on every Project row in the sidebar → project zip

Closes #79.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cli/README.md was stale against v0.0.4: it described a single-provider
(Anthropic + Modal) setup, claimed the wizard downloaded the compose file
from raw.githubusercontent.com, and omitted `trainable reconfigure`. Update
it to match the current implementation:

- multi-provider selection (Claude API key / subscription OAuth, OpenAI,
  Gemini, LiteLLM-routed backends) as a flat picker, with a Providers table
- compose file bundled inside the wheel (offline installs)
- version-pinned images on `trainable up`
- `trainable reconfigure` command and the existing-config merge flow

Assisted-by: Claude Code
…t:8000 (#96)

Replace getSSEBase()/getBackendUrl() (which returned http://host:8000)
with relative /api paths at all EventSource and raw file/image/PDF fetch
sites, routing through the existing Next rewrite proxy so HTTPS deploys
no longer hit mixed-content blocking or an unexposed port. Both helpers
are deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
…tores (#90)

docker-compose.prod.yml shipped hardcoded default credentials
(trainable/trainable, minioadmin/minioadmin) and published Postgres
(5432), MinIO (9000/9001) and the auth-less Jaeger UI (16686) on
0.0.0.0 — direct data-store compromise with well-known creds on any
networked host, and Jaeger traces leak prompts/paths/token counts.

- Replace every hardcoded secret with required env-var interpolation
  that fails loudly if unset: POSTGRES_USER/PASSWORD/DB and
  MINIO_ROOT_USER/PASSWORD via `${VAR:?set VAR in .env}`. The backend's
  DATABASE_URL and AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are now
  derived from those same vars, so app creds always match the
  datastores (var names match what backend/config.py reads).
- Bind Postgres, MinIO API+console and the Jaeger UI to 127.0.0.1 so
  they are reachable only from the host / internal compose network,
  never from another machine. Drop the Jaeger OTLP host publishes
  (4317/4318) — the backend reaches them by service name internally.
  frontend (3000) and backend (8000) stay published as before.
- Document the newly-required vars in .env.example with placeholders
  and a note that prod has no defaults.

Validated: `docker compose -f docker-compose.prod.yml config` parses
and interpolates with a filled .env, and fails fast with a clear
message when the secrets are unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Add an opt-in shared-secret gate for the API. When API_AUTH_TOKEN is
unset (the default) nothing changes — every endpoint stays open, so
local dev and the frontend are unaffected. When set, all /api/*
requests must send `Authorization: Bearer <token>`.

- backend/auth.py: pure-ASGI BearerTokenAuthMiddleware (no
  BaseHTTPMiddleware wrapping, so SSE streaming is untouched);
  constant-time token comparison; 401 + WWW-Authenticate on failure.
- Exempt: /api/health, /api/readyz, CORS preflight (OPTIONS).
- SSE: EventSource cannot send headers, so the stream endpoint
  (/api/sessions/{id}/stream) additionally accepts ?token=<token>;
  the query param is stream-only and does not unlock other routes.
- config.py: api_auth_token setting (env API_AUTH_TOKEN, default None).
- Middleware added before CORSMiddleware so CORS stays outermost and
  answers preflights before auth runs.
- .env.example: new "Optional — security" section documenting it.
- tests/test_api_auth.py: middleware unit tests (open-by-default,
  401/200 matrix, exemptions, stream query token, preflight).

Closes #88

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
…edentials

Replace the `cors_origins=["*"]` + `allow_credentials=True` default
(any web page could script credentialed cross-origin requests against
localhost:8000) with a safe, configurable policy:

- config.py: default origins are the local frontend
  (http://localhost:3000, http://127.0.0.1:3000); overridable via
  CORS_ORIGINS as a comma-separated env var (NoDecode + validator so
  plain CSV works, not just JSON).
- main.py: if `*` is configured it is honored but credentials are
  disabled (allow_credentials=False) with a startup warning — the
  wildcard+credentials combination is now impossible.
- .env.example: document CORS_ORIGINS.

No impact on the standard setup: the frontend proxies /api through
Next.js rewrites (same-origin), so CORS is not involved there.

Closes #89

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
- Stream uploads to S3 in bounded 8 MB chunks (single put_object for small
  bodies, multipart upload for larger ones) instead of reading the whole
  body into RAM; abort the multipart upload on any failure.
- Enforce the configured max_upload_size_bytes cap incrementally while
  reading (413 without buffering the oversize body).
- Validate bucket against the app-provisioned allowlist (new
  settings.s3_allowed_buckets, also used by startup bucket init) and
  reject traversal/absolute/backslash keys; write endpoints (upload +
  put-presign) are scoped to the datasets/projects/ prefix.
- Authentication is intentionally NOT added here — it lands globally via
  issue #88.

Closes #91

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
A stalled provider HTTP call had no wall-clock limit: the runner's outer
asyncio.timeout was deliberately removed (the Modal sandbox timeout only
bounds tool execution), and every provider accepted timeout_seconds but
discarded it. A hung network call left the session's background task
alive forever — spinner never cleared, task-registry entry never freed.

Thread the existing timeout_seconds into each provider around the HTTP
call only, so tool-execution time is never counted:

- base.py: new enforce_wall_clock() helper — asyncio.wait_for around a
  single provider call, raising builtin TimeoutError on expiry.
- openai: per-request SDK timeout via client.with_options(timeout=...)
  plus the hard wall-clock cap (SDK retries can't stretch past it).
- gemini: hard cap around generate_content (google-genai has no default
  request timeout at all).
- litellm: keep the existing per-attempt timeout= knob, add the hard cap
  so backend retries/fallbacks can't exceed the budget.
- claude: query() runs the whole tool loop internally, so it must not be
  wrapped wholesale; bound each underlying API HTTP request via Claude
  Code's API_TIMEOUT_MS env instead (caller-supplied value wins).

On expiry the providers let TimeoutError propagate: run_agent's existing
TimeoutError handler publishes agent_timeout, moves the session to
timed_out, and the finally block frees the task registry — the run ends
cleanly instead of hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Every boto3 call ran synchronously inside async def handlers/services,
stalling the event loop for each S3 round-trip. Wrap them all in
asyncio.to_thread, behavior-identical:

- services/s3_sync.py: put_object in the per-file sync loop
- routers/s3_browser.py: list_buckets, list_objects_v2, presign
  generation, and the whole upload path (put_object + multipart
  create/upload_part/complete/abort)
- routers/experiments.py: put_object in create_experiment/attach_data;
  the four get_object(...).read() sites now stream to a temp file in
  bounded 1 MB chunks inside a worker thread (_download_to_tempfile)
  instead of reading whole bodies into memory on the loop

Test mock: conftest get_object now returns a fresh BytesIO body per call
so chunked reads hit EOF like a real StreamingBody.

Closes #92

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
/api/health stays a cheap static liveness check. New /api/readyz pings
the DB (SELECT 1 on the async engine) and S3 (list_buckets via
asyncio.to_thread so sync boto3 doesn't block the event loop) and
returns 503 with per-check detail when either is down, so an
orchestrator no longer routes traffic to a backend whose Postgres pool
or object store is dead.

Compose wiring (both docker-compose.yml and docker-compose.prod.yml):
- backend healthcheck: curl -fsS /api/health (curl is already in the
  backend image), 5s interval, 15s start_period
- frontend depends_on backend with condition: service_healthy, so the
  frontend waits for a responsive backend instead of racing it

tests/test_readyz.py covers ready, S3-down, DB-down, and health-stays-
static; verified live in-process (real app, TestClient): 200 when both
up, 503 EndpointConnectionError when S3 unreachable, 503 when DB
connect fails. Both compose files pass `docker compose config -q`.

Closes #119

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
create_experiment accumulated every file fully in RAM (content += chunk)
and then retained all bytes again in the staged tuples, so a folder
upload of many large files (500 MB per-file cap) could OOM the worker.

- Stream each multipart body straight to its temp file in bounded 1 MB
  chunks; the size cap is enforced on a running counter and the content
  hash for dataset versioning is computed incrementally (sha256) while
  streaming — the full bytes are never materialized.
- Staged tuples are now (tmp_path, remote_path) only; the temp path is
  registered before streaming so the finally block also cleans up
  partially-written files on mid-stream failure.
- The S3 push now uses s3.upload_file from the temp file (streams from
  disk, off-loop) instead of put_object(Body=<whole bytes>).
- record_upload accepts precomputed content_hash/size_bytes so streaming
  callers don't need the full bytes; content-based callers unchanged.

Closes #94

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
HomePage.connectSSE and useNotebookSSE each opened their own EventSource
to the identical /api/sessions/{id}/stream endpoint, doubling server
fan-out and parsing every event twice. Add SSEStreamContext to expose
the single page-level stream via a publish/subscribe bus; the notebook
now subscribes instead of opening a second connection.

Closes #100

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
…wn on every token

renderChatItem was a plain function, so every agent_token/agent_message
setChatItems re-ran the whole list and re-parsed markdown for every prior
message. Extract a memo-wrapped ChatItemView keyed by item.id (only the
streaming bubble's item reference changes, so React bails out on the
rest) and hoist the assistant bubble's remark-plugins array to a module
constant so it doesn't defeat the memoization with a fresh array each
render.

Closes #98

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
)

generic_exception_handler only logged unhandled request exceptions and
never reported them to Sentry. Background agent work spawned via
asyncio.create_task in routers/sessions.py (send_message's
_run_followup) runs outside the request lifecycle, so FastAPI's
exception handler never saw those errors either — they were silently
swallowed into a "failed" session state with no logging or tracking.

Add errors.capture_exception(), a thin no-op-safe wrapper around
sentry_sdk.capture_exception (no-ops with no DSN and never lets a
Sentry-side failure mask the original error), call it from the generic
handler, and call it (plus add the missing logger.exception) from the
background-task except block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
…aming

bottomRef.scrollIntoView({behavior:'smooth'}) fired on every chatItems
change, including every agent_token during streaming, yanking the view
back to the bottom and making it impossible to read earlier output.
Track whether the user is pinned near the bottom via a scroll listener +
threshold and only auto-scroll while pinned; use behavior:'auto' while a
bubble is actively streaming, and re-pin when the user sends a message.

Closes #99

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
…grations) (#121)

Scaffolds backend/alembic/ wired to the app's async engine (settings.database_url)
and Base.metadata, with a single autogenerated initial revision reflecting today's
full schema. init_db() now runs `alembic upgrade head` on boot (in a worker thread,
since env.py drives its own asyncio.run()); a DB that already has the full schema
but no alembic_version table gets `stamp head` instead, so existing deployments
don't get destructive re-creation attempts. _run_migrations is kept in place but
unused, documented as dead code for rollback reference.

Verified schema-equivalence of `alembic upgrade head` (fresh DB) vs
`create_all + _run_migrations` (legacy path) on both SQLite and Postgres: only
difference found is two redundant duplicate indexes the legacy code created
under typo'd names, no logical schema divergence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Adds focused unit tests for the two most critical, near-untested modules
in the agent runtime, with the Modal boundary faked throughout:

- test_sandbox.py: run_code's success/timeout/exception paths against a
  faked modal.Sandbox (create/stdout/stderr/wait) — asserts span
  attributes, metric/log-event dispatch routing, and usage recording per
  path.
- test_runner.py: run_agent's reachable orchestration state machine
  (spawn -> run -> complete/error/cancel) and its interplay with the real
  task registry (register_task/abort_agent/is_agent_running/
  cleanup_session), using real asyncio Tasks for the registry tests so
  cancellation semantics are genuine. _drive_provider is faked — its
  internals are already covered by test_drive_provider.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
… union

ChatItem.meta, generatedFiles/restoredFiles, the SSE handler's event.data,
and MetricsTab's ChartTooltip were all any-typed, defeating the type
system exactly where malformed backend payloads are most likely.

- lib/types.ts: define SSEEvent as a discriminated union over every
  event.type the backend publishes, replacing the old unused/inaccurate
  per-event Data interfaces with ones matching what the handler actually
  reads (and defends against).
- page.tsx connectSSE: narrow on event.type per case (each case gets its
  own block scope) instead of one blanket `event.data as any`; type
  ChatItem.meta as a flat, all-optional ChatItemMeta (a full discriminated
  union would require threading narrowed prop types through ~8 render
  components that read meta without first narrowing on item.type — out of
  scope for this change); type generatedFiles/restoredFiles as
  GeneratedFile[]; add metaStr/metaNum runtime-checked readers for
  Message.metadata on session-reload restore.
- MetricsTab.tsx: give ChartTooltip a real props interface instead of any.

Runtime behavior is unchanged — this is purely a typing refactor.

Closes #101

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
frontend/package.json had no test runner and no test script at all —
non-trivial client logic (the SSE dispatcher, the fetch wrapper) shipped
with zero coverage, so a single renamed event `type` or route path could
silently break live updates with nothing to catch it.

Adds Vitest + jsdom + @testing-library/react as devDependencies, a
`test` script (`vitest run`), and a `frontend-test` CI job that runs
`npm ci && npm test`.

Two real test suites (20 cases total):
  - src/lib/notebook/useNotebookSSE.test.ts: event routing (fires the
    right handler per `notebook.*` type), per-notebook filtering of
    cell-lifecycle events, kernel/structure events bypassing the
    filter, malformed-payload tolerance, EventSource lifecycle (opens
    scoped to sessionId, skips when disabled, closes on unmount), and
    that handler updates take effect without re-opening the stream.
  - src/lib/api.test.ts: fetchJSON-backed helpers (GET/POST bodies,
    conditional query-string building, the not-ok -> Error path) and
    the raw-fetch FormData helpers (createExperiment, attachData's
    webkitRelativePath-vs-name fallback for folder uploads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
… the app

There was no app/error.tsx, app/global-error.tsx, or any ErrorBoundary in
src/ — with untyped SSE payloads and live-rendered agent-authored
markdown + self-contained HTML artifacts, one bad payload throwing in
render took down the entire SPA with a blank screen.

- src/app/error.tsx: route-level boundary (Next.js App Router convention)
  with a "Try again" / "Reload page" recovery screen.
- src/app/global-error.tsx: root-layout boundary with its own <html>/
  <body> and inline styles (no dependency on globals.css/Tailwind/app
  components, since those may be what failed to mount).
- src/components/ErrorBoundary.tsx: reusable class-component boundary
  (no hook equivalent exists for getDerivedStateFromError/componentDidCatch)
  with an optional custom fallback + reset callback.
- Wrapped the workspace canvas (WorkspaceSidebar, keyed by sessionId) and
  every markdown renderer (chat assistant bubbles, sub-agent result
  summaries, the report tab, and markdown file previews) so a crash is
  contained to that panel/bubble instead of the whole app.

Closes #102

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Comment thread frontend/src/lib/notebook/useNotebookSSE.test.ts Outdated
Comment thread frontend/src/lib/notebook/useNotebookSSE.test.ts Outdated
The backend /compare endpoint already aggregates metrics, feature
overlap, and cost totals across up to 8 sessions, but nothing in the
frontend consumed it. This adds the leaderboard UI:

- api.ts: typed api.compare(sessionIds) client fn; types.ts: Compare*
  types mirroring routers/compare.py (incl. the feature_overlap
  list-when-empty quirk)
- /compare page: sortable leaderboard table (per-metric latest values
  with best-value highlight, total cost, created-at), overlaid
  per-metric charts reusing the MetricsTab chart stack (palette,
  tooltip, formatters now exported), session legend toggles, and a
  feature-overlap panel (common + per-session extras)
- experiments page: checkbox column + "Compare selected (n/8)" header
  action that navigates to /compare with the selected session ids
  (project-scoped when the selection stays within one project)

Closes #105

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
…Greptile P1 on #87)

Label inference (labels=None) iterated both sequences, so the sklearn and
fallback paths received exhausted iterators and silently produced an
all-zero matrix. Materialize to lists once up front; +2 regression tests
(generator inputs, with and without explicit labels).
A throwing listener in `publish` ran inside connectSSE's try/catch before
the UI switch, silently swallowing the event and killing all UI updates for
that message. Wrap each listener invocation in its own try/catch.
Conflicts: frontend/package-lock.json (lockfile churn — took staging's,
regenerated with `npm install` against the merged package.json).
page.tsx auto-merged: #148 SSE bus + #169 sample-dataset wiring +
#87 download UI all preserved.
Conflict in page.tsx: #152's handleChatScroll pin-tracking callback and
staging's #169 suggested-prompt effect appended at the same anchor — kept
both.
Conflicts/semantic reconciliation:
- MetricsTab.tsx: #157 typed ChartTooltip props (dropped `any`); staging's
  #161 compare page imports it — kept the typed props AND the export.
- SSE union extended with events other staging PRs added/consume:
  `budget_exceeded` (#165, new BudgetExceededSSEData) and the
  `notebook.kernel.state`/`notebook.structure.changed`/`notebook.cell.*`
  events dispatched by useNotebookSSE (#148's typed bus requires them).
  StructureChangedEvent moved to lib/notebook/types.ts (re-exported from
  useNotebookSSE) so lib/types.ts can reference it.
- page.tsx budget_exceeded case adapted to #157's per-case
  `const data = event.data` block pattern.
- useNotebookSSE.test.tsx: cell.completed payload gained exec_count: null
  to satisfy the now-typed CellCompletedEvent.
…ptile P1 on #159)

FileViewer is memoized and keeps its component instance across file
switches, so a markdown render crash left the boundary stuck in its error
fallback for every subsequent file. key={filePath} remounts the boundary
per file.
Conflict in page.tsx FileViewer markdown block: kept #159's ErrorBoundary
wrapper (with key={filePath} P1 fix) AND staging's api.filesRawUrl helper
(#133). Also prettier --write on global-error.tsx/ErrorBoundary.tsx
(branch predates the CI format gate).
Reconciliation with the #148 SSE-bus chain:
- DROPPED useNotebookSSE.test.ts (stale — it tested the hook's own
  EventSource, which #148 replaced with the SSEStreamContext bus; #148's
  bus-based useNotebookSSE.test.tsx is the live suite).
- api.test.ts add/add: combined both suites — #158's fetch-mocked
  fetchJSON/FormData helper tests AND staging's (#133) relative-URL builder
  tests — 15 tests total.
- vitest.config.ts add/add: kept one config — staging's (#148) plus #158's
  globals + setupFiles (vitest.setup.ts / @testing-library/jest-dom).
- package.json: union of devDeps (@testing-library/dom + jest-dom), single
  test script; package-lock.json: took staging's, regenerated via npm install.
- ci.yml: #158's frontend-test job kept as-is (staging had none — #133 only
  wired the npm script); all 10 jobs intact, YAML valid.
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Too many files changed for review. (122 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@lucastononro

Copy link
Copy Markdown
Owner Author

Merged into integration branch staging-v0.0.5 (see the STAGING-v0.0.5.md ledger on that branch for merge order, Greptile follow-ups and test results). These changes will land on main via the staging merge — closing to clear the queue.

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.

2 participants