Skip to content

test(cli): the watch-mode --icons-specifier test asserts on the module it waited for - #8365

Closed
vjeux wants to merge 2 commits into
meta:mainfrom
vjeux:uikit/f2-cli-watch-race
Closed

vjeux wants to merge 2 commits into
meta:mainfrom
vjeux:uikit/f2-cli-watch-race

Conversation

@vjeux

@vjeux vjeux commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

The failure this test showed is the build's second read of the theme, fixed in #8598; closing in its favor. The branch stays.

Depends on nothing (based on main).

Maintainer impact

build-theme.icons-specifier.test.mjs › watch mode forwards the flag to every rebuild fails intermittently in CI's test-node shard, on pull requests that change no CLI code. It fails at the final assertion: expected null to be 'import { testIcons } from "./icons.mjs"'.

Root cause (corrected in review; the first commit's diagnosis was wrong):

  • The test changes the theme and polls until the rebuilt module carries the new token, re-touching the theme on each poll until it does.
  • A rebuild reads the theme twice: it loads the module, then scans its source for the icon import.
  • The re-touch was an in-place writeFileSync, which truncates the file before it writes. A re-touch landing between a rebuild's two reads gives that rebuild an empty source to scan. With the test's empty icon registry, the build still exits 0, with the new token and no import line, and the poll accepts that module.
  • theme build renames its outputs into place, so no read of the module is ever half-written.

Intended invariant

Every theme the watcher sees is whole, so every rebuild the poll can accept was built from the touched theme, import included.

Change

  • Each touch is atomic: staged in a sibling file and renamed over the theme, as an editor's atomic save does. The watcher already follows renames.
  • The assertion reads the module the poll accepted. The comment above it now describes the real race.

Evidence

  • Failure before: a scratch copy of the test whose in-place re-touch holds its truncate open for 300 ms fails 5/5 with the CI error, expected null to be 'import { testIcons } from "./icons.mjs"'.
  • Success after: the same copy with the atomic touch, its staged file held open the same 300 ms, passes 5/5. The theme the watcher sees is never empty.
  • Product/runtime behavior verified unchanged: test file only.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run packages/cli/clients/cli/commands/build-theme.icons-specifier.test.mjs: 8 passed, three runs.
  • ASTRYX_STRICT_LINT=1 eslint on the file, pnpm check:changesets: clean.

…e it waited for

The test polls until the rebuilt module carries the new token, re-touching
the theme on each poll. A re-touch made before the rebuild showed up queues
one more rebuild, and the final assertion read the module from disk a
second time; landing inside that rewrite (writeFileSync truncates first),
it saw an empty module and the import line read null. The assertion now
reads the snapshot the poll saw, which a rebuild wrote, so it still pins
that the watch loop forwards --icons-specifier.
@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 25, 2026 7:35am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 25, 2026, 7:50 AM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 25, 2026
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the snapshot doesn't stop this failure, because the module the poll accepts can itself lack the import. theme build renames its outputs into place, so no read sees a half-written module; instead, a rebuild whose second read of the theme lands inside the test's in-place re-touch (fs.writeFileSync(themeFile, touched) truncates first) exits 0 with #0a0b0c and no import line, because the testIcons registry is empty, and the snapshot keeps that module. Replaying the test's watch flow with a wider gap between that truncate and write reproduces expected null to be 'import { testIcons } from "./icons.mjs"' at the new assertion, and staging the re-touch in a sibling file and renaming it over the theme makes it pass (the watcher already follows renames). Please make the re-touch atomic and update the comment above rebuiltModule to describe this race.

[Automated review]

Comment on lines +323 to +327
// The rebuilt module as read when the new token showed up. The
// assertion below reads this snapshot, not the file again: a re-touch
// in this loop can queue one more rebuild, and a second read can land
// inside that rewrite (writeFileSync truncates first) and see an empty
// module.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This mechanism can't occur at this head. theme build writes each output to a sibling temporary file and renames it over the old one (packages/cli/api/integration/add-helpers.mjs:357-388), so a read always sees a complete module.

The import-less module comes from the rebuild the poll observed. Its second read of the theme (packages/cli/api/theme/build/build.mjs:2688) can land inside the in-place re-touch below, and with the empty testIcons registry that build still exits 0, without the import.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right: theme build renames its outputs into place, so no read of the module was ever half-written. The import-less module came from a rebuild whose second read of the theme landed inside the test's in-place re-touch. b033699 makes each touch atomic, and the comment above rebuiltModule now describes that race.

try {
if (fs.readFileSync(builtFile, 'utf-8').includes('#0a0b0c'))
const generated = fs.readFileSync(builtFile, 'utf-8');
if (generated.includes('#0a0b0c')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The token is the only gate here, so this accepts the import-less module described above, and rebuiltModule keeps it. Staging the re-touch and renaming it over the theme removes the race, and the watcher already follows renames (packages/cli/clients/cli/commands/theme.mjs:206-219):

const staged = `${themeFile}.tmp`;
fs.writeFileSync(staged, touched);
fs.renameSync(staged, themeFile);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in b033699, as suggested: each touch is staged in a sibling .tmp file and renamed over the theme. A scratch copy of the test whose in-place re-touch holds its truncate open for 300 ms fails 5 of 5 runs with the CI error. The same copy with the atomic touch, its staged file held open the same 300 ms, passes 5 of 5.

…omically

A rebuild reads the theme twice: it loads the module, then scans its source
for the icon import. The test's re-touch was an in-place writeFileSync,
which truncates first, so a re-touch landing between those two reads gave a
rebuild an empty source to scan; with the test's empty icon registry that
build exits 0 with the new token and no import line, and the poll accepted
it. Each touch is now staged in a sibling file and renamed over the theme,
as an editor's atomic save does (the watcher follows renames). The comment
above the snapshot names the real race: theme build renames its outputs
into place, so a read of the module was never half-written.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This change fixes the flake: staging each touch in a sibling file and renaming it over the theme means a rebuild can no longer read a truncated theme and publish a module without the icon import, and the updated comments describe that race accurately. The assertion still fails if the watch loop stops forwarding --icons-specifier to its rebuilds, so the #4620 guard is intact. No further changes are needed.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — b033699f Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant