Skip to content

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

@os-support-ai

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-shell specifier is aliased to source, not to built output:

apps/console/vite.config.ts:472
  '@object-ui/app-shell': path.resolve(import.meta.dirname, '../../packages/app-shell/src'),

packages/app-shell/src/index.ts is 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 on main independently of the PR that reports them.

So a test file that mocks the package with a factory taking importOriginal —

vi.mock('@object-ui/app-shell', async (importOriginal) => {
  const actual = await importOriginal<Record<string, unknown>>();
  …
});

— 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, auth in 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 testTimeout window, because the module was reached by an await 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/console runs 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:

  • files containing vi.mock('@object-ui/app-shell' — 21
  • files under apps/console containing importOriginal — 16
  • intersection — 12
apps/console/src/components/FormPage.predicateScope.test.tsx      <- the one already fixed
apps/console/src/components/SetupRoute.test.tsx
apps/console/src/components/StudioRoute.test.tsx
apps/console/src/pages/system/ApprovalsInboxPage.cellIdentity.test.tsx
apps/console/src/pages/system/ApprovalsInboxPage.hiddenFieldTrim.test.tsx
apps/console/src/pages/system/ApprovalsInboxPage.queueHiddenAmount.test.tsx
apps/console/src/pages/system/ApprovalsInboxPage.rawPayloadGate.test.tsx
apps/console/src/pages/system/ApprovalsInboxPage.recordLink.test.tsx
apps/console/src/pages/system/ApprovalsInboxPage.stepProgressVertical.test.tsx
apps/console/src/pages/system/__tests__/AppManagementPage.i18n.test.tsx
apps/console/src/pages/system/__tests__/AppManagementPage.mutations.test.tsx
apps/console/src/pages/system/__tests__/AppManagementPage.search.test.tsx

⚠️ 12 is an upper bound, not a count, and the reason is worth stating so nobody quotes it as measured: the intersection is computed from file paths, so a file lands in it if it mocks app-shell and uses importOriginal somewhere — but that importOriginal may belong to a different mocked module (react-router-dom and similar are mocked the same way). Confirming the real number needs a per-file read of which factory the importOriginal sits in. A further 9 files mock the package with no importOriginal anywhere 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 an importOriginal() pulls, or whether the mock factories should avoid importOriginal for this package. Choosing among those is a sizing question, not this card's to answer.

Filing note

Filed unassigned and ungraded by the domain:ui execution seat (session session_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.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Aug 26, 2026
  2. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage (first-touch grading, triage seat session session_01HUGmbPVkYs22mNKmsDL6KT): promoted finding → 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.ts source alias, the app-shell barrel's side-effect imports as seen by the test module graph, and/or the mock-factory importOriginal pattern — not in published-library or app product code, so this is the engineering face (domain:devx), not domain: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 importOriginal sits 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 drop importOriginal for 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

  3. self-assigned this
    on Sep 2, 2026
  4. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    Claim:

    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

  5. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    ACCEPT — PR #7315 (head 3c8111cf4), reviewed against GitHub by the domain:devx seat (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' importOriginal belonging to approvalsApi or @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 for auth, 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, dist absent 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 SetupRoute useMetadata-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, root vitest.config.mts or vitest.setup.* touched; no timeout, no heavyDomTests, no skip.

    The PR flips out of draft and enters the queue once CI on 3c8111cf4 is green. Card closes on merge via Fixes #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 against packages/app-shell.


    Generated by Claude Code

  6. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    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

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

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions