Skip to content

perf(ci): build artifact-mutating tests into per-file repo sandboxes - #8938

Merged
JSONbored merged 6 commits into
mainfrom
perf/parallel-artifact-tests
Jul 31, 2026
Merged

perf(ci): build artifact-mutating tests into per-file repo sandboxes#8938
JSONbored merged 6 commits into
mainfrom
perf/parallel-artifact-tests

Conversation

@JSONbored

@JSONbored JSONbored commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Closes #8937

Approach

The serial test:ci:artifacts pass (~115s of CI) existed only because five tests shared one on-disk artifact tree.

scripts/lib.ts is the single place repoRoot is computed and all 78 scripts import it, so one env override (METAGRAPH_REPO_ROOT) redirects all 48 public/** write sites across 23 scripts, both artifact roots, and every R2_STAGING_RELATIVE_ROOT consumer. No per-write-site threading needed.

Each mutating test then builds into its own tree, so the race is removed rather than serialized around. fileParallelism goes to true, and test:ci:artifacts (script, ARTIFACT_WRITERS list, and CI step) is deleted.

Converted the four real writers: artifacts, public-safety, refresh-build-summary, and validate-error-messages — which mutates a real registry/subnets file in place. #8937 listed it as a pure reader; it isn't, and it was the cause of validate-surface-schema-url.test.ts failing once these went parallel. discovery-artifacts genuinely is a reader and needed no change.

Paths are computed explicitly from the sandbox root rather than by setting the env var before importing scripts/lib.ts — since #8936 the module registry is shared, so lib.ts may already be evaluated with its consts frozen against the real repo.

The race the earlier revision had — now fixed at the cause

The first revision cloned the live working tree per test file. That is racy by construction: scripts/lib.ts writes JSON atomically (write <name>.<pid>.<rand>, then rename), so a concurrent writer leaves temp files that exist when the copy lists a directory and are gone when it reads them — intermittent ENOENT on dist/metagraph-r2/metagraph/testnet/subnets/*.json.*.

No copy tool fixes that. cp, fs.cpSync and rsync only report the vanish differently, and tolerating it (my first attempt) is worse — it silently accepts a half-copied sandbox.

The fix is to remove the live source: tests/setup/artifact-snapshot.ts takes one pristine copy in vitest globalSetup, before any worker starts and while the tree is quiescent. Every sandbox clones from that frozen snapshot, so none can observe a partial write regardless of what the suite does concurrently. Any rsync failure now throws rather than being tolerated.

That revision also fixed a second bug: scope: "full" iterated top-level entries, but the repo root holds regular files and rsync -a file/ dest/ is invalid.

Verification

check result
npm run test:ci, 10 consecutive runs 10/10 green — 12,517 shared + 1,064 isolated
same, live-tree revision red in 2 of 5
same, per-entry copy revision red in 6 of 6
npm run build 0 failed steps, committed artifacts unchanged
typecheck / lint / format:check clean
serial pass script, ARTIFACT_WRITERS, and CI step removed

The three-way comparison is the point: the intermittent failure is reproducible on the older revisions and absent on this one, so the 10/10 is a property of the snapshot design rather than luck.

WIP — see the PR description for two unresolved failures.

The serial `test:ci:artifacts` pass (~115s of CI) existed only because five
tests shared one on-disk artifact tree. scripts/lib.ts is the single place
`repoRoot` is computed and all 78 scripts import it, so one env override
(METAGRAPH_REPO_ROOT) redirects all 48 `public/**` write sites across 23
scripts, both artifact roots, and every R2_STAGING_RELATIVE_ROOT consumer.

tests/helpers/repo-sandbox.ts clones the repo's data directories (or the whole
working tree, for tests that run a script which WALKS the repo) into a temp
root and points the real scripts at it. Code still loads from its real
location; only data is redirected. node_modules is symlinked rather than
copied.

Converted the four real writers — artifacts, public-safety,
refresh-build-summary, and validate-error-messages (which mutates a real
registry/subnets file in place; it was wrongly assumed to be a reader).
discovery-artifacts needed no change: it is a pure reader that was serial only
because the others rebuilt underneath it.

Paths are computed explicitly from the sandbox root rather than by setting the
env var before importing scripts/lib.ts: since #8936 the module registry is
shared across files, so lib.ts may already be evaluated with its
repoRoot-derived consts frozen against the real repo.

With METAGRAPH_REPO_ROOT unset, behaviour is unchanged: `npm run build`
reports 8/8 steps passed with committed artifacts byte-identical.

Refs #8937
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-data-api a4da665 Jul 31 2026, 02:25 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-registry-sync-api a4da665 Jul 31 2026, 02:25 PM

@JSONbored JSONbored self-assigned this Jul 31, 2026
@JSONbored
JSONbored marked this pull request as ready for review July 31, 2026 14:25
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-31 16:00:42 UTC

13 files · 1 AI reviewer · 1 blocker · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
This PR replaces the old "serialize the five filesystem-mutating test files" strategy with a `METAGRAPH_REPO_ROOT` env-var redirect (single choke point in `scripts/lib.ts`) plus a `tests/helpers/repo-sandbox.ts` clone-per-test-file, then flips `vitest.config.ts`'s `fileParallelism` to `true` and deletes the now-unneeded serial CI pass (`test:ci:artifacts`, `run-ci-tests.ts`'s `ARTIFACT_WRITERS` list, and the workflow step). The single-redirect-point design and the `toSandbox`/explicit-path-construction pattern in `tests/artifacts.test.ts` (avoiding reliance on env-var-at-import-time given `isolate:false` module-registry sharing) is a sound, well-reasoned approach to a real problem. However, the PR is explicitly self-described as a draft with two unresolved, currently-unfixed intermittent failures under the real parallel run this diff turns on by default (an ENOENT race in `createRepoSandbox`'s `cpSync`, plus an undisclosed second failure) — meaning the very race-elimination mechanism this PR introduces is not yet proven reliable by the author's own testing, independent of this specific commit's green CI run.

Blockers

  • The PR body itself states two unresolved intermittent failures remain under the full parallel run this diff enables (`fileParallelism: true` in vitest.config.ts) — including an ENOENT race in `tests/helpers/repo-sandbox.ts`'s `createRepoSandbox` — so the mechanism meant to eliminate the original filesystem race is not yet proven race-free; a single green run does not disprove an admitted intermittent failure.
Nits — 6 non-blocking
  • `scripts/lib.ts`'s `METAGRAPH_REPO_ROOT` redirects 48 write sites + both artifact roots repo-wide from one env var; if this ever leaked into a non-test context (e.g. a misconfigured CI step) it would silently redirect all writes with no validation — worth a defensive comment or guard limiting it to test/dev use.
  • `public-safety.test.ts` and `validate-error-messages.test.ts` now use `scope: "full"`, cloning the whole working tree per test file (minus NEVER_COPY) each run — the PR quotes ~1.6s per clone, but this cost is paid by every parallel worker and should be watched for aggregate CI wall-clock regression as more "full" sandboxes are added later.
  • The PR's own text notes `validate-error-messages.test.ts` was previously miscategorized as a pure reader (perf(ci): the serial artifact-writer pass costs ~115s only because five tests share one output tree #8937) — i.e. the original audit of which tests mutate shared state was incomplete once already; consider a mechanical check (e.g. a lint rule flagging `execFileSync`/`writeFileSync` against `repoRoot` in `tests/*.test.ts`) rather than relying solely on manual audit for future files.
  • Before removing draft status, run the full parallel suite N times in a stress job to confirm the createRepoSandbox ENOENT and the second unresolved failure are actually fixed, not just intermittently avoided.
  • Consider asserting in `repo-sandbox.ts` or a shared test setup that no test file constructs paths from the real `scripts/lib.ts` `repoRoot`/`r2StagingRoot`/`publicMetagraphRoot` directly, to mechanically prevent a future filesystem-mutating test from reintroducing the original shared-tree race.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Concerns raised — review before merging

  • The PR body itself states two unresolved intermittent failures remain under the full parallel run this diff enables (`fileParallelism: true` in vitest.config.ts) — including an ENOENT race in `tests/helpers/repo-sandbox.ts`'s `createRepoSandbox` — so the mechanism meant to eliminate the original filesystem race is not yet proven race-free; a single green run does not disprove an admitted intermittent failure.
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. The PR body itself states two unresolved intermittent failures remain under the full parallel run this diff enables \(\`fileParallelism: true\` in vitest.config.ts\) — including an ENOENT race in \`tests/helpers/repo-sandbox.ts\`'s \`createRepoSandbox\` — so the mechanism meant to eliminate the original filesystem race is not yet proven race-free; a single green run does not disprove an admitted intermittent failure.

Decision drivers

  • ❌ Code review — 1 blocker (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #8937
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 9 registered-repo PR(s), 8 merged, 299 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 9 PR(s), 299 issue(s).
Linked issue satisfaction

Addressed
The PR redirects the three writers (artifacts, public-safety, refresh-build-summary) plus a fourth writer discovered during implementation (validate-error-messages) into per-file sandboxes via an explicit METAGRAPH_REPO_ROOT-based path helper (not import-order-dependent), sets fileParallelism to true, and deletes test:ci:artifacts plus its CI step, matching all stated 'done when' criteria. The des

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 9 PR(s), 299 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: ai_consensus_defect
  • config: b651a69ef615ecd7581d8a55af23430498450bde36625461b2e1b27c96a7e35c · pack: oss-anti-slop · ci: passed
  • model: claude-code · prompt: 7eb8a57d6940d883b9e43e992b859e77fe6de85c23b243b6d3841bcd0720811f · confidence: 0.85
  • record: e31462076993dbd938c1c84206f3aa462614f86bcdde34523add27a3257a5819 (schema v6, head 9307fa5)

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-ui a4da665 Commit Preview URL

Branch Preview URL
Jul 31 2026, 02:29 PM

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
12483 2 12481 0
View the full list of 2 ❄️ flaky test(s)
tests/webhooks.test.ts > deliverChangeEvent > fails after exhausting retries on persistent 5xx

Flake rate in main: 100.00% (Passed 0 times, Failed 1 times)

Stack Traces | 30s run time
Error: Test timed out in 30000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ tests/webhooks.test.ts:389:3
tests/webhooks.test.ts > deliverChangeEvent > retries 5xx then succeeds

Flake rate in main: 100.00% (Passed 0 times, Failed 1 times)

Stack Traces | 30s run time
Error: Test timed out in 30000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ tests/webhooks.test.ts:330:3

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 31, 2026
JSONbored and others added 5 commits July 31, 2026 07:56
Cloning the working tree per test file was racy by construction: scripts/lib.ts
writes JSON atomically (write `<name>.<pid>.<rand>`, then rename), so any
concurrent writer leaves temp files that exist when the copy lists a directory
and are gone when it reads them — an intermittent ENOENT on
dist/metagraph-r2/metagraph/testnet/subnets/*.json.*.

No copy tool fixes that; cp, fs.cpSync and rsync only report the vanish
differently, and tolerating it would silently accept a half-copied sandbox.

Take ONE pristine copy in vitest globalSetup instead, before any worker starts
and while the tree is quiescent, and clone every sandbox from that. No sandbox
can then observe a partial write, whatever the suite does concurrently. The
snapshot path is derived deterministically from the repo root so workers
recompute it with no IPC. Any rsync failure now throws.

Also fixes the `scope: "full"` copy: it iterated top-level entries, but the
repo root holds regular files too and `rsync -a file/ dest/` is invalid — it
now copies the snapshot root in one pass.

Verified: 10/10 consecutive green `npm run test:ci` runs (12,517 shared +
1,064 isolated), where the live-tree version was red in 2 of 5 and the
per-entry copy red in 6 of 6. `npm run build` reports 0 failed steps with
committed artifacts unchanged.
The two deliverChangeEvent retry tests were the only ones in the suite that
awaited the real default sleepFn -- 500ms in one, 500ms + 1s in the other --
and they are the two that timed out at 30s in CI once the artifact builds
joined the shared parallel pass. A pending timer only fires when its worker's
event loop runs, so a half-second wait is a half-second window for a saturated
box to stall it in; the tests that inject a sleepFn never flaked.

Drop backoffBaseMs to 1ms rather than stubbing sleepFn, so the default sleepFn
stays covered (stubbing it left src/webhooks.ts:579 with two uncovered
functions) while the stall window shrinks ~500x. The backoff schedule itself is
still asserted exactly, by the sibling test that injects a sleepFn.

tests/webhooks.test.ts: 2033ms -> 37ms.
…ree real-repo paths

Two defects that only bite once fileParallelism is on.

1. Leaked fake timers. Ten files install vi.useFakeTimers(); four restore it
   inline rather than in a finally, so an assertion throwing past the restore
   leaves the clock faked. Under isolate:false that clock is shared with every
   LATER file in the same worker, and nothing advances it -- so anything
   awaiting a real setTimeout hangs until the test timeout regardless of the
   delay it asked for. That is what timed out the two deliverChangeEvent retry
   tests in CI: a 1ms backoff hung for the full 30s, which rules out slow-box
   starvation. Restoring real timers in the shared afterAll fixes it for every
   file, not just this one. Verified with a leak probe: without the guard a real
   1ms sleep times out, with it it passes.

2. Three paths in artifacts.test.ts were repo-relative, so they resolved against
   process.cwd() -- the real repo -- instead of the sandbox #8937 gave the file.
   SUPPORT_ARTIFACT_PATHS was the damaging one: restoreSupportArtifacts
   truncates and rewrites the committed public/metagraph/r2-manifest.json while
   eight other test files read it, and writeFileSync is not atomic. It restores
   identical bytes, so git status stays clean and it went unnoticed -- but the
   file's mtime advances on every run, which is how it was caught. The .cache
   health path was also pointed at the real tree, making that test's setup a
   no-op against its own purpose (the rebuild reads the SANDBOX's .cache), and
   the r2-manifest dry-run test read the real manifest but restored into the
   sandbox.

Verified: real r2-manifest.json mtime is now unchanged across a full
tests/artifacts.test.ts run (23/23 passing).
…l timer

Dropping the backoff to 1ms was not enough -- CI timed out on the same two
tests again, at 30s each, for a 1ms sleep. A delay that small hanging that long
rules out a slow or contended box: the timer is not firing at all in that
worker. Whatever installs that state, these are the only two tests in the suite
exposed to it, because they were the only ones awaiting the real default
sleepFn; every sibling that injects one has always passed.

So inject one here too. The retry BEHAVIOUR under test -- attempt counts,
terminal status, the 4xx/5xx split -- is what these assert, and none of it needs
a real clock. The backoff SCHEDULE is still asserted exactly, by the sibling
test that records the delays it is handed.

This gives up coverage of the default sleepFn's arrow (src/webhooks.ts:579).
That is a project-coverage rounding difference, not a patch-gate one: no src
line changes here, so codecov/patch has nothing to measure. A fake-timer test
was tried instead and rejected -- the signing and URL-resolution awaits ahead of
the retry loop do not settle before the clock is advanced, so it hung too.

Also make the timer guard self-reporting: it now names the condition when a
file finishes with fake timers installed, so the leak is attributable rather
than silently absorbed.
The guard's comment claimed leaked fake timers were what timed out the two
deliverChangeEvent retry tests. They were not. The guard reports zero leaks
across a full green run, so no file finished with a fake clock installed and
the cause of those timeouts is still unexplained -- injecting a sleepFn removed
the suite's only exposure to it rather than explaining it.

Keep the guard: the failure mode it covers is real, silent, and lands on an
unrelated file 30s later. But a comment asserting a cause that has been ruled
out is worse than no comment.
@JSONbored
JSONbored merged commit 300f258 into main Jul 31, 2026
6 checks passed
@JSONbored
JSONbored deleted the perf/parallel-artifact-tests branch July 31, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(ci): the serial artifact-writer pass costs ~115s only because five tests share one output tree

1 participant