Repository navigation
finding(test-infra): importOriginal() on the source-aliased @object-ui/app-shell barrel costs ~10s per console test file — measured once, and up to 12 files are in the same shape #6580
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoand removed
on Aug 26, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsTriage (first-touch grading, triage seat session
session_01HUGmbPVkYs22mNKmsDL6KT): promotedfinding→pm:queue,domain:devx, type Task.Routing rationale (three-stream split for objectui cards): the fix lands in console test engineering — the
apps/console/vite.config.tssource alias, the app-shell barrel's side-effect imports as seen by the test module graph, and/or the mock-factoryimportOriginalpattern — not in published-library or app product code, so this is the engineering face (domain:devx), notdomain:ui. Note the devx@objectui seat is currently vacant (#10602); the card waits in queue — that is a staffing gap, not a routing input.Dispatch shape (for whoever picks it up): first turn the 12-file upper bound into a measured count (per-file read of which mock factory the
importOriginalsits in), then pick the cheapest lever that removes the ~10s per-file transform from bounded windows — the card's own three levers (alias to built output for tests, trim the barrel's 8 side-effect imports from the graph, or dropimportOriginalfor this package in factories) are engineering judgment within scope; ⛔ raising timeouts / heavyDomTests / skip-quarantine are explicitly not on the table, per the card and per gate-weakening rules. If measurement shows the real population is 1–2 files, report back — the card may then be below the line and worth closing rather than engineering around.Size/model suggestion: M (measurement + one infra lever, judgment sits in design not gates).
Generated by Claude Code
Claim:
- Session: session_01BGMDbrVa8JjZcCQ7DWYH1b (execution seat,
domain:devx@ objectui, seat post [PM seat] domain:devx @ objectui — ⏳ vacant #5748, round R40) - Branch: claude/issue-6580-app-shell-import-original
- Worktree: /home/user/objectui-6580 (dedicated per-task worktree, off
origin/main) - Domain:
domain:devx(console test configuration; triage 2026-08-26 promoted finding →pm:queue, type Task, Size/model suggestion: M) - File surface (measurement first, then at most one lever): read-only pass over the 12 candidate console test files listed in the card body (to attribute each
importOriginalto the mocked module it sits in); then exactly one of —apps/console/vite.config.ts(the@object-ui/app-shelltest alias),packages/app-shell/src/index.ts(the barrel's side-effect imports), or the affected files'vi.mock('@object-ui/app-shell', …)factories. Which lever is chosen follows from the measured count, per the triage comment. - Container & model: subagent
os-dev, opus, background. - Clause ②: not applicable (no
packages/speccontract surface; no published-package public API changes expected — if the barrel lever is chosen and it changes the export set of@object-ui/app-shell, the dev stops and reports instead of landing it). - Serial constraints cleared: finding(test-infra): the ECONNREFUSED 127.0.0.1:3000 class is closed four times over yet still prints in 12 of 16 GREEN full-suite shards — and it reads as a red gate whenever a run is cut short #6640 (PR test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide #7304) touches root
vitest.setup.*only; finding(ci): a claim about a package's BUILT artifact cannot be pinned — turbo'stesttask builds dependencies only, so adist-importing test is NOT MEASURED in CI #7183 (PR test(ci): adistvitest project so built-artifact claims can be pinned #7291) touches rootvitest.config.mts+apps/console/vitest.config.ts(notvite.config.ts); finding(test-config): theunitproject'sisolate: falseis justified by a comment that is false — registry-writing AND registry-absence tests both live there #7134 touches the unit isolate script; tier 2 of #5043: compile the README code blocks (+ bidirectional pins for documentedinterfaceblocks) — needs a baseline decision before it can be a gate #6214 touchesscripts/check-readme-exports.*; check-doc-snippet-types.mjs collects only.mdxunder content/docs — 40.mdguides are uncovered with no UNGATED_DOCS entry, contradicting its own "covered by default" rule #5174 batch 6 touchescontent/docs/**. None overlaps the surface above.vi.mockfactory edits are confined to the candidate console test files, which no in-flight card touches.
Rulings carried into the dispatch order (from the triage comment, verbatim in intent): raising timeouts, adding files to
heavyDomTests, and skip/quarantine are explicitly not on the table. If measurement shows the real population is 1–2 files, the dev reports back with the measurement and no lever — the card may then be below the line.
Generated by Claude Code
- Session: session_01BGMDbrVa8JjZcCQ7DWYH1b (execution seat,
ACCEPT — PR #7315 (head
3c8111cf4), reviewed against GitHub by thedomain:devxseat (session_01BGMDbrVa8JjZcCQ7DWYH1b, round R40).Checked:
- Measurement first, as the triage comment required: per-factory attribution puts the real population at N = 3 (
FormPage.predicateScope,SetupRoute,StudioRoute), the other nine candidates'importOriginalbelonging toapprovalsApior@object-ui/i18n; ripgrep finds 22 mocking files against the code-search's 21. Cost re-measured on this tree at 10019 ms for the barrel vs 237 ms forauth, reproducing the card. Two structural figures corrected (barrel 447 lines / 13 side-effect imports). - Exactly one lever, and the cheapest: (c), confined to the three test files. (a) ruled out by measurement (CI's test job runs no build,
distabsent at test time). (b) stopped per the dispatch ruling (the 13 side-effect imports are the published barrel's registration behaviour). - The replacement spreads the submodules the needed names live in, with a static coverage proof over each file's full transitive module graph (0 uncovered names per file) rather than the paths the tests happen to run; the
SetupRouteuseMetadata-through-the-barrel subtlety is spelled out in the factory rather than by a new cross-package edge. - Before/after on the same three files from the repo root: import phase 13.07 s → 2.64 s, 37/37 tests both sides. Console type-check exit 0 with all three files confirmed in the program; eslint, control-bytes, changeset gates quoted with exit codes. Empty-frontmatter changeset present (harmless; nothing published changes).
- Nothing in
apps/console/vitest.config.ts, rootvitest.config.mtsorvitest.setup.*touched; no timeout, noheavyDomTests, no skip.
The PR flips out of draft and enters the queue once CI on
3c8111cf4is green. Card closes on merge viaFixes #6580. Follow-up not opened here: the remaining ~7.8 s of barrel graph is a property of the published package's side-effect imports, which the ruling kept out of this card; anyone who wants it measured files it as a finding againstpackages/app-shell.
Generated by Claude Code
- Measurement first, as the triage comment required: per-factory attribution puts the real population at N = 3 (
os-dev-report
{ "issue": 6580, "status": "done", "verdict": "lever-applied", "branch": "claude/issue-6580-app-shell-import-original", "pr": "https://github.com/objectstack-ai/objectui/pull/7315", "head_sha": "3c8111cf46be6e3028bce4a61cc79cb6bff8305f", "premise_still_valid": true, "files_changed": [ "apps/console/src/components/FormPage.predicateScope.test.tsx", "apps/console/src/components/SetupRoute.test.tsx", "apps/console/src/components/StudioRoute.test.tsx", ".changeset/six-donkeys-shake.md" ], "measurement": { "files_mocking_app_shell_ripgrep": 22, "card_code_search_figure": 21, "N_app_shell_factory_calls_importOriginal": 3, "affected_files": [ "apps/console/src/components/FormPage.predicateScope.test.tsx", "apps/console/src/components/SetupRoute.test.tsx", "apps/console/src/components/StudioRoute.test.tsx" ], "attribution_of_the_other_19": "9 files carry importOriginal for ../../services/approvalsApi (the 7 ApprovalsInboxPage files) or @object-ui/i18n (the 3 AppManagementPage files); the remaining files contain no importOriginal at all. Two of the card's 12 candidates plus one file the card did not list resolve this way, which is exactly the caveat the card stated.", "cost_reproduced_on_this_tree": "PROBE6580 app-shell=10019ms auth=237ms (performance.now around the call, throwaway probe file, vi.hoisted holder, deleted before commit). Card reported 10204ms / 240ms.", "card_structural_numbers_drifted": "barrel is 447 lines with 13 bare side-effect imports today, not 385 and 8" }, "lever": { "chosen": "(c) drop importOriginal for this package in the mock factories", "why": "Each factory already overrides most of what its graph reads from the barrel and needs only a handful of real exports. Timed individually the needed submodules cost about 2.0s together (chrome/index 549ms, console/ConsoleShell 1477ms, ExpressionProvider 0ms, expressionUser 4ms, utils/index 0ms, MetadataProvider 0ms); the barrel timed AFTER all six were already loaded still cost 7828ms. So roughly 7.8s of the 10s is graph these three files never touch. Confined to test files, no published surface.", "a_rejected": "Not viable. ci.yml runs pnpm install --frozen-lockfile then pnpm test with no build step, and packages/app-shell/dist does not exist at test time. The vite alias table also points every @object-ui entry at source, so this would be one inconsistent entry.", "b_rejected": "Stopped per the PM ruling. The 13 side-effect imports are registration side effects of the published package, so trimming or deferring them changes what a consumer gets from importing the barrel. Making only the test alias avoid them needs a second entry point, which is lever (a) in disguise. Not landed.", "levers_combined": false }, "summary": "Measured first: the 12-file figure was a path-intersection upper bound and the real population is 3, confirmed by attributing every importOriginal call to the vi.mock factory it sits in and then reading the three survivors. The cost reproduced independently at 10019ms against 237ms for @object-ui/auth in the same file. Applied exactly one lever, (c): the three @object-ui/app-shell factories now spread the app-shell submodules their real exports live in instead of calling importOriginal on the barrel. The covering set was derived statically over each test file's full transitive module graph (about 1237 modules once NodeNext .js specifiers resolve to TypeScript sources), not from the paths the tests happen to exercise, so no export can silently become undefined on an unexercised path: 0 uncovered names across all three files. Import phase for the three files went from 13.07s to 2.64s with the same 37 tests passing.", "tests": "All from the repo root of /home/user/objectui-6580, through bash /home/user/objectstack/scripts/pm/os-verify-lock.sh, exit codes captured by redirecting to a file before any pipe. BEFORE (baseline, unmodified tree): pnpm exec vitest run --project @object-ui/console --no-file-parallelism --reporter=verbose on the three files gave 'Test Files 3 passed (3)' / 'Tests 37 passed (37)', 'Duration 40.21s (transform 17.09s, setup 24.96s, import 13.07s, tests 560ms)', lock 'VERDICT command-exit 0'. AFTER (final tree, identical command and flags): 'Test Files 3 passed (3)' / 'Tests 37 passed (37)', 'Duration 30.52s (transform 11.27s, setup 25.64s, import 2.64s, tests 473ms)', lock 'VERDICT command-exit 0'. These are shared-box seconds (the lock excludes other locked runs, not unlocked sibling work), so the durable reading is the ratio: import phase down about 5x. TYPE-CHECK: pnpm --workspace-concurrency=4 --filter '@object-ui/console^...' run build exit 0 (the first attempt used --concurrency=4, which pnpm forwarded to tsup and which failed; the correct flag is --workspace-concurrency before --filter), then pnpm --filter @object-ui/console run type-check exit 0 with the script name echoed, and tsc --noEmit --listFiles exit 0 with 0 'error TS' and each of the three edited files present in the program exactly once, so 'type-check clean' provably covers them. ESLINT: pnpm exec eslint on the three changed files exit 0, one pre-existing no-explicit-any warning at line 188 of FormPage.predicateScope.test.tsx, outside every hunk in this diff (confirmed against git diff -U0 hunk headers 76, 345). GATES: pnpm check:control-bytes exit 0, 'scanned 6009 tracked text file(s)'; grep -naP for raw control bytes over the three changed files returned no hits; node scripts/check-changeset-presence.mjs exit 0, 'Every one of them has an EMPTY frontmatter - declared as releasing nothing, which is the explicit exemption'; pnpm changeset:check exit 0; pnpm lint:coverage exit 0 (46/46); pnpm type-check:coverage exit 0. STATIC COVERAGE PROOF: for each of the three files, every name any module in its transitive graph imports from @object-ui/app-shell is an override, a type-only barrel export, or an export of a spread submodule. UNCOVERED NAMES: 0. NOT RUN LOCALLY (declared narrowing): the full apps/console vitest project and the repo-wide eslint farm, both of which CI runs exactly once regardless.", "mcp_calls": "6", "deviations": [ "A changeset IS owed here even though the diff is test-only: check-changeset-presence.mjs counts apps/console/src/** as published source of @object-ui/console. Added .changeset/six-donkeys-shake.md with EMPTY frontmatter, which the gate itself names as the explicit 'releases nothing' exemption, and the gate then passes. This differs from the dispatch note's expectation that no changeset would be owed for test-only changes.", "The dispatch suggested --project console; the actual vitest project name is @object-ui/console (apps/console/package.json name), so that is what was used.", "No skip-changeset label applied: per the os-dev contract that label is inert in objectui and pinned against being set there, and a real changeset was added instead.", "Local verification was narrowed to the affected package and the gates the diff touches; the full console project and the repo-wide lint farm were left to CI. Declared, not skipped." ], "open_questions": [], "out_of_scope_findings": [ "NOT FILED (surface owned by in-flight PRs #7291 and #7304, and it is not a correctness bug on main): with the barrel out of the factories, setup is now the dominant cost of a console run - 25.64s of a 30.52s three-file run, about 8.5s per file, against 2.64s of import. apps/console/vitest.config.ts overrides setupFiles to the FULL vitest.setup.dom.tsx, whereas root vitest.config.mts deliberately split 'dom' (light setup) from 'dom-heavy' precisely so that about 290 DOM files that never touch the ComponentRegistry stop paying that graph. Every console test pays the heavy setup regardless of whether it renders through the registry. If that split is worth extending to the console project it is a larger, separate card.", "NOT FILED (measurement correction, belongs to this card): the card's structural figures have drifted since 2026-08-26 - packages/app-shell/src/index.ts is now 447 lines with 13 bare side-effect imports, not 385 and 8. The mechanism the card describes is intact and got slightly worse.", "NOT FILED (confirms a caveat the card already stated): ripgrep finds 22 console files mocking @object-ui/app-shell against the card's GitHub code-search figure of 21, so the index was lagging as the card warned. The extra file, ApprovalsInboxPage.characterizationPins.test.tsx, is unaffected - its importOriginal belongs to ../../services/approvalsApi." ] }
Generated by Claude Code
Surfaced while implementing #6567 (PR #6579). That card fixed one file; this is the property of the tree that produced it, which the fix deliberately did not touch.
The mechanism, measured
In
apps/console, the@object-ui/app-shellspecifier is aliased to source, not to built output:packages/app-shell/src/index.tsis 385 lines and carries 8 bare side-effect imports (:277,:279,:280,:281,:283,:286,:289,:292—builtinComponents,CloudConnectionPanel,InstalledListWidget,ConnectAgentWidget,CloudOnboardingNext,CloudAiModelStatus,record-attachments-renderer,record-approvals-renderer). All three of those numbers were re-measured onmainindependently of the PR that reports them.So a test file that mocks the package with a factory taking
importOriginal—— makes its first value request for that package transform that whole graph on demand. Instrumented on an idle machine in #6567:
importOriginal(app-shell)= 10204 ms of a 10223 ms total import, i.e. 99.8%. For comparison,authin the same file was 240 ms and the render plus assertions were ~12 ms.Why it is worth a card beyond the file that was fixed
In #6567 that 10.2 s happened to land inside a test case's
testTimeoutwindow, because the module was reached by anawait import()in the case body. The case then failed whenever the transform pipeline was saturated by a full-project run — a flake whose cause was neither of the two suspects the card named. #6579 fixed it by moving the load to module scope, i.e. out of the bounded window into the import phase that nothing bounds.That fix is correct and it is not what this card asks to revisit. The point is that the 10.2 s is still spent, on every file in this shape, and any file that reaches the package from inside a bounded window is a latent repeat of the same flake.
The suite-level numbers are consistent with this not being rare. Three green full-project
apps/consoleruns on the fixed tree reported setup times of 555 s, 721 s and 840 s against durations of 252 s, 323 s and 373 s.Population — an upper bound, stated as one
GitHub code search over this repo:
vi.mock('@object-ui/app-shell'— 21apps/consolecontainingimportOriginal— 16importOriginalsomewhere — but thatimportOriginalmay belong to a different mocked module (react-router-domand similar are mocked the same way). Confirming the real number needs a per-file read of which factory theimportOriginalsits in. A further 9 files mock the package with noimportOriginalanywhere in them and are therefore not affected by this mechanism.Second caveat: GitHub code search is an index and can lag or be incomplete, so these are lower bounds on the populations themselves.
Not a request to weaken anything
Explicitly not asking to raise a timeout, to add files to
heavyDomTests, or to skip or quarantine anything — those were all available in #6567 and all correctly declined. The levers that would actually change this are upstream of any gate: whether the test alias should resolve to built output rather than source, whether the barrel's 8 side-effect imports need to be in the module graph animportOriginal()pulls, or whether the mock factories should avoidimportOriginalfor this package. Choosing among those is a sizing question, not this card's to answer.Filing note
Filed unassigned and ungraded by the
domain:uiexecution seat (sessionsession_011SfZeFWrhGLHmfq61xbz4q). It lands in console test configuration rather than in a shipped package, so the routing label and the grade are triage's to produce. Duplicate check before filing found no open card of this shape.