Skip to content

fix(test): bound the native-profile teardown and stop parsing partial JSON (#1061) - #1071

Open
lidge-jun wants to merge 1 commit into
devfrom
codex/1061-native-profile-harness
Open

fix(test): bound the native-profile teardown and stop parsing partial JSON (#1061)#1071
lidge-jun wants to merge 1 commit into
devfrom
codex/1061-native-profile-harness

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Two defects in the same test, both reported in #1061.

The teardown had no deadline. It did expect(await restart.exited).toBe(0), so a child stalled in server.stop(true) hung the run until CI killed the job — a 30-minute wait for a test that had already finished its work.

Every wait is bounded now, including the ones after a signal. An ignored SIGTERM would otherwise reproduce the same hang one layer down, so it escalates to SIGKILL and observes the reap before throwing.

The settled file was parsed the moment it existed. waitFor proves a path exists, which is not the precondition a caller that immediately calls JSON.parse actually needs. The child wrote that file non-atomically, so a partially written document satisfied the wait and then threw Unexpected EOF — the failure the issue reports.

The child now publishes through atomicWriteFile, which the repository already uses precisely because it guarantees a reader sees either nothing or the whole document. The reader waits for something parseable rather than something present.

Both fixes reuse mechanisms already in the tree: the bounded-wait shape comes from the sibling native-profile-startup.test.ts, the deadline from tests/helpers/test-budget.ts, and the atomic writer from src/config.ts.

Closes #1061.

Verification

$ bun run typecheck
(clean)

$ bun test tests/native-profile-crash-boundaries.test.ts tests/native-profile-startup.test.ts tests/config.test.ts
 132 pass, 0 fail

$ bun run privacy:scan
Privacy scan passed

Both paths are driven, not merely present — a green suite says nothing about a timeout branch nobody exercises:

  • An opt-in, test-only stall (OCX_TEST_STALL_ON_STOP=1) makes the child hang exactly where the reported failure occurs, and the teardown must give up and reap it. Removing the bound makes that test time out at 30s instead of passing in 1.2s.
  • The partial-write race is reproduced directly: partial JSON on disk, then the real document, with the wait required to hold out.

One honest limit: a child that ignored SIGKILL is not testable and not a real case, so the escalation branch itself stays unproven.

Full suite on this branch is unaffected; the only failure anywhere is jawcode-metadata-sync, which fails identically on origin/dev.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • Tests
    • Strengthened validation of native profile startup and recovery scenarios.
    • Added coverage for incomplete state files to ensure they are not read until valid data is available.
    • Added checks for safely stopping stalled startup processes and escalating termination when necessary.
    • Improved test timing and cleanup reliability for more consistent results.

… JSON (#1061)

The teardown awaited child.exited with no deadline. A child stalled in
server.stop(true) hung the run until CI killed the job - a 30-minute wait for a
test that had already finished its work.

The waits are bounded now, including the ones after a signal: an ignored
SIGTERM would otherwise reproduce the same hang one layer down, so it escalates
to SIGKILL and observes the reap before throwing.

Second half, same issue: waitFor proves a file exists, which is not the
precondition a caller that immediately parses it needs. A half-written document
satisfied the wait and then threw Unexpected EOF. The settled file is now
published through atomicWriteFile, and the reader waits for something
parseable rather than something present.

Both paths are driven, not just present. An opt-in test-only stall makes the
child hang exactly where the reported failure occurred, and the teardown has to
give up and reap it; removing the bound makes that test time out at 30s instead
of passing in 1.2s. The partial-write race is reproduced directly.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deterministic PR hygiene checks passed.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The startup child now publishes settled results atomically and can stall during shutdown. Recovery tests wait for parseable JSON and use bounded shutdown with SIGKILL escalation. New tests cover partial writes and stalled-child cleanup.

Changes

Native profile crash-boundary tests

Layer / File(s) Summary
Atomic startup result publication
tests/helpers/native-profile-startup-child.ts:6, tests/helpers/native-profile-startup-child.ts:71-89
The child uses atomicWriteFile for success and error results. It can stall indefinitely after receiving the stop signal when OCX_TEST_STALL_ON_STOP=1.
Bounded recovery and teardown validation
tests/native-profile-crash-boundaries.test.ts:12, tests/native-profile-crash-boundaries.test.ts:87-139, tests/native-profile-crash-boundaries.test.ts:166-180, tests/native-profile-crash-boundaries.test.ts:241, tests/native-profile-crash-boundaries.test.ts:253, tests/native-profile-crash-boundaries.test.ts:289-335
waitForJson retries until the settled file contains parseable JSON. stopStartup performs bounded termination and escalates to SIGKILL. Startup spawning accepts extra environment variables. Tests cover stalled shutdown cleanup and partial JSON writes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug

Suggested reviewers: luvs01, ingwannu, wibias

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the two main changes: bounded native-profile teardown and prevention of partial JSON parsing.
Linked Issues check ✅ Passed The changes address issue #1061 by bounding teardown, escalating stalled children, and preventing partial JSON reads.
Out of Scope Changes check ✅ Passed All changes remain within the native-profile teardown and settled-state JSON reliability objectives in issue #1061.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/1061-native-profile-harness

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.

@github-actions github-actions Bot added the bug Something isn't working label Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/native-profile-crash-boundaries.test.ts`:
- Around line 130-135: The final child-reap wait in stopStartup must remain
bounded and its result must be checked; update
tests/native-profile-crash-boundaries.test.ts:128-133 to store the Promise.race
result and throw explicitly when it is null. In cleanup at
tests/native-profile-crash-boundaries.test.ts:309-313, replace the unbounded
child.exited await with the same bounded reap wait used by stopStartup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d6cb39a0-e1ee-46e6-9edc-d0d081554080

📥 Commits

Reviewing files that changed from the base of the PR and between 0e92714 and ee76578.

📒 Files selected for processing (2)
  • tests/helpers/native-profile-startup-child.ts
  • tests/native-profile-crash-boundaries.test.ts

Comment on lines +130 to +135
child.kill("SIGKILL");
// Observe the escalation before throwing, so a caller asserting on
// `exitCode` is not racing the reap.
await Promise.race([child.exited, Bun.sleep(KILL_GRACE_MS).then(() => null)]);
}
throw new Error("startup child did not stop");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline tests/native-profile-crash-boundaries.test.ts || true

echo "== relevant lines 100-150 =="
sed -n '100,150p' tests/native-profile-crash-boundaries.test.ts

echo "== relevant lines 280-330 =="
sed -n '280,330p' tests/native-profile-crash-boundaries.test.ts

echo "== occurrences of native-profile test helpers / exported interfaces =="
rg -n "stopStartup|child\\.exited|KILL_GRACE_MS|SIGKILL|exited" tests/native-profile-crash-boundaries.test.ts src || true

Repository: lidge-jun/opencodex

Length of output: 11802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== surrounding stopStartup callers in test =="
sed -n '200,230p' tests/native-profile-crash-boundaries.test.ts
sed -n '240,290p' tests/native-profile-crash-boundaries.test.ts
sed -n '298,360p' tests/native-profile-crash-boundaries.test.ts

echo "== static control-flow probe for stopStartup unbounded awaits =="
python3 - <<'PY'
from pathlib import Path
p = Path("tests/native-profile-crash-boundaries.test.ts")
text = p.read_text()
sig = "async function stopStartup("
start = text.find(sig)
end = text.find("\nfunction spawnSwitch") if "function spawnSwitch" in text[start:] else start + 2000
body = text[start:end]
lines = body.splitlines()
for i, line in enumerate(lines, 1):
    j = start + sum(len(x)+1 for x in lines[:i-1])
    if "await Promise.race([child.exited" in line or "await child.exited" in line:
        print(f"{p}:{i+j}: {line.strip()}")
PY

echo "== unbounded child.exited in target test sites =="
python3 - <<'PY'
from pathlib import Path
p = Path("tests/native-profile-crash-boundaries.test.ts")
for needle, line in [(133, "tests/native-profile-crash-boundaries.test.ts:133"), (312, "tests/native-profile-crash-boundaries.test.ts:312")]:
    print(needle + " contains Promise.race:", "Promise.race" in p.read_text().splitlines()[needle-1])
PY

Repository: lidge-jun/opencodex

Length of output: 7128


Keep final child-reap waits bounded and check their result.

tests/native-profile-crash-boundaries.test.ts:109-126 documents that this suite avoids unbounded await child.exited after signals, but line 133 still discards the final Promise.race result after SIGKILL. If the child is unreaped, stopStartup throws, and the expected-failure cleanup at line 312 then uses an unbounded await child.exited. That can restore the teardown hang this test adds coverage for.

  • tests/native-profile-crash-boundaries.test.ts:128-133: store the final race result and throw explicitly when it is null.
  • tests/native-profile-crash-boundaries.test.ts:309-312: replace the final await child.exited in cleanup with the same bounded reap wait used in stopStartup.
📍 Affects 1 file
  • tests/native-profile-crash-boundaries.test.ts#L130-L135 (this comment)
  • tests/native-profile-crash-boundaries.test.ts#L309-L313
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/native-profile-crash-boundaries.test.ts` around lines 130 - 135, The
final child-reap wait in stopStartup must remain bounded and its result must be
checked; update tests/native-profile-crash-boundaries.test.ts:128-133 to store
the Promise.race result and throw explicitly when it is null. In cleanup at
tests/native-profile-crash-boundaries.test.ts:309-313, replace the unbounded
child.exited await with the same bounded reap wait used by stopStartup.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant