Skip to content

fix(#517): root-cause mechanism 2 — non-deterministic key order in childContractsSignature - #535

Merged
richard-devbot merged 1 commit into
mainfrom
flake-517-mechanism-2
Aug 1, 2026
Merged

fix(#517): root-cause mechanism 2 — non-deterministic key order in childContractsSignature#535
richard-devbot merged 1 commit into
mainfrom
flake-517-mechanism-2

Conversation

@richard-devbot

Copy link
Copy Markdown
Owner

Summary

Diagnoses and fixes the second, previously-undiagnosed mechanism from #517 — an intermittent full-suite-only flake where dashboard-command-pages.test.js's "index-served lite runs keep the persisted rollup..." saw run.fromIndex === undefined, meaning an unchanged run paid an unnecessary full re-parse instead of being served from the cache.

Root cause: childContractsSignature built its per-task signature object by mutating a SHARED object from inside concurrent async callbacks (sig[taskId] = perFile) — the resulting key INSERTION ORDER followed I/O completion order, not tasks.json's own task order. Promise.all resolves its result array positionally regardless of which callback's stat() actually finishes first, but a direct assignment inside the callback body races on completion instead.

sigEqual is a plain JSON.stringify comparison (key-order-sensitive), so two structurally IDENTICAL signatures could serialize to different strings purely from timing jitter — invisible in isolation (fast, low-contention stat() calls tend to resolve in call order) but real under full-suite CPU/IO load, where resolution order shuffles.

This matches every clue from the issue thread: deterministic in isolation (20/20 clean per the prior diagnosis), the index file genuinely is written (waiting on it didn't help — the bug isn't about missing data), and the signature covers mtimes so "a recompute difference under load" was the right instinct — it was just a key-ordering difference in an otherwise-identical recompute, not a real value change.

Verification

  • Live-reproduced the mechanism directly with a standalone script before touching any repo code: up to 3! = 6 distinct key orderings from Promise-resolution-order variance alone, on identical input data.
  • New tests/hub-rollup-signature-order-517.test.js stresses the actual mechanism via syncRootRuns with an injected jittery stat (randomized 0-8ms latency per call — no sleep-based flake-chasing), 40 repeated sync-pairs, asserting the run is served from the index every time.
  • Mutation-tested: reverting to the shared-object-mutation pattern reproduces the failure directly (8/40 served from index instead of 40/40).
  • Fixed by collecting [taskId, perFile] tuples and rebuilding via Object.fromEntries, whose insertion order is always the array order Promise.all guarantees (tasks.json's own order), independent of resolution timing.
  • 5 consecutive clean full-suite runs (1916/1916 each), typecheck/lint/security/validate all clean.

Test plan

  • npm test — 1916/1916 pass, ×5 consecutive full-suite runs
  • npm run typecheck — 0 errors
  • npm run lint — 0 errors
  • node scripts/security-audit.mjs — clean
  • npm run validate — clean
  • New regression test mutation-tested against the pre-fix code

🤖 Generated with Claude Code

…ildContractsSignature

Root cause: childContractsSignature built its per-task signature object by
mutating a SHARED object from inside concurrent async callbacks
(`sig[taskId] = perFile`) — the resulting key INSERTION ORDER followed I/O
completion order, not tasks.json's own task order. Promise.all resolves its
result array positionally regardless of which callback's stat() actually
finishes first, but a direct assignment inside the callback body races on
completion instead.

sigEqual is a plain JSON.stringify comparison (key-order-sensitive), so two
structurally IDENTICAL signatures could serialize to different strings
purely from timing jitter. Invisible in isolation — fast, low-contention
stat() calls tend to resolve in call order — but real under full-suite
CPU/IO load, where resolution order shuffles. This forced an unnecessary
re-parse of an unchanged run, losing `fromIndex` and failing
dashboard-command-pages.test.js's "index-served lite runs keep the
persisted rollup..." assertion — mechanism 2 from #517, left undiagnosed
after PR #532 fixed mechanism 1 (the ENOTEMPTY teardown race).

Live-reproduced the mechanism directly (not just reasoned about it): a
standalone script confirmed up to 3! = 6 distinct key orderings from
Promise-resolution-order variance alone. Fixed by collecting
`[taskId, perFile]` tuples and rebuilding via Object.fromEntries, whose
insertion order is always the ARRAY order Promise.all guarantees
(tasks.json's own order), independent of resolution timing.

New tests/hub-rollup-signature-order-517.test.js stresses the actual
mechanism via syncRootRuns with an injected jittery `stat` (randomized
0-8ms latency per call, no sleep-based flake-chasing) — 40 repeated
sync-pairs, asserting the run is served from the index every time.
Mutation-tested: reverting to the shared-object-mutation pattern reproduces
the failure directly (8/40 served from index instead of 40/40).

Verified: 5 consecutive clean full-suite runs (1916/1916 each), typecheck/
lint/security/validate all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Aug 1, 2026

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 32e19268-47e7-4f3a-b958-cdcb632280cf

📥 Commits

Reviewing files that changed from the base of the PR and between ec08bdb and f34b1b8.

📒 Files selected for processing (2)
  • src/observability/dashboard/state/rollup-index.js
  • tests/hub-rollup-signature-order-517.test.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix non-deterministic rollup signature ordering causing index cache misses (#517)

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Make child-contracts signature construction deterministic under async I/O timing variance
• Prevent false signature mismatches that forced unnecessary rollup re-parse and lost fromIndex
• Add regression test that injects jittery stat() latency to reproduce the full-suite flake
 mechanism
Diagram

graph TD
  T["Regression test"] --> S["syncRootRuns"] --> C["childContractsSignature"] --> O["Ordered entries"] --> E["sigEqual (JSON.stringify)"] --> I[("Rollup index")]
  C --> FS{{"io.stat (fs)"}}

  subgraph Legend
    direction LR
    _fn["Function/module"] ~~~ _db[("Cache/index store")] ~~~ _ext{{"External I/O"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Canonicalize signature before stringify (sort keys / stable stringify)
  • ➕ Makes all JSON.stringify-based comparisons insensitive to insertion order everywhere
  • ➕ Can be applied as a centralized fix at the comparison layer
  • ➖ Adds overhead and complexity (deep traversal/sorting) on every signature compare
  • ➖ May mask other ordering bugs instead of fixing the actual data construction
2. Replace JSON.stringify equality with deep-equal that ignores key order
  • ➕ Directly addresses the brittleness of order-sensitive comparison
  • ➕ Avoids needing to enforce deterministic insertion in every producer
  • ➖ Still adds runtime cost and introduces another dependency/utility
  • ➖ Comparison semantics can become subtle (arrays vs objects, prototype concerns)

Recommendation: Keep the PR’s approach: fix determinism at the source by collecting [taskId, perFile] tuples and using Object.fromEntries in Promise.all’s guaranteed positional order. It’s minimal, fast, and targets the actual race (shared object mutation inside concurrent callbacks) without broadening comparison semantics across the system.

Files changed (2) +105 / -5

Bug fix (1) +20 / -5
rollup-index.jsMake childContractsSignature deterministic under async completion jitter +20/-5

Make childContractsSignature deterministic under async completion jitter

• Replaces shared-object mutation inside concurrent callbacks with Promise.all collection of [taskId, perFile] entries and reconstruction via Object.fromEntries. This ensures object key insertion order follows tasks.json/taskIds order (not fs stat completion order), preventing false signature mismatches in JSON.stringify-based equality checks.

src/observability/dashboard/state/rollup-index.js

Tests (1) +85 / -0
hub-rollup-signature-order-517.test.jsAdd regression test for #517 signature key-order flake via jittery stat() +85/-0

Add regression test for #517 signature key-order flake via jittery stat()

• Adds a targeted test that seeds a minimal run fixture, injects randomized stat latency, and repeatedly syncs twice to assert the run is served from the index every time. This stress-tests the exact non-deterministic key insertion mechanism without relying on suite-level load conditions.

tests/hub-rollup-signature-order-517.test.js

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (6)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Remediation recommended

1. stat(p) uses ambiguous name 📜 Skill insight ⚙ Maintainability
Description
The new test defines stat: async (p) using a single-letter parameter name, reducing readability
and violating the descriptive naming requirement. This makes the test harder to maintain and review.
Code

tests/hub-rollup-signature-order-517.test.js[R35-38]

+    stat: async (p) => {
+      await new Promise((resolve) => setTimeout(resolve, Math.random() * 8));
+      return realStat(p);
+    },
Relevance

●●● Strong

Trivial readability nit (rename single-letter param) typically accepted; no conflicting precedent
needed.

PR-#532

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399635 requires descriptive variable names. The added code introduces `stat: async
(p) => ... realStat(p), where p` is an ambiguous single-letter name.

tests/hub-rollup-signature-order-517.test.js[35-38]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A single-letter variable name (`p`) is used for the path argument in the new test IO shim, which is not descriptive.

## Issue Context
Compliance requires JavaScript variables to have descriptive names.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[35-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Temp fixture not cleaned 📜 Skill insight ▣ Testability
Description
The new regression test creates a temporary project directory and fixture files (including .rstack
fixture data) but never removes them, leaving filesystem state behind across runs. This can
accumulate temporary directories, cause flaky behavior, and create avoidable disk/inode pressure in
CI and on developer machines.
Code

tests/hub-rollup-signature-order-517.test.js[R42-85]

+function seedFixture() {
+  const projectRoot = mkdtempSync(join(tmpdir(), 'rstack-sig-order-517-'));
+  const runId = '2026-07-06T12-00-00-000Z-sig-order-fixture';
+  const runDir = join(projectRoot, '.rstack', 'runs', runId);
+  const taskIds = ['003-architecture', '004-implementation', '005-testing'];
+  for (const taskId of taskIds) {
+    mkdirSync(join(runDir, 'tasks', taskId), { recursive: true });
+    writeFileSync(join(runDir, 'tasks', taskId, 'builder.json'), '{}');
+  }
+  writeFileSync(join(runDir, 'manifest.json'), JSON.stringify({
+    run_id: runId, schema_version: 2, goal: 'sig-order fixture', status: 'IN_PROGRESS',
+    created_at: '2026-07-06T12:00:00.000Z',
+  }));
+  writeFileSync(join(runDir, 'tasks.json'), JSON.stringify({
+    tasks: taskIds.map((id) => ({ id, status: 'PASS' })),
+  }));
+  // Far enough in the past that statusFromEntry always classifies this as
+  // 'stalled' (never 'active'), isolating the test to the signature-equality
+  // mechanism rather than the separate status-classification branch.
+  writeFileSync(join(runDir, 'events.jsonl'), JSON.stringify({
+    ts: '2026-07-06T12:01:00.000Z', type: 'task_started', task_id: '003-architecture',
+  }) + '\n');
+  return { projectRoot, runId };
+}
+
+test('#517: an unchanged run stays index-served across repeated syncs under I/O timing jitter', async () => {
+  const { projectRoot, runId } = seedFixture();
+  const io = jitteryIo();
+  const attempts = 40;
+  let servedFromIndex = 0;
+
+  for (let i = 0; i < attempts; i++) {
+    await syncRootRuns(projectRoot, { io, now: Date.now() });
+    const { runs } = await syncRootRuns(projectRoot, { io, now: Date.now() });
+    const run = runs.find((entry) => entry.runId === runId);
+    assert.ok(run, `fixture run present on attempt ${i}`);
+    if (run.fromIndex === true) servedFromIndex++;
+  }
+
+  assert.equal(
+    servedFromIndex, attempts,
+    `an unchanged run must be served from the index every time regardless of stat() timing (${servedFromIndex}/${attempts} were)`,
+  );
+});
Relevance

●●● Strong

Strong accepted precedent requiring temp test directories be cleaned up via try/finally + rmSync.

PR-#167
PR-#532

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400495 requires tests that create data (files/directories) to include
corresponding cleanup in afterEach/afterAll (or equivalent). In
tests/hub-rollup-signature-order-517.test.js, the test uses mkdtempSync() to allocate a
projectRoot under the OS temp directory and writes fixture files, but there is no cleanup hook
(e.g., after/afterEach) or try/finally to remove the directory; by contrast, existing
rollup-index tests demonstrate the established pattern of wrapping temp-fixture creation in
try/finally and deleting with rmSync afterward.

tests/hub-rollup-signature-order-517.test.js[42-85]
tests/dashboard-rollup-index.test.js[42-69]
PR-#167
Skill: testing-qa

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression test allocates a temporary `projectRoot` directory with `mkdtempSync()` and writes fixture files, but it does not delete the directory after the test completes, leaking temp directories and leaving `.rstack` fixture data behind.

## Issue Context
`seedFixture()` creates a temp directory under the OS temp folder and writes multiple files into it. PR Compliance ID 1400495 requires tests that create filesystem data to also clean it up via `afterEach`/`afterAll` (or equivalent) or a `try/finally` pattern. Other tests in the repo that use mkdtemp-based fixtures typically clean up with `rmSync(..., { recursive: true, force: true })` in a `finally` block (or via `t.after`).

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[24-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Unseeded random test jitter 🐞 Bug ⚙ Maintainability
Description
The new regression test injects stat() latency using Math.random(), so if a regression causes
intermittent failures they may be hard to reproduce locally due to a different random delay
schedule. This reduces diagnosability and can make the test’s regression-catching effectiveness
dependent on chance permutations of delays.
Code

tests/hub-rollup-signature-order-517.test.js[R33-39]

+function jitteryIo() {
+  return {
+    stat: async (p) => {
+      await new Promise((resolve) => setTimeout(resolve, Math.random() * 8));
+      return realStat(p);
+    },
+  };
Relevance

●● Moderate

Determinism is valued, but no clear precedent specifically about seeding/removing Math.random jitter
in tests.

PR-#475

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test’s IO jitter is directly driven by Math.random-based delays, which makes the execution
schedule non-reproducible across runs if a failure occurs.

tests/hub-rollup-signature-order-517.test.js[33-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test uses `Math.random()` to introduce timing jitter. While the assertion should be stable when the fix is correct, regressions may reproduce intermittently depending on the random schedule, making failures harder to debug.

## Issue Context
This test is intended to stress ordering variance; you can keep the variance while making it reproducible by using a deterministic delay schedule or a seeded PRNG.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[33-39]

## Suggested change
Replace `Math.random()` with one of:
- A deterministic per-call delay sequence (e.g., a counter cycling 0..7ms).
- A deterministic delay derived from the stat path (e.g., stable hash(path) % 8).
- A small seeded PRNG local to the test (fixed seed constant) so a failure can be reproduced exactly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Test uses existence-only assertion 📜 Skill insight ▣ Testability
Description
The new test includes assert.ok(run, ...), which only asserts existence/truthiness rather than
verifying specific expected behavior or state. This weakens the test signal and violates the
test-assertion quality requirement.
Code

tests/hub-rollup-signature-order-517.test.js[R76-78]

+    const run = runs.find((entry) => entry.runId === runId);
+    assert.ok(run, `fixture run present on attempt ${i}`);
+    if (run.fromIndex === true) servedFromIndex++;
Relevance

● Weak

Closest precedent: reviewers rejected “avoid assert.ok/truthiness-only assertions” guidance in
tests.

PR-#503

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399609 flags assertions that only check existence/truthiness. The added assertion
assert.ok(run, ...) is exactly this pattern.

tests/hub-rollup-signature-order-517.test.js[76-78]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test asserts `run` is truthy (`assert.ok(run, ...)`), which is an existence-only assertion.

## Issue Context
Compliance requires assertions to verify actual behavior/state, not mere existence.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[76-78]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Missing regression attribution block 📜 Skill insight ⚙ Maintainability
Description
The new regression test file’s header comment is missing required attribution metadata (date found
and QA report path) and also diverges from established tests/-folder header conventions (a leading
/** ... */ block with an owner: line). This reduces auditability and makes tests harder to scan
and maintain consistently across the suite.
Code

tests/hub-rollup-signature-order-517.test.js[R1-23]

+// #517 mechanism 2: an intermittent full-suite-only flake where
+// dashboard-command-pages.test.js's "index-served lite runs keep the
+// persisted rollup..." saw `run.fromIndex === undefined` — meaning a run
+// that should have been served from the cached index instead paid a full
+// re-parse, even though nothing about it had actually changed between the
+// two `buildFullState` calls.
+//
+// Root cause: `childContractsSignature` built its per-task signature object
+// by mutating a SHARED object from inside concurrent async callbacks
+// (`sig[taskId] = perFile`), so the resulting key INSERTION ORDER followed
+// I/O completion order, not `tasks.json`'s own task order — Promise.all
+// resolves its result ARRAY positionally regardless of which callback's
+// stat() actually finished first, but a direct assignment inside the
+// callback body races on completion instead. `sigEqual` is a plain
+// `JSON.stringify` comparison (key-order-sensitive), so two structurally
+// IDENTICAL signatures could serialize to different strings purely from
+// timing jitter — invisible in isolation (fast, low-contention stats tend to
+// resolve in call order) but real under full-suite CPU/IO load, where
+// resolution order shuffles.
+//
+// Reproduced directly here via an injected `stat` with randomized latency —
+// no sleep-based flake-chasing, a deterministic stress on the actual
+// mechanism.
Relevance

● Weak

Closest precedent: adding found-date/Report attribution lines in test headers was explicitly
rejected.

PR-#508
PR-#509
PR-#524

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400067 requires regression tests to include attribution fields such as issue ID,
what broke, the date found, and a QA report path, but the added test header only describes the
issue/mechanism and omits the found date and a Report: path. Separately, PR Compliance ID 1400208
requires new test files to follow existing project conventions; other hub tests use a leading `/**
... */ header that includes an owner:` line, while this new file uses line comments and omits
owner:, demonstrating the convention mismatch in the cited header region.

tests/hub-rollup-signature-order-517.test.js[1-23]
tests/hub-rollup-signature-order-517.test.js[1-25]
tests/hub-motion-tokens-512.test.js[1-16]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression test file’s header comment needs to be updated to both (1) include all required attribution metadata (including the date found and QA report path) and (2) match established `tests/` directory conventions by using the standard leading block comment format (including an `owner:` line).

## Issue Context
Compliance requires regression tests to include attribution metadata (issue ID, what broke, date found, QA report path) so failures can be traced back to a specific incident and QA evidence. Project standards also require new test files to follow existing header/comment conventions in `tests/` (commonly a `/** ... */` block with a structured summary and `owner:`) to keep the suite readable and consistent for maintenance and tooling.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[1-25]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
6. Test uses real filesystem IO 📜 Skill insight ▣ Testability
Description
The new test directly performs filesystem operations (mkdtempSync, mkdirSync, writeFileSync,
and realStat) instead of mocking/stubbing them. This can increase test runtime variance and
violates the requirement to mock external dependencies in tests.
Code

tests/hub-rollup-signature-order-517.test.js[R26-63]

+import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
+import { stat as realStat } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+import { syncRootRuns } from '../src/observability/dashboard/state/rollup-index.js';
+
+function jitteryIo() {
+  return {
+    stat: async (p) => {
+      await new Promise((resolve) => setTimeout(resolve, Math.random() * 8));
+      return realStat(p);
+    },
+  };
+}
+
+function seedFixture() {
+  const projectRoot = mkdtempSync(join(tmpdir(), 'rstack-sig-order-517-'));
+  const runId = '2026-07-06T12-00-00-000Z-sig-order-fixture';
+  const runDir = join(projectRoot, '.rstack', 'runs', runId);
+  const taskIds = ['003-architecture', '004-implementation', '005-testing'];
+  for (const taskId of taskIds) {
+    mkdirSync(join(runDir, 'tasks', taskId), { recursive: true });
+    writeFileSync(join(runDir, 'tasks', taskId, 'builder.json'), '{}');
+  }
+  writeFileSync(join(runDir, 'manifest.json'), JSON.stringify({
+    run_id: runId, schema_version: 2, goal: 'sig-order fixture', status: 'IN_PROGRESS',
+    created_at: '2026-07-06T12:00:00.000Z',
+  }));
+  writeFileSync(join(runDir, 'tasks.json'), JSON.stringify({
+    tasks: taskIds.map((id) => ({ id, status: 'PASS' })),
+  }));
+  // Far enough in the past that statusFromEntry always classifies this as
+  // 'stalled' (never 'active'), isolating the test to the signature-equality
+  // mechanism rather than the separate status-classification branch.
+  writeFileSync(join(runDir, 'events.jsonl'), JSON.stringify({
+    ts: '2026-07-06T12:01:00.000Z', type: 'task_started', task_id: '003-architecture',
+  }) + '\n');
Relevance

● Weak

Team previously rejected requests to mock/stub away real FS/process IO in tests.

PR-#511
PR-#514

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400146 requires tests to mock external dependencies rather than making real calls.
The new test imports and uses Node filesystem APIs and calls realStat on actual paths under a temp
directory.

tests/hub-rollup-signature-order-517.test.js[26-63]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test performs real filesystem reads/writes and stats, instead of mocking/stubbing filesystem dependencies.

## Issue Context
The compliance rule requires tests to mock external dependencies (including filesystem operations) to avoid relying on real external IO.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[26-63]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Magic numbers in jitter test 📜 Skill insight ⚙ Maintainability
Description
The new test uses literal values (e.g., 8 for jitter max and 40 attempts) instead of named
constants. This makes test intent and tuning harder to maintain.
Code

tests/hub-rollup-signature-order-517.test.js[R33-71]

+function jitteryIo() {
+  return {
+    stat: async (p) => {
+      await new Promise((resolve) => setTimeout(resolve, Math.random() * 8));
+      return realStat(p);
+    },
+  };
+}
+
+function seedFixture() {
+  const projectRoot = mkdtempSync(join(tmpdir(), 'rstack-sig-order-517-'));
+  const runId = '2026-07-06T12-00-00-000Z-sig-order-fixture';
+  const runDir = join(projectRoot, '.rstack', 'runs', runId);
+  const taskIds = ['003-architecture', '004-implementation', '005-testing'];
+  for (const taskId of taskIds) {
+    mkdirSync(join(runDir, 'tasks', taskId), { recursive: true });
+    writeFileSync(join(runDir, 'tasks', taskId, 'builder.json'), '{}');
+  }
+  writeFileSync(join(runDir, 'manifest.json'), JSON.stringify({
+    run_id: runId, schema_version: 2, goal: 'sig-order fixture', status: 'IN_PROGRESS',
+    created_at: '2026-07-06T12:00:00.000Z',
+  }));
+  writeFileSync(join(runDir, 'tasks.json'), JSON.stringify({
+    tasks: taskIds.map((id) => ({ id, status: 'PASS' })),
+  }));
+  // Far enough in the past that statusFromEntry always classifies this as
+  // 'stalled' (never 'active'), isolating the test to the signature-equality
+  // mechanism rather than the separate status-classification branch.
+  writeFileSync(join(runDir, 'events.jsonl'), JSON.stringify({
+    ts: '2026-07-06T12:01:00.000Z', type: 'task_started', task_id: '003-architecture',
+  }) + '\n');
+  return { projectRoot, runId };
+}
+
+test('#517: an unchanged run stays index-served across repeated syncs under I/O timing jitter', async () => {
+  const { projectRoot, runId } = seedFixture();
+  const io = jitteryIo();
+  const attempts = 40;
+  let servedFromIndex = 0;
Relevance

● Weak

Very similar “replace magic numbers with named constants in tests” suggestion was rejected.

PR-#531
PR-#522

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400588 requires replacing magic numbers used for timeouts/configuration with named
constants. The test hardcodes 8 in the jitter delay and 40 for the number of attempts.

tests/hub-rollup-signature-order-517.test.js[33-71]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test introduces numeric literals that function as configuration (`Math.random() * 8`, `attempts = 40`) rather than self-describing named constants.

## Issue Context
These numbers control test stress intensity and timing jitter; making them named constants (e.g., `JITTER_MAX_MS`, `ATTEMPTS`) clarifies intent and centralizes future tuning.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[33-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +35 to +38
stat: async (p) => {
await new Promise((resolve) => setTimeout(resolve, Math.random() * 8));
return realStat(p);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. stat(p) uses ambiguous name 📜 Skill insight ⚙ Maintainability

The new test defines stat: async (p) using a single-letter parameter name, reducing readability
and violating the descriptive naming requirement. This makes the test harder to maintain and review.
Agent Prompt
## Issue description
A single-letter variable name (`p`) is used for the path argument in the new test IO shim, which is not descriptive.

## Issue Context
Compliance requires JavaScript variables to have descriptive names.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[35-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +42 to +85
function seedFixture() {
const projectRoot = mkdtempSync(join(tmpdir(), 'rstack-sig-order-517-'));
const runId = '2026-07-06T12-00-00-000Z-sig-order-fixture';
const runDir = join(projectRoot, '.rstack', 'runs', runId);
const taskIds = ['003-architecture', '004-implementation', '005-testing'];
for (const taskId of taskIds) {
mkdirSync(join(runDir, 'tasks', taskId), { recursive: true });
writeFileSync(join(runDir, 'tasks', taskId, 'builder.json'), '{}');
}
writeFileSync(join(runDir, 'manifest.json'), JSON.stringify({
run_id: runId, schema_version: 2, goal: 'sig-order fixture', status: 'IN_PROGRESS',
created_at: '2026-07-06T12:00:00.000Z',
}));
writeFileSync(join(runDir, 'tasks.json'), JSON.stringify({
tasks: taskIds.map((id) => ({ id, status: 'PASS' })),
}));
// Far enough in the past that statusFromEntry always classifies this as
// 'stalled' (never 'active'), isolating the test to the signature-equality
// mechanism rather than the separate status-classification branch.
writeFileSync(join(runDir, 'events.jsonl'), JSON.stringify({
ts: '2026-07-06T12:01:00.000Z', type: 'task_started', task_id: '003-architecture',
}) + '\n');
return { projectRoot, runId };
}

test('#517: an unchanged run stays index-served across repeated syncs under I/O timing jitter', async () => {
const { projectRoot, runId } = seedFixture();
const io = jitteryIo();
const attempts = 40;
let servedFromIndex = 0;

for (let i = 0; i < attempts; i++) {
await syncRootRuns(projectRoot, { io, now: Date.now() });
const { runs } = await syncRootRuns(projectRoot, { io, now: Date.now() });
const run = runs.find((entry) => entry.runId === runId);
assert.ok(run, `fixture run present on attempt ${i}`);
if (run.fromIndex === true) servedFromIndex++;
}

assert.equal(
servedFromIndex, attempts,
`an unchanged run must be served from the index every time regardless of stat() timing (${servedFromIndex}/${attempts} were)`,
);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Temp fixture not cleaned 📜 Skill insight ▣ Testability

The new regression test creates a temporary project directory and fixture files (including .rstack
fixture data) but never removes them, leaving filesystem state behind across runs. This can
accumulate temporary directories, cause flaky behavior, and create avoidable disk/inode pressure in
CI and on developer machines.
Agent Prompt
## Issue description
The regression test allocates a temporary `projectRoot` directory with `mkdtempSync()` and writes fixture files, but it does not delete the directory after the test completes, leaking temp directories and leaving `.rstack` fixture data behind.

## Issue Context
`seedFixture()` creates a temp directory under the OS temp folder and writes multiple files into it. PR Compliance ID 1400495 requires tests that create filesystem data to also clean it up via `afterEach`/`afterAll` (or equivalent) or a `try/finally` pattern. Other tests in the repo that use mkdtemp-based fixtures typically clean up with `rmSync(..., { recursive: true, force: true })` in a `finally` block (or via `t.after`).

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[24-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +33 to +39
function jitteryIo() {
return {
stat: async (p) => {
await new Promise((resolve) => setTimeout(resolve, Math.random() * 8));
return realStat(p);
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

7. Unseeded random test jitter 🐞 Bug ⚙ Maintainability

The new regression test injects stat() latency using Math.random(), so if a regression causes
intermittent failures they may be hard to reproduce locally due to a different random delay
schedule. This reduces diagnosability and can make the test’s regression-catching effectiveness
dependent on chance permutations of delays.
Agent Prompt
## Issue description
The test uses `Math.random()` to introduce timing jitter. While the assertion should be stable when the fix is correct, regressions may reproduce intermittently depending on the random schedule, making failures harder to debug.

## Issue Context
This test is intended to stress ordering variance; you can keep the variance while making it reproducible by using a deterministic delay schedule or a seeded PRNG.

## Fix Focus Areas
- tests/hub-rollup-signature-order-517.test.js[33-39]

## Suggested change
Replace `Math.random()` with one of:
- A deterministic per-call delay sequence (e.g., a counter cycling 0..7ms).
- A deterministic delay derived from the stat path (e.g., stable hash(path) % 8).
- A small seeded PRNG local to the test (fixed seed constant) so a failure can be reproduced exactly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@richard-devbot
richard-devbot merged commit c055bd0 into main Aug 1, 2026
10 checks passed
@richard-devbot
richard-devbot deleted the flake-517-mechanism-2 branch August 1, 2026 07:04
richard-devbot pushed a commit that referenced this pull request Aug 1, 2026
…ipped (PRs #535 #536 #538)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant