Repository navigation
UN-4329 [FEAT] Add the v1 unit-test harness and the frontend-v1-unit rig group - #2429
Conversation
…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.
[Medium impact] The PR appears safe to merge; no blocking issues remain.
|
…, 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
left a comment
There was a problem hiding this comment.
One must-fix before merge (inline on render.tsx): session and distribution are accepted but reach no provider.
…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
left a comment
There was a problem hiding this comment.
Re-checked at 6c6897e.
session/distributionare gone fromrenderV1(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.
frontendis 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.
|
Thanks, @ritwik-g. The focused-run coverage request is done here, in 0ef3db4, so lane J doesn't hit it.
|
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
Unstract test resultsPer-group results
Critical paths
|
…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



What
frontend/vitest.v1.config.mtsrunssrc/v1minusplugins/, andfrontend/vitest.v1-plugins.config.mtsruns 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 excludessrc/v1/**. Nothing else in it changed.src/v1/test/(imported as@v1/test), documented insrc/v1/test/README.md:renderV1/renderHookV1over a single provider chain inproviders.tsx: a fresh QueryClient with retries off, MemoryRouter withroute/path, and the realapp/theme/ThemeProviderfrom UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 plus#v1-portal. The helpers returnuser,queryClient,portalandtheme. There are nosessionordistributionoptions yet: no v1 code reads either, so such an option would let a role or distribution test pass without reaching its branch.renderV1throws if one is passed, and each returns with the slot that reads it (README table).TestSession/makeSessionstay as fixture data forapiPath.apiPath(session, …)are described in the README.await expect(el).toHaveNoA11yViolations()/expectNoA11yViolationson axe-core.installResizeObserver,installIntersectionObserverandmockMatchMedia.vitest_configfield 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 newfrontend-v1-unitgroup uses it.<testsuites tests="0">with no<testsuite>, whichparse_junitread as a truncated write. That is what made an emptyfrontend-v1-unit-cloudgo 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
frontendgroup (vitest run src) also ran anything undersrc/v1, on v0's config.How
projects. In projects mode coverage is root-level, so there's no per-project report directory or threshold. The rig'svitest run <paths>would also run both projects in thefrontendgroup. Hence the small rig field.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@v1alias, so@/fails to resolve here just as it fails intypecheck:v1.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 sawECONNREFUSED :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.tswaits for pending interceptors aftercleanup(), 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.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).prepareProvidersstores the theme forThemeProviderto hydrate and creates#v1-portalthroughlib/portal.resetProvidersclears 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.v1-allowed-imports/v1-non-test-imports-testoverlaid a toytest/render.tsonto the copied scaffold. That file now shadows the realrender.tsx, so I renamed it totest/case-helper.ts. The cases still assert the same rules.tests/rig/tests/conftest.pygains UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355's autouse_no_inherited_extra_manifestsfixture, byte-identical to 7f2ce99, so whichever PR merges second merges cleanly.Acceptance
python -m tests.rig list-groups→frontend-v1-unit tier=unit runner=vitest. With the cloud overlay manifest → alsofrontend-v1-unit-cloud.frontendcount unchangedfrontendgroup runs 882 tests: 664 v0 plus 218 v1 that v0's config also picks up. On this branch (CI report, 2026-11-05)frontendruns 664 andfrontend-v1-unitruns 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.)frontend-v1-unit❌ 23 passed / 2 failed,frontend✅ 654 / 0.frontend❌ 654 / 1,frontend-v1-unit✅ 23 / 0.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.Error: Unmocked request (add a handler with mockApi() or server.use()):GET http://localhost:3000/api/v1/unstract/org-1/adapter/?adapter_type=LLMCan 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)
@tanstack/react-query) that nothing imports yet outsidesrc/v1/test.exclude: src/v1/**, and the count is unchanged (654).vitest_configproduce exactly the commands they did before (checked for all 26).Database Migrations
Env Config
Relevant Docs
frontend/src/v1/test/README.md(harness, provider swap guide, network rules, coverage ratchet)frontend/src/v1/README.md(scripts table)Related Issues or PRs
UN-4329 (P0-T08), epic UN-4257. Unblocked by UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380.
Cloud counterpart: Zipstack/unstract-cloud#1945 —
frontend-v1-unit-cloud+ thecopy_cloud_deps.pymapping. Merge this PR first: the cloud group needs the rig'svitest_config.Migration for the open v1 PRs. I checked each PR's v1 files on this branch: all pass, and all are well above the threshold.
FakeResizeObservers and theFakeIntersectionObserverwithinstallResizeObserver()/installIntersectionObserver()from@v1/test(use.resize(el, size)/.intersect(el, true)). Drop theafterEach(vi.unstubAllGlobals), since the config setsunstubGlobalsandrestoreMocks.ThemeProvider.test.tsx: replace the hand-rolledmatchMediaand listener list withmockMatchMedia()and.setDark(true)/.listenerCount(). DropafterEach(vi.unstubAllGlobals). Menu tests:renderV1()already renders inside the realThemeProviderand creates#v1-portalwithgetPortalContainer(), so drop any hand-built root or portal. The UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 commits in this stack are now on main.renderV1()returnsuser, which replacesrender()plususerEvent.setup()per test.vi.stubGlobal("CSS", …)is fine as-is (it's auto-unstubbed). Keep the local clipboard override.src/v1/test/providers.fixture.{ts,json}sits beside the harness without clashing. Optional:await expect(container).toHaveNoA11yViolations()onBrandMark.All four already import
it/expectfromvitest, soglobals: falseneeds no edits. jest-dom matchers come fromsrc/v1/test/setup.ts.For P0-T05 (lane D):
@tanstack/react-queryis pinned at5.104.5because the default provider needs a QueryClient. P0-T05 adopts or re-pins it.For P0-T02 (UN-4285): the cloud
copy_cloud_deps.pymapping only covers the test rig's merged tree, and P0-T02's overlay wiring may supersede it.Dependencies Versions
All exact pins, each at least 2 weeks old:
msw2.15.0 (2026-07-08), dev. Not 3.x: vitest 3.2.6's@vitest/mockerdeclares anmsw ^2.4.9peer.axe-core4.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-v83.2.6 (2026-06-01), dev. Its peer is exactly vitest 3.2.6, which matches the lock.@tanstack/react-query5.104.5 (2026-10-19), runtime (see above).bun.lockwas regenerated with bun 1.4.4. Local bun 1.2.17 droppedconfigVersion, 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,resetHandlerswith arguments, a mocked fetch, fake timers left on).bun run typecheck:v1,bun run lint:v1andbun run typecheckare clean.UNSTRACT_RIG_EXTRA_MANIFESTSpointing at cloud'sgroups.cloud.yamlplus the new group. 10 new tests cover the field and the frontend separation.resetHandlers(args)dropped the guard, and overlapping axe runs threw. All five are fixed in d88eeb2, each with a known-bad fixture case.⚪ frontend-v1-unit-cloud 0/0/0, rig exit 0.ws.link()matches. Observer fakes re-register afterdisconnect().@v1/testexportsws. The fixture runner usesprocess.execPathinstead ofnpxfrom PATH.frontend-v1-unit-cloudskips there (exit 5), the same as OSSfrontenddoes today.Screenshots
None: no UI.
Checklist
I have read and understood the Contribution Guidelines.
🤖 Generated with Claude Code