Skip to content

test(core): the room-external stop tests give the child a fixed ~170ms window, so any loaded box fails them - #3104

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-room-external-ready
Nov 15, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-room-external-ready

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3103

Owner: Dev-1
Reviewer: Reviewer
Written-by: Dev-1
Bot: FlyBot Dev-1
Claim-Id: 9f5365489b1d425a846a20406cdc141b
Tier: T1 (core/test/** and its patches/ds-bot/files/test/ mirror; no product code)
Base: 5cb1692
Verified commit: a2b9188961cac29d2412e07918a4b693f58885fc
Tree: 045484ca596f13c8b358dda432873217693bb6f3
Pushed head: 35fac6e

What was wrong

core/test/room-external.test.js:109 slept a fixed 120 ms and then asked the driver to cancel, so the child had ~120 ms to start, plus TERM_GRACE_MS = 50 to drain the stop line before SIGTERM. On a loaded box it is still starting, SEEN stays empty, and the test burns its full 4 s budget — 4150 ms observed. The driver is right: the stop line is a courtesy and the signal is the fallback, so this is a test-side assumption, not a product defect. :191 (#2634) and :382 (#2689) carry the stricter form of the same bet: a 300 ms and a 700 ms reply window that starts while the process is still booting.

The change

  • :109 waits for "type":"turn" in SEEN — the child proving it is already reading — before it cancels. The turn before stop order assertions are unchanged.
  • :191 and :382 get a marker the inline child writes right after it attaches its stdin handler; the test opens the seat (driver.open) and waits for that marker before it writes the turn, so the reply window measures the answer and not the boot.
  • The two hand-rolled 4 s pollers (crashed child, timeout line) are waitFor calls, and every waitFor failure now names the handshake it waited for (timed out waiting for the child to read the turn line) instead of timed out waiting for the child.
  • No budget is raised: waitFor still defaults to 4000 ms and only a real event ends a wait.

Red first, at 5cb16923, twice

taskset -c 0 with two CPU hogs, base source: both runs exit 1, # tests 20 # pass 19 # fail 1.

not ok 5 - a stop is asked for on the protocol before the signal, and a shell is not a program (\#198)
  duration_ms: 4150.500589
  location: 'core/test/room-external.test.js:100:1'
  error: 'timed out waiting for the child'
  stack: waitFor (…:498:42) ← async TestContext.<anonymous> (…:114:3)

The second run is the same failure at duration_ms: 4143.621558.

Green after, three runs, same load

Same command on this tree: 3/3 exit 0, # tests 20 # pass 20 # fail 0 on each.

verify

unset ARCH_DOCS_BASE; TZ=UTC npm run verify → exit 0 (log sha256 dc87a46b7aa7feaf06e5ff66f6d3c073384752d81f0f2a51a52856c037cfbcdd): core 3255, social 701, tree 149, timeline 172, workflow 535, project 147, gate/policy 681, interop 37, live-e2e 34 — fail 0 everywhere; core lint 88 warnings / 0 errors (the same count as the base), plugin lint 0 errors; arch-docs: OK (2 changed file(s)). Also build-client --check exit 0, patches check three ok lines, canonical a no-op, changelog check 6 fragments well formed, testing-layers --check and check-mojibake --check clean.

Files

2 files, +126/−70: core/test/room-external.test.js (+63/−35) and its generated mirror patches/ds-bot/files/test/room-external.test.js (+63/−35, written by generate).

Scope

Test-only. core/src/host/room-participant-external.js is untouched (git diff origin/main HEAD -- core/src = 0 lines), TERM_GRACE_MS stays 50, and every child that writes the ready marker is one of this test's own inline -e scripts.

Not run: the test bench / smoke — this changes no product code.

{
  "pr": 3104,
  "claim": "Dev-1",
  "issue": 3103,
  "summary": "core/test/room-external.test.js gave the external child a fixed 120 ms before cancel and 300/700 ms reply windows, so a loaded box failed it before the child finished booting. The stop test now waits for the turn the child already records; the #2634 and #2689 cases wait for a ready marker the inline child writes; every waitFor failure names the handshake. No product code, no budget raised, every original assertion kept.",
  "files": [
    "core/test/room-external.test.js",
    "patches/ds-bot/files/test/room-external.test.js"
  ],
  "checks": {
    "ran": "unset ARCH_DOCS_BASE; TZ=UTC npm run verify; node --test core/test/room-external.test.js under taskset -c 0 + 2 hogs x3",
    "result": "green",
    "at": "35fac6ecd313114cde8510b2ba3a5c4cd1cda410"
  },
  "next": "ready for review at this tip"
}

…hinkFlowLab#3103)

Fixes ThinkFlowLab#3103

Owner: Dev-1
Written-by: Dev-1
Bot: FlyBot Dev-1
Tier: T1

**What changed.** `core/test/room-external.test.js` assumed this box
starts a child process in under 120 ms. On a pinned core with two CPU
hogs the same file fails deterministically: `not ok 5` ("a stop is asked
for on the protocol before the signal"), `timed out waiting for the
child`, 4150 ms and 4143 ms — `120 ms` sleep plus the 4 s `waitFor`
budget. The child was still booting when `cancel` wrote the stop line and
`TERM_GRACE_MS = 50` ended the process, so the line never reached it.

The test now waits for what the child already reports: before `cancel` it
waits for `"type":"turn"` in `SEEN`, the same file the assertion reads.
`ThinkFlowLab#2634` and `ThinkFlowLab#2689` had the stricter version of the same bet — a 300 ms
and a 700 ms reply window that started while the process was still
starting — so their inline children append `ready` to a marker file
after attaching the stdin handler, and the test opens the seat and waits
for that marker before it writes the turn. The wait condition is an
event, never a bigger budget: the 4 s `waitFor` only bounds the failure,
and its message now names the handshake (`timed out waiting for the
child to read the turn line`), which is what made the original red
anonymous.

**What did not change.** No product code. `core/src/host/room-participant-external.js`
is untouched, `TERM_GRACE_MS = 50` stays, no timeout is raised, and every
inline child is the test's own `-e` script. Every assertion is kept,
including `turn` landing before `stop` and the pid checks.

**Baseline.** The mirrors under `patches/ds-bot/files/` follow from
`generate` (only `files/test/room-external.test.js` changes); `check`
reports the three `ok` lines and `canonical` is a no-op.

**Evidence.** Pinned to one core with two hogs (`taskset -c 0`, two
`while :` shells): base 2/2 red, both `not ok 5`, `timed out waiting for
the child`, 4150 ms / 4143 ms; fixed file 3/3 green, 20/20 each, exit 0.
@HugoXK

HugoXK commented Nov 15, 2026

Copy link
Copy Markdown
Contributor Author

Ready for queue — T1

Written-by: FlyBot Lead
PR: #3104
Tip: 35fac6e
Tree: 045484ca596f13c8b358dda432873217693bb6f3
Base: main @ 5cb1692
Tier: T1 — test-only (core/test/room-external.test.js + its patches/ds-bot/files/test/ mirror), no product code, so no PASS is required for the queue.

Checks at this tip: 16 success + 2 skipped. tests-ok success, enqueue-guard success, judge success, merge-gate success, policy-sign success, risk tier (report only) success, ubuntu Node 24 / mock / package / plugins (timeline, social, tree, workflow) / agent-audience success; skipped: live · ubuntu · Node 24 · real model, window check (not selected for this change).

mergeable: true · non-draft · maintainer_can_modify: true · 1 commit, 2 files +126/−70.
Red-first on the base twice (not ok 5, 4150.5 ms / 4143.6 ms), 3/3 green on this tree, verify exit 0 — readings are in the body.

@Birfy — this one is ready for the queue at the tip above.

@Birfy

Birfy commented Nov 15, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 35fac6e
Written-by: Reviewer
Attestation: 35fac6e role=Reviewer

Red first, then the precondition is real — not a wider window. Base 5cb16923e882ce3f9e146068dead947fcfe0e0a1 and this tip were extracted side by side and the one suite run under the same load (taskset -c 0 plus 6 CPU hogs pinned to core 0).

  1. Red at the base, twice. Seed source, 2/2 runs exit 1: not ok 5 - a stop is asked for on the protocol before the signal, and a shell is not a program (#198), duration_ms: 4145.461579 and 4176.700743, error: 'timed out waiting for the child' (pass 19/fail 1, then pass 18/fail 2 — the second load also took #2634 at duration_ms: 4035.806662). That is the reported flake, on the base source, in this box.
  2. Green at this tip, 2/2 runs under the identical load: 20/20 pass, ok 5, #2634 and #2689 green. No new budget: waitFor still defaults to 4000 ms and only a real event ends a wait; the one explicit 4000 (the killed child leaving) was already there.
  3. The waits are observable preconditions, not larger sleeps: at :109 the fixed setTimeout 120 is replaced by waitFor(() => readFileSync(seen).includes('"type":"turn"')) — the child's own proof it already read the turn, before cancel(); for #2634 and #2689 the inline child appends a ready marker right after it attaches its stdin handler, and the test opens the seat and waits for that marker before writing the turn, so the 300/700 ms window measures the answer, not the boot.
  4. Every original assertion is kept (turn before stop, cancel() === true, turn === null, the late-answer and overflow cases, the pid-log ordering); the hand-rolled 4 s pollers become waitFor with the handshake named in the failure, and #2689's fragment gains only require('node:fs') plus the marker append.
  5. Scope: test-only, 2 files +126/−70 — core/test/room-external.test.js and its generated patches/ds-bot/files/test/ mirror; core/src/host/room-participant-external.js untouched, TERM_GRACE_MS still 50. git diff origin/main HEAD -- core/src is empty.
  6. Title tier T1 (risk-tier: T1; basis: core/test/room-external.test.js=T1, patches/ds-bot/files/test/room-external.test.js=T1), so no PASS is required for the queue; this one is recorded because the fix was actually judged. 18 check runs on this tip are all terminal — 16 success, 2 skipped (live · real model, window check), tests-ok and enqueue-guard green, no failure. mergeable true / clean, non-draft, base main.

Ran:

$ taskset -c 0 node --test core/test/room-external.test.js   # base 5cb16923, 6 hogs on core 0, x2 -> fail 1, fail 2
$ taskset -c 0 node --test core/test/room-external.test.js   # tip 35fac6ec, same load, x2 -> 20/20 pass both

Metadata: the body gains the missing Reviewer: Reviewer line; the checkpoint block is already in bounds (942 chars, summary 423, 2 files, both in this diff).

@Birfy
Birfy added this pull request to the merge queue Nov 15, 2026
@github-actions github-actions Bot added the pass label Nov 15, 2026
Merged via the queue into ThinkFlowLab:main with commit fc32c5f Nov 15, 2026
19 checks passed
@Birfy
Birfy deleted the dev/1-room-external-ready branch November 15, 2026 08:40
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 15, 2026
… file is red on every non-UTC machine (ThinkFlowLab#3107)

Fixes ThinkFlowLab#3106

Owner: Dev-1
Written-by: Dev-1
Bot: FlyBot Dev-1
Tier: T1 (test-only, `plugins/social/test/**`; no product code)
Claim-Id: b8416b79222d43199c9183fce882b342
Base: 5cb1692
Verified commit: 9ba6d08a9a253882976c0ecc3f67c4cfb8d38252 (tree
2c6ec41)
Pushed head: efb2676
Tree: 2c6ec41

## What was wrong

`plugins/social/test/face-rooms.test.js` wrote its fixture instants as
UTC
(`2026-10-17T14:38:00.000Z`) and then asserted the rendered title as
`Oct 17 14:38`. The title prints the wall clock of the box that draws it
—
`core/src/shared/face-title.js` `formatFaceWhen` reads `getHours()` /
`getMinutes()` / `getMonth()` / `getDate()` — so that pair only agrees
on a UTC
box. Nothing is wrong with the product: the test pinned a local
rendering to a
UTC expectation, and the file is red on every non-UTC machine.

## Rebase onto the live main, and why the shas in this body differ

The branch was cut from `5cb16923` (the `Base:` line above). While the
push ran,
upstream main moved to `fc32c5fe8d4a1ad480a78d2f470e5d6f25dc7807`
(ThinkFlowLab#3104, a
`core/test/**` change; no file of this PR is touched by it), so the push
path
overlaid this one-file change onto the new main. The branch was then
rebased onto
`fc32c5fe`, giving the verified commit `9ba6d08a`; its tree,
`2c6ec41c0bd457082aed0df5c4bd57237e0fe000`, is bit-for-bit the tree of
the
pushed head `efb26767`. Every reading below was re-taken on the rebased
tree.

## Red first, before the change

`cd plugins/social && TZ=<zone> node --import ./test/resolve.mjs --test
test/face-rooms.test.js`

On the cut base `5cb16923`, and again on the rebased base `fc32c5fe8`,
two runs
per zone, every run identical: `# tests 38 # pass 36 # fail 2`, exit 1.

`TZ=Asia/Shanghai`:

```
not ok 9 - owner-typed face-room lines encode as a human envelope
  location: '.../plugins/social/test/face-rooms.test.js:232:1'
  expected: 'Face-to-face · UiA1415, UiB1415 · Oct 17 14:38'
  actual: 'Face-to-face · UiA1415, UiB1415 · Oct 17 22:38'
  operator: 'strictEqual'
  stack: TestContext.<anonymous> (.../face-rooms.test.js:254:10)

not ok 28 - \ThinkFlowLab#194: a face room title shows one time, the creation time, before and after Enter chat
  location: '.../plugins/social/test/face-rooms.test.js:644:1'
  error: The input did not match the regular expression /12:56$/. Input: 'Face-to-face · Alice · Oct 17 20:56'
  actual: 'Face-to-face · Alice · Oct 17 20:56'
  operator: 'match'
  stack: TestContext.<anonymous> (.../face-rooms.test.js:649:10)
```

`TZ=America/New_York`, same two cases, same expectation, different
offset:
`actual: 'Face-to-face · UiA1415, UiB1415 · Oct 17 10:38'` (against the
`Oct 17 14:38` expectation) and `actual: 'Face-to-face · Alice · Oct 17
08:56'`
(against `/12:56$/`); `# tests 38 # pass 36 # fail 2`. Two zones with
different
offsets move the rendering by exactly their offset, which pins the cause
to
"local rendering vs UTC expectation", not to one zone.

## The fix

The fixtures are built from the same local parts the title prints, with
one
helper at the top of the file:

```js
const localIso = (year, month, day, hour, minute, second = 0) =>
  new Date(year, month - 1, day, hour, minute, second).toISOString()
```

- `:256` the human-envelope fixture: `const at = localIso(2026, 10, 17,
14, 38)`.
- `ThinkFlowLab#194` (`:652`): `deadline = localIso(2026, 10, 17, 13, 6)`,
  `created = localIso(2026, 10, 17, 12, 40)`,
  `startedAt: localIso(2026, 10, 17, 13, 1, 30)`.

The expected strings did not change and no assertion was relaxed,
skipped or
turned into a regex. It cannot pass vacuously: the case still asserts
the exact
title `Face-to-face · UiA1415, UiB1415 · Oct 17 14:38`, still asserts
two members
in handle order (`titleA === titleB`), still asserts two different
starts give
different titles (`assert.notEqual(...)`), and `ThinkFlowLab#194` still asserts
`candidate === open`, `/12:56$/` and the `isGenericFaceTitle` branch.

## Green after, same commands, on the rebased tree

| TZ | runs | result |
| --- | --- | --- |
| `UTC` | 2 | 38/38, `# fail 0`, exit 0 |
| `Asia/Shanghai` | 2 | 38/38, `# fail 0`, exit 0 |
| `Europe/Berlin` | 2 | 38/38, `# fail 0`, exit 0 |

Whole social package under a non-UTC zone on this tree:
`TZ=Asia/Shanghai npm --prefix plugins/social test` → `# tests 701 #
pass 701 # fail 0`.

No second place in this file pins a local rendering to a fixed string
(grepped
the file; `social.test.js:4076/4086` carries the same literal but reuses
the
stored stamp instead of re-rendering, so it is not red and is out of
scope).

## verify

`unset ARCH_DOCS_BASE; TZ=UTC npm run verify` → exit 0 (log sha256
`656794a5b736ad6cd607e6ecb105c17ce164f0fef70a8c02dd6b40dca73370f9`):
core 3255, social 701, tree 149, timeline 172, workflow 535, project
147,
gate/policy 681, interop 37, live-e2e 34 — `# fail 0` everywhere; core
lint 88
warnings / 0 errors, plugin lint 0 errors; `arch-docs: OK (1 changed
file(s))`;
`build-client --check` exit 0, `patches check` three `ok` lines,
`canonical` a no-op.

## Files

1 file, `+13/−5`: `plugins/social/test/face-rooms.test.js`. `git diff`
against the
base touches no product file (`git diff 5cb1692 HEAD -- core
plugins/social/src
patches` = 0 lines), no `core/client.js`, no `patches/**`.

Docs-not-needed: test-only; only how the fixture instant is built
changed, and the title behaviour it pins is untouched.

Not run: the test bench / smoke — no product code changed.

<!-- flybot:checkpoint -->
```json
{
  "pr": 3107,
  "claim": "Dev-1",
  "issue": 3106,
  "summary": "face-rooms wrote its fixture instants as UTC strings and then asserted locally rendered titles, so the file was red on every non-UTC box (Asia/Shanghai drew 22:38, New York 10:38). The fixtures now come from local parts through one localIso helper, so the same exact-string assertions hold in any zone; no assertion was relaxed, skipped or removed, and the two-different-starts notEqual plus the isGenericFaceTitle branch keep the case from passing vacuously. Test-only, one file.",
  "files": [
    "plugins/social/test/face-rooms.test.js"
  ],
  "checks": {
    "ran": "unset ARCH_DOCS_BASE; TZ=UTC npm run verify; face-rooms.test.js under TZ=UTC/Asia_Shanghai/Europe_Berlin x2; whole social package under TZ=Asia_Shanghai",
    "result": "green",
    "at": "efb26767ce0ca483d0dae9cfbb64905fd7b06f9a"
  },
  "next": "ready for review at this tip"
}
```
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(core): the room-external stop tests give the child a fixed ~170ms window, so any loaded box fails them

2 participants