Skip to content

UN-4329 [FEAT] Add the v1 unit-test harness and the frontend-v1-unit rig group - #2429

Merged
Deepak-Kesavan merged 12 commits into
mainfrom
UN-4329-v1-unit-harness
Nov 5, 2026
Merged

Deepak-Kesavan merged 12 commits into
mainfrom
UN-4329-v1-unit-harness

Conversation

@Deepak-Kesavan

@Deepak-Kesavan Deepak-Kesavan commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Adds the v1 frontend unit-test harness (P0-T08). frontend/vitest.v1.config.mts runs src/v1 minus plugins/, and frontend/vitest.v1-plugins.config.mts runs cloud v1 plugins. Each config has its own setup, its own coverage report (coverage/v1, coverage/v1-plugins) and its own threshold of 80% on lines, statements, functions and branches.
  • frontend/vitest.config.mjs (v0) now excludes src/v1/**. Nothing else in it changed.
  • src/v1/test/ (imported as @v1/test), documented in src/v1/test/README.md:
    • renderV1 / renderHookV1 over a single provider chain in providers.tsx: a fresh QueryClient with retries off, MemoryRouter with route/path, and the real app/theme/ThemeProvider from UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 plus #v1-portal. The helpers return user, queryClient, portal and theme. There are no session or distribution options yet: no v1 code reads either, so such an option would let a role or distribution test pass without reaching its branch. renderV1 throws if one is passed, and each returns with the slot that reads it (README table). TestSession / makeSession stay as fixture data for apiPath.
    • Network mocking with MSW at the HTTP boundary. An unmocked request fails the test and names it, even when the code catches the error. That covers fetch, XHR (so axios) and WebSocket. Nothing leaves the process. Typed fixtures per service module and apiPath(session, …) are described in the README.
    • Accessibility: await expect(el).toHaveNoA11yViolations() / expectNoA11yViolations on axe-core.
    • Fakes for browser APIs happy-dom doesn't drive: installResizeObserver, installIntersectionObserver and mockMatchMedia.
  • Rig: an optional vitest_config field on a group, emitted as --config. It is validated to apply to the vitest runner only and to point at a file inside the workdir. A new frontend-v1-unit group uses it.
  • Rig: a vitest group that runs no tests now reports ⚪ empty instead of ❌ "Errors 1". vitest's zero-test junit is <testsuites tests="0"> with no <testsuite>, which parse_junit read as a truncated write. That is what made an empty frontend-v1-unit-cloud go red wherever Node exists.
  • test-fixtures/v1-harness/ runs real vitest processes on known-bad cases, to prove the harness fails when it should.

Why

  • UN-4329 / P0-T08 (epic UN-4257). v1 unit tests must run and report on their own, never mixed with v0. This blocks P0-T10 and P1-T01/02/03/04/05/10/13/15/16/17/21/22.
  • Before this PR, the frontend group (vitest run src) also ran anything under src/v1, on v0's config.

How

  • Separate config, not Vitest projects. In projects mode coverage is root-level, so there's no per-project report directory or threshold. The rig's vitest run <paths> would also run both projects in the frontend group. Hence the small rig field.
  • The v1 config isn't built from v0's. v0's optionalPluginImports() stubs any missing import under a /plugins/ path. Inherited, it made a missing v1 module import as {}, which UN-4286's missing-import test (stacked into UN-4290 [FEAT] Move the v1 menus onto design tokens #2402) caught. v1 declares only react + svgr and only the @v1 alias, so @/ fails to resolve here just as it fails in typecheck:v1.
  • The unmocked-request guard uses catch-all handlers, not onUnhandledRequest: "error". Under happy-dom that strategy doesn't stop an HTTP request. happy-dom's fetch falls through to Node's http, MSW intercepts it a second time, and then the request goes out for real (I saw ECONNREFUSED :3000). For a WebSocket it only logs. The catch-alls answer with a network error, or close the socket (1011), and record the request. setup.ts waits for pending interceptors after cleanup(), so a request started by an unmount effect fails the test that unmounted it. The afterAll check catches requests made after a file's last test. resetHandlers(...args) keeps the catch-alls last.
  • Swapping in the real providers is a one-file edit to src/v1/test/providers.tsx (the table is in the README). UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 merged while this PR was open, and the theme slot is already swapped (a2ccc55). prepareProviders stores the theme for ThemeProvider to hydrate and creates #v1-portal through lib/portal. resetProviders clears the portal, the storage key and the store after each test. The query slot (P0-T05) and the router slot (P0-T03) are still stand-ins.
  • The boundary fixtures v1-allowed-imports / v1-non-test-imports-test overlaid a toy test/render.ts onto the copied scaffold. That file now shadows the real render.tsx, so I renamed it to test/case-helper.ts. The cases still assert the same rules.
  • tests/rig/tests/conftest.py gains UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355's autouse _no_inherited_extra_manifests fixture, byte-identical to 7f2ce99, so whichever PR merges second merges cleanly.

Acceptance

Spec line Proof
The rig lists the new groups python -m tests.rig list-groups → frontend-v1-unit tier=unit runner=vitest. With the cloud overlay manifest → also frontend-v1-unit-cloud.
frontend count unchanged v0 tests are the same on main and on this branch: this branch adds or removes no v0 test file. Against main at a194073 (after #2399, #2404 and #2405), main's frontend group runs 882 tests: 664 v0 plus 218 v1 that v0's config also picks up. On this branch (CI report, 2026-11-05) frontend runs 664 and frontend-v1-unit runs 249 (those 218 plus 31 harness self-tests). Only v1 tests moved. (When this PR opened, the same check read 654 v0 tests; main has since added 10.)
A failing v1 test turns only the v1 group red frontend-v1-unit ❌ 23 passed / 2 failed, frontend ✅ 654 / 0.
…and the reverse a failing v0 test: frontend ❌ 654 / 1, frontend-v1-unit ✅ 23 / 0.
A cloud plugin failure stays in the cloud group frontend-v1-unit-cloud ❌ 1 / 1, frontend-v1-unit ✅ 27 / 0, frontend ✅ 654 / 0 (run before the #2399 merge). After the merge: frontend-v1-unit ✅ 68 / 0, frontend ✅ 654 / 0.
An unmocked call fails, naming the URL rig junit: Error: Unmocked request (add a handler with mockApi() or server.use()):
GET http://localhost:3000/api/v1/unstract/org-1/adapter/?adapter_type=LLM
Existing rig groups unaffected all 26 existing groups' resolved commands (cwd + argv) are byte-identical before and after this PR.

Can this PR break any existing features? If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No runtime change. Test tooling only, plus one new runtime dependency (@tanstack/react-query) that nothing imports yet outside src/v1/test.
  • v0 tests: the only v0 config change is exclude: src/v1/**, and the count is unchanged (654).
  • Rig: groups without vitest_config produce exactly the commands they did before (checked for all 26).

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • frontend/src/v1/test/README.md (harness, provider swap guide, network rules, coverage ratchet)
  • frontend/src/v1/README.md (scripts table)
  • Spec: P0-T08 (Confluence 1637187606)

Related Issues or PRs

Dependencies Versions

All exact pins, each at least 2 weeks old:

  • msw 2.15.0 (2026-07-08), dev. Not 3.x: vitest 3.2.6's @vitest/mocker declares an msw ^2.4.9 peer.
  • axe-core 4.14.0 (2026-10-05), dev. Used directly with a ~40-line matcher. Not vitest-axe (last released 2022) or jest-axe (pulls in jest packages).
  • @vitest/coverage-v8 3.2.6 (2026-06-01), dev. Its peer is exactly vitest 3.2.6, which matches the lock.
  • @tanstack/react-query 5.104.5 (2026-10-19), runtime (see above).
  • bun.lock was regenerated with bun 1.4.4. Local bun 1.2.17 dropped configVersion, so I avoided it. The other lock changes are hoisting only.

Notes on Testing

  • bun run test:v1 (after merging main with UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399, UN-4502 [FEAT] Add the v1 PDF loader, document hook and viewer states #2404 and UN-4503 [FEAT] Add the v1 DocumentViewer for PDFs, images and text, with highlights, toolbar and zoom #2405): 249 vitest tests pass, with v1 coverage at 97.0% statements, 93.3% branches and 92.7% functions. 98 fixture tests also pass (89 boundary + 9 harness: unmocked fetch, XHR, WebSocket, a request on unmount and which test it fails, a request after the last test, resetHandlers with arguments, a mocked fetch, fake timers left on).
  • bun run typecheck:v1, bun run lint:v1 and bun run typecheck are clean.
  • Rig self-tests: all pass, both plain and with UNSTRACT_RIG_EXTRA_MANIFESTS pointing at cloud's groups.cloud.yaml plus the new group. 10 new tests cover the field and the frontend separation.
  • Self-review (review-pr own mode, 4 lenses, every finding adversarially verified) found 5 issues: an unmocked WebSocket passed silently, unmount-effect requests were misattributed or lost, requests after the last test went unreported, resetHandlers(args) dropped the guard, and overlapping axe runs threw. All five are fixed in d88eeb2, each with a known-bad fixture case.
  • Reviewer (ritwik-g): session/distribution options reached no provider. They are dropped and rejected until a slot reads them (482068f). The zero-test junit read as an error; fixed in 6c6897e. An empty plugins run through the rig now reports ⚪ frontend-v1-unit-cloud 0/0/0, rig exit 0.
  • Sonar's 2 new issues are fixed in 6d6df2c.
  • Greptile (3 findings) and Sonar (1) are fixed in c8d8d10, and every thread is answered and resolved. The WebSocket catch-all now steps aside when a test's own ws.link() matches. Observer fakes re-register after disconnect(). @v1/test exports ws. The fixture runner uses process.execPath instead of npx from PATH.
  • Not run: the cloud self-hosted runner has no npx, so cloud's frontend-v1-unit-cloud skips there (exit 5), the same as OSS frontend does today.

Screenshots

None: no UI.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

…rig group

- vitest.v1.config.mts runs src/v1 (minus plugins/) with its own setup,
  coverage report (coverage/v1) and 80% threshold; vitest.v1-plugins.config.mts
  runs cloud v1 plugins. vitest.config.mjs now excludes src/v1.
- src/v1/test: renderV1/renderHookV1 over one pluggable provider chain
  (QueryClient, MemoryRouter, v1 theme root + #v1-portal), typed session and
  distribution, MSW at the HTTP boundary with a catch-all that fails the test
  naming every unmocked request, axe-core assertions, observer/matchMedia fakes.
- Rig: optional vitest_config on a group (vitest runner only, must exist inside
  workdir), passed as --config; existing groups' commands are unchanged.
- test-fixtures/v1-harness proves an unmocked request fails a real vitest run.
- Deps (exact): msw 2.15.0, axe-core 4.14.0, @vitest/coverage-v8 3.2.6,
  @tanstack/react-query 5.104.5.
… the harness

v0's optionalPluginImports() stubbed a missing import under any /plugins/
path, so a missing v1 module imported as {} instead of failing (UN-4286's
missing-import test caught it). The v1 config now declares its own plugins
(react, svgr) and only the @v1 alias.
…-review

- WebSocket: a catch-all ws.link closes unmocked sockets (1011) and records
  the URL; before, MSW opened a mocked socket and the test passed.
- Requests started in cleanup() (unmount effects) now fail the test that
  unmounted: afterEach awaits pending interceptors before checking.
- Requests after a file's last test fail the file from afterAll.
- server.resetHandlers(...args) keeps the catch-alls last.
- Overlapping axe runs are queued instead of throwing 'Axe is already running'.
- A test leaving fake timers on gets real timers back before teardown.
Each gap has a known-bad case in test-fixtures/v1-harness.
@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no blocking issues remain.

Summary

Adds a separate v1 test harness and the frontend-v1-unit rig group.

  • V1 tests run separately from the v0 frontend suite.
  • V1 component tests get shared rendering and browser test helpers.
  • Vitest groups with no tests are reported as empty.

All three earlier findings are fixed: mocked sockets bypass the catch-all, disconnected observers can reconnect, and @v1/test exports ws. No new actionable issues were found.

Reviews (7) · Last reviewed commit: "UN-4329 [FIX] Measure v1 coverage on who..." · Reviewed by Greptile

Comment thread frontend/src/v1/test/network.ts Resolved
Comment thread frontend/src/v1/test/dom.ts Resolved
Comment thread frontend/src/v1/test/index.ts Resolved
…, export ws

- The WebSocket catch-all steps aside when a test's own ws.link() matches the
  URL: MSW runs every matching ws handler, so it closed mocked sockets too.
- Observer fakes re-register on observe() after disconnect().
- @v1/test re-exports ws.
- The harness fixtures run the lockfile's vitest via process.execPath instead
  of resolving npx from PATH.
# Conflicts:
#	frontend/bun.lock
#	frontend/package.json
#	frontend/src/v1/README.md
…2399 landed

The theme slot in src/v1/test/providers.tsx swaps its stand-in for
app/theme/ThemeProvider, the one-file change the harness was built for.
prepareProviders stores the requested theme for it to hydrate and creates
#v1-portal with lib/portal's getPortalContainer; resetProviders clears the
portal, the theme storage key and the store after each test. PORTAL_CONTAINER_ID,
V1_ROOT_CLASS and ThemeMode now come from lib/.
- setup.ts: settle() awaits two macrotask turns without an await-in-loop.
- test_groups.py: build the manifest outside pytest.raises so only
  load_groups() can raise.

@ritwik-g ritwik-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One must-fix before merge (inline on render.tsx): session and distribution are accepted but reach no provider.

Comment thread frontend/src/v1/test/render.tsx Resolved
…a provider reads them

No ProviderSlot and no v1 code outside test/ consumed context.session or
context.distribution, so session: null, makeSession({ role: "user" }) and
distribution: "onprem" rendered the same tree as the defaults, and a test of
a role or distribution branch passed without reaching it. A test-only context
would not fix that: v1 components may not import test/.

renderV1/renderHookV1 no longer take either (type error), and throw if one is
passed anyway. TestSession/makeSession stay as fixture data for apiPath. Each
option returns with the slot that reads it (README table: session store;
plugin registry, UN-4285/UN-4286). New test: each accepted option (theme)
changes what renders.
… truncated junit

vitest under passWithNoTests writes <testsuites tests="0"> with no
<testsuite> child. parse_junit found no counters and reported Errors 1, so an
empty frontend-v1-unit-cloud (no cloud v1 plugin yet) went red wherever Node
exists. parse_junit now reads a childless <testsuites> root's counters, and a
vitest group that exits 0 having run nothing is recorded as exit 5, the rig's
"nothing collected" code, so it reads empty rather than a green row that
verified nothing. Exit 5 never fails a non-pytest group.

@ritwik-g ritwik-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-checked at 6c6897e.

  • session/distribution are gone from renderV1 (type error plus a runtime throw), and the README says where each comes back.
  • An empty vitest group now reads ⚪, not ❌. In a merged OSS+cloud tree, the empty cloud group is ⚪ and isolation holds both ways.
  • frontend is still exactly main's v0 set (664).

Approving. One request before lane J starts, here or as a quick follow-up: focused runs still exit 1 on the global coverage threshold (npx vitest run --config vitest.v1.config.mts src/v1/lib/cn.test.ts passes 4/4, then fails at 3.34% lines). Suggest coverage.enabled: !!process.env.CI, or --coverage from test:v1 and the rig.

The coverage threshold is global, so a focused run (one test file) failed
it whatever its tests did: vitest run --config vitest.v1.config.mts
src/v1/lib/cn.test.ts passed 4/4, then exited 1 at 3.34% lines. Coverage
is now on when CI is set (the rig and every workflow) and in bun run
test:v1 (--coverage); a focused local run skips it. Requested by Ritwik in
the approving review.
@Deepak-Kesavan

Copy link
Copy Markdown
Contributor Author

Thanks, @ritwik-g. The focused-run coverage request is done here, in 0ef3db4, so lane J doesn't hit it.

  • coverage.enabled: Boolean(process.env.CI) in vitest.v1.config.mts, which the plugins config shares. bun run test:v1 passes --coverage. So the threshold is measured on whole-suite runs only: CI (the rig and every workflow set CI) and test:v1.
  • Your example: npx vitest run --config vitest.v1.config.mts src/v1/lib/cn.test.ts now passes 4/4 and exits 0 locally. With CI=true it still exits 1 at 3.34% lines. env -u CI bun run test:v1 still measures: 249 passed, 96.99% statements, threshold enforced.
  • The README coverage section says the same, including --coverage to measure a focused run on purpose.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

✅ All checks passed! No linting or formatting issues found.

@sonarqubecloud

sonarqubecloud Bot commented Nov 5, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
⚪ e2e-agent-kv e2e 0 0 0 1 0.1
✅ e2e-api-deployment e2e 3 0 0 0 16.4
✅ e2e-coowners e2e 1 0 0 0 1.4
✅ e2e-etl e2e 1 0 0 0 12.6
✅ e2e-login e2e 2 0 0 0 1.2
✅ e2e-prompt-studio e2e 1 0 0 0 12.9
✅ e2e-smoke e2e 2 0 0 0 1.2
✅ e2e-workflow e2e 1 0 0 0 16.2
✅ frontend unit 664 0 0 0 12.0
✅ frontend-v1-unit unit 249 0 0 0 3.7
✅ integration-backend integration 1557 0 0 26 81.6
✅ integration-connectors integration 1 0 0 7 9.0
✅ integration-workers integration 181 0 0 1 53.3
❌ ui e2e 0 1 0 0 0.0
✅ unit-agent-kv-schema unit 143 0 0 0 1.0
✅ unit-backend unit 2159 0 0 1 39.1
✅ unit-connectors unit 82 0 0 0 7.1
✅ unit-core unit 281 0 0 0 1.8
✅ unit-filesystem unit 1 0 0 0 2.6
✅ unit-platform-service unit 22 0 0 0 1.7
✅ unit-rig unit 139 0 0 0 3.2
✅ unit-runner unit 12 0 0 0 2.4
✅ unit-sdk1 unit 1356 0 0 0 30.6
✅ unit-tool-sidecar unit 8 0 0 0 1.2
✅ unit-workers unit 1668 0 0 2 104.7
TOTAL 8534 1 0 38 417.0

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • platform-key-whoami — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@Deepak-Kesavan
Deepak-Kesavan merged commit e45284f into main Nov 5, 2026
20 checks passed
@Deepak-Kesavan
Deepak-Kesavan deleted the UN-4329-v1-unit-harness branch November 5, 2026 06:33
johnyrahul pushed a commit that referenced this pull request Nov 5, 2026
…ts own configs

Resolves the package.json and v1 README conflicts with #2429. The v1 unit
check now runs vitest.v1.config.mts and vitest.v1-plugins.config.mts with
coverage, and the guard-fixtures check also runs the v1-harness fixtures
(UN-4612).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HzRGGUgsEDiwJ56zKJ4ASv
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.

3 participants