Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); test(plugin-gantt): read the chart through an async query, not a race by os-sam · Pull Request #6966 · objectstack-ai/objectui · GitHub
Skip to content

test(plugin-gantt): read the chart through an async query, not a race - #6966

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake
Aug 31, 2026
Merged

test(plugin-gantt): read the chart through an async query, not a race#6966
os-sam merged 1 commit into
mainfrom
claude/issue-6959-gantt-lazy-read-flake

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#6959

packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 read the chart with a synchronousgetByTestId and lost the race twice in fifteen minutes in the merge queue, ejecting two PRs that do not touch plugin-gantt. This converts that read — and the sibling read on the next line — to the house async form. The assertion is unchanged. Nothing is skipped, quarantined, or timeout-inflated, and ObjectGantt itself is untouched.

One correction to the card's mechanism, stated rather than applied silently

The card attributes the fallback DOM to a React.lazy boundary. It is not one. ObjectGantt.tsx:65 imports GanttViewstatically, and there is no React.lazy anywhere in packages/plugin-gantt. Loading Gantt chart... is the if (loading) early return at ObjectGantt.tsx:1580-1587, driven by useState(true) plus the async data/schema fetch.

Everything else in the card holds, and the remedy is identical — this is a synchronous read of an asynchronously-appearing surface either way. The correction matters only for where the window is, and that is what made the probe below constructible.

The actual race

mount() gates on a mock callfind('task', { $expand }) was ISSUED. ObjectGantt flips loading false in the finally of reload(), i.e. after that find resolves. Those are two facts one promise-resolution apart, so on return from mount() the component can still be painting its loading branch.

mount()'s find-call gate is kept, deliberately. The refusal probe above this block needs proof of the schema-dependent commit, and the chart's presence alone does not carry it: loading flips false after the first find resolves, before objectSchema lands. Swapping one gate for the other would have weakened the refusal pin. The block now waits for both facts.

Measurement — the timing fact mutated, never the assertion

A throwaway probe copied the file's mocks, schema and mount() verbatim and changed exactly one thing: find resolves 500 ms late, as on a loaded queue runner. Both legs ran; the probe was deleted and is not in this PR.

  • Leg A, old spellingDOM at read time: "Loading Gantt chart...", and the sync read threw Unable to find an element by: [data-testid="gantt-view"]. That reproduces the queue failure in the card.
  • Leg B, new spellingawait findByTestId resolved through that fallback; data-count=2.

At 25 ms the probe did not reproduce: the window closed before waitFor's 50 ms poll observed the call. That negative reading is the point — locally the window is sub-50 ms, which is exactly why this passes on a quiet runner and fails on a loaded one.

The census — is :257 the only one?

No. Bound, stated: git ls-files -- '*.test.tsx' '*.test.ts' '*.spec.tsx' '*.spec.ts' = 2282 files. Hot control: every tier below reports :257 itself. Tier 1 initially did not — it required a literal render( in the block and :257 mounts through a helper — the control caught that and the instrument was fixed rather than the corpus declared clean.

tierrulehits
1async block, first sync DOM read, no gate naming that surface1105
2+ surface async-witnessed elsewhere in its own package413
3+ no DOM-touching gate at all before the read, resolving awaited file-local helpers176

Tier 3 is the shape: the awaited settle never touches the DOM.

In packages/plugin-gantt (in scope) tier 3 returns 3, and hand triage leaves exactly one real defect — :257. The other two are false positives: ObjectGantt.quickfilter.test.tsx:141 is gated at :135 through the file-local gv() helper, and GanttView.autoscheduledlg.test.tsx:63 has a synchronous mount with no async settle to race. The 10 tier-2 plugin-gantt hits triage the same way — every one has a real DOM gate, several hidden behind gv() / renderSettled().

173 tier-3 candidates sit outside plugin-gantt and are deliberately NOT touched here — several of those packages have live PRs. Assume roughly the 2-in-3 false-positive rate measured in plugin-gantt; the concentrations are app-shell 58, fields 28, apps/console 25, components 16, plugin-form 11, plugin-grid 9. These are reported to the PM for separate dispatch, not filed as findings from this seat.

Changeset

Owed, and added with empty frontmatter. scripts/check-changeset-presence.mjs guards all of each package's src/ with "No carve-out for test files under src/" stated explicitly in its header, and @object-ui/plugin-gantt is in the fixed group. Verified in both directions: the gate exits 1 before the changeset and 0 after, quoting

✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) … Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.

The skip-changeset label is not used: in this repo it is an inert historical label that exempts nothing, and the empty-frontmatter changeset is the declaration mechanism.

Verification — union run on the final commit, 6f5c01fb5

Joined with && so the verdict covers every part; run through the shared verify lock.

  • pnpm exec vitest run packages/plugin-gantt/Test Files 53 passed (53), Tests 434 passed (434)
  • pnpm --filter @object-ui/plugin-gantt run type-check — clean. Measured, not assumed: tsc --noEmit --listFiles contains 0 hits for the edited file (the package tsconfig excludes tests); the tsconfig.test.json leg contains 1. Coverage comes from the second leg.
  • check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:shell-escape-residue — all ✅ OK
  • check-changeset-presence / -no-major / -fixed / -overwrite — all

The vitest invocation matters: pnpm --filter PKG exec vitest is refused by this repo's guard, which reports that it silently runs apps/console's 22 files and prints Test Files 22 passed while running none of this package's. The guard fired on my first attempt; the runs above are the root-level form it prescribes.

Repo-wide pnpm lint was narrowed, and the narrowing is measured rather than asserted:

  1. Population read from eslint's own config, not guessed — files: ['**/*.{ts,tsx}'] plus the config's ignores.
  2. File count read from --format json1 file linted, 0 errors, 6 warnings.
  3. Invariance for untouched files — languageOptions declares only ecmaVersion and globals; there is no project / projectService, so type-aware linting is off and no rule's verdict on any file can depend on another file's contents.

The 6 warnings are pre-existing: linting the base blob of the same file gives 6 warnings / 0 errors, all @typescript-eslint/no-explicit-any — identical before and after. CI deliberately sets no --max-warnings.

Generated by Claude Code


Generated by Claude Code

`ObjectGantt.referenceArms-6837.test.tsx` mounted through its `mount()` helper
— whose gate settles a MOCK CALL, `find('task', { $expand })` was ISSUED — and
then read the DOM synchronously with `getByTestId('gantt-view')`. Those are two
different facts, one promise resolution apart: `ObjectGantt` flips `loading`
false in the `finally` of `reload()`, i.e. after that find RESOLVES, so on
return from `mount()` the component can still be painting `Loading Gantt
chart...`. The synchronous read raced it and lost twice in fifteen minutes in
the merge queue, ejecting two pull requests that do not touch `plugin-gantt`.
The assertion is unchanged and `mount()` keeps its find-call gate: the refusal
probe above needs proof of the SCHEMA-dependent commit, which the chart's
presence alone does not carry, since `loading` flips false after the FIRST find
resolves and before `objectSchema` lands. The block now waits for both facts
rather than trading one for the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.9 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-DkRl-aik.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.09KB116.42KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.26KB63.17KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Labels

Projects

None yet

2 participants

@os-sam@claude