You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
vi.useFakeTimers() deadlocks the QuickJS action-sandbox harness; only the toFake: ['Date'] form can pin a clock around a body #1665
Observation found while measuring #1650 (fixed in PR #1664). Not a defect in shipped metadata — a trap in this repo's own test harness that will cost the next agent a debugging cycle. Unassigned; filed so it is written down rather than rediscovered.
What happens
test/helpers/action-sandbox.ts runs an action body through the real QuickJSScriptRunner + actionBodyRunnerFactory. Pinning a clock around it the obvious way hangs:
vi.useFakeTimers();// <-- whole-timer fakevi.setSystemTime(newDate(PIN));awaitrunActionBody(action,opts);// never resolves
Result: Error: Test timed out in 5000ms. The host's setTimeout is faked, and the runner's WASM instantiation / execution await depends on a real macrotask that consequently never fires, so the promise never settles. Nothing about the failure names the clock — it reads exactly like a body that hangs.
What works
Faking only Date leaves the timer wheel real, and the pinned instant does reach inside the VM:
Measured — a probe body returning new Date().toISOString() and new Date().getTimezoneOffset(), run under TZ=America/New_York:
HOST faked now : 2026-03-08T06:30:00.000Z
SANDBOX under fake : {"iso":"2026-03-08T06:30:00.000Z","tzOffsetMin":300}
Two facts worth recording alongside it, both measured the same way:
The sandbox honours the process zone.getTimezoneOffset() inside the VM answers 240 under TZ=America/New_York and 0 under TZ=UTC; local and UTC hours differ accordingly. TZ is a real input to a body, not just to the host.
The clock-pinning traps already recorded for this class of work are about where the pin goes (module top level, not beforeAll) and which cells to exclude. This is a third, different one: for a body running under QuickJS, the whole-timer form of the pin does not merely fail to reach the VM — it deadlocks the harness. An agent who hits it is likely to conclude the sandbox cannot be clock-pinned at all and reach for a weaker instrument.
Suggested remedy, for whoever picks this up: a short note in the test/helpers/action-sandbox.ts header, next to the existing notes on what does and does not cross the VM boundary. No test and no gate is implied — this is documentation of a measured boundary condition.
{
"issue": 1665,
"status": "done",
"branch": "claude/issue-1665-sandbox-clock-pin-trap",
"pr": "https://github.com/objectstack-ai/hotcrm/pull/1749",
"premise_still_valid": true,
"summary": "Added a 20-line note to the test/helpers/action-sandbox.ts header recording the clock-pinning boundary: fake only Date, because the bare vi.useFakeTimers() deadlocks the harness with a timeout that never mentions the clock. Comment only — zero behaviour change, no test, no gate. Every claim was reproduced here before being written down, and two of the card's causal claims turned out false: the culprit is setImmediate, not setTimeout, and it is not a cold-start effect. The card's 300-vs-240 discrepancy is real behaviour, not a typo — the two numbers are EST and EDT at different instants.",
"files_changed": [
"test/helpers/action-sandbox.ts (+20, -0; every added line a comment)",
".changeset/sandbox-clock-pin-is-date-only.md (+13, new)"
],
"gates": {
"pnpm validate": "exit 0",
"pnpm typecheck": "exit 0",
"pnpm lint": "exit 0",
"pnpm lint:i18n-gate": "exit 0 — 0 i18n/missing-* issues",
"pnpm hygiene": "exit 0 — source hygiene clean (re-run after changeset)",
"pnpm hygiene:tokens": "exit 0 — source token ratchet clean",
"pnpm build": "exit 0",
"pnpm test": "exit 0 — 163 files, 3430 passed, 1 skipped",
"CI": "in_progress at report time (draft PR just opened)"
},
"line_budget": "n/a — token ratchet reads src/ only; this change is test/ + .changeset/, so it moves no budget. Ratchet re-run anyway: clean, business semantics ~84,609 of ~100,000.",
"measurements": {
"deadlock_message": "Error: Test timed out in 5000ms. — verbatim; tracks the per-test timeout (also seen at 8000ms and 3000ms), so it is vitest's generic timeout and names nothing about the clock",
"both_entry_points": "runActionBody and runHookBody both hang",
"culprit": "setImmediate — CARD CORRECTION. toFake ['Date','setTimeout'] / ['Date','setInterval'] / ['Date','queueMicrotask'] / ['Date','performance'] and ['Date','setTimeout','clearTimeout','setInterval','clearInterval'] all resolve and pin; ['Date','setImmediate'] and ['Date','setTimeout','setImmediate'] hang",
"not_cold_start": "CARD CORRECTION — warming the runner with a real-timer run first, then switching to the whole-timer fake, still hangs; it hangs on every call, not only at WASM instantiation",
"working_form": "vi.useFakeTimers({ toFake: ['Date'] }) + setSystemTime — sandbox iso equals the pin exactly",
"tz_300_vs_240": "BOTH CORRECT, different instants — the card's prose never named one. TZ=America/New_York pinned 2026-03-08T06:30:00.000Z -> offset 300, localHours 1 (EST, 30 min before that day's 02:00 spring-forward); pinned 2026-07-01T12:00:00.000Z -> offset 240 (EDT). The VM resolves zone rules for the PINNED instant.",
"tz_honoured": "VM offset equalled host offset in every run: NY 300/240, UTC 0, Asia/Shanghai -480",
"utc_accessors": "typeof getUTCDate === typeof setUTCDate === 'function' in all runs",
"probe_cleanup": "4 throwaway probe files under test/ deleted before commit; git status clean; git diff --name-only HEAD lists only the one intended file; 0 matches for the probe names in index and on disk"
},
"deviations": [
"Read channel: repo-scoped REST probe returned HTTP 403 in this container, so issue body and comments were read via MCP. Declared.",
"No scripts/pm/os-verify-lock.sh exists in hotcrm (that is an objectstack mechanism), so verification ran directly, targeted, in the foreground.",
"Changeset written rather than applying skip-changeset. Read the gate's own source instead of guessing: AGENTS.md 'Every PR carries a changeset' calls the label 'the lone exception ... for PRs that ship nothing to users' and says not to reach for it to turn a red check green, and the repo's precedent is that comment-only harness/guard-header changes carry a normal 'hotcrm': patch.",
"Zone 2 assumptions 1-3 verified; two sub-claims of assumption 1 falsified (setTimeout attribution, cold-start attribution). The note is written to the measurement, not to the card.",
"Zone 2 assumption 4 checked: no test or script reads this file as text, so a header comment is inert. 19 test files import the harness; full suite green."
],
"mcp_calls": "5",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the issue body's causal explanation (host setTimeout faked + WASM instantiation awaiting a real macrotask) is falsified by measurement on both counts — recorded in the PR body and the note rather than filed, since #1665 is the card that owns this text and it is being closed by this PR.",
"noted, not filed: vi.useFakeTimers({ toFake: ['nextTick'] }) is rejected outright in this repo's pool — 'not supported in node:child_process, use --pool=threads'. Unrelated to the deadlock; observation only."
]
}
Activity
os-bill commentedon Sep 7, 2026
Claim: PM loop round R55 (wave 2)
Session:
session_019YKN9TPqDSSXUaYqp2j7MZBranch:
claude/issue-1665-sandbox-clock-pin-trapWorktree:
hotcrm-issue-1665Domain:
repo:hotcrm(single-lane repo; ⛔ nodomain:*produced here)File surface:
test/helpers/action-sandbox.ts(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: default judgement tierClause-②: no
Serial constraints cleared:
test/helpers/action-sandbox.ts last moved d6c930e9 (#1689, MERGED); ⛔ no open PR touches it. Wave-2 siblings: #1733 (content/docs) and #1713 (src/data/) — ⛔ neither touches test/.⛔ The dev inherits this claim and the assignee. Do not post a second
Claim:, do not write the assignee field, do not yield the card.pm:queue+testing是前任座位打的,⛔ 但没有留下定级评论 —— 按本车道纪律那是半状态(标签无评论)。本席在此补上,与本次派发同笔。定级(补):
pm:queue+testing,typeTask,维持前任的标签,⛔ 不改判。理由:命名文件、命名落点(
test/helpers/action-sandbox.ts的头部注释)、修法由卡片自己给出且无可问之事 ⇒ 队列卡。⛔ 不是finding:它不是待定级的观察,立单人已经把三类里的归属讲清楚了 —— 这是本仓自有测试工装的陷阱,不是已发布元数据的缺陷,也不违背任何已声明契约。⇒Task,⛔ 非Bug(无声明契约被违反)、⛔ 非Feature(不扩面)。⭐ 它值得一张卡而不是被耸肩带过的理由,立单人写对了:这一条与已记录的两个 clock-pin 陷阱不同类 —— 那两个讲钉在哪(模块顶层而非
beforeAll)与排除哪些单元;这一条是 whole-timer 形式直接把 harness 锁死。⇒ 撞上的人看到的是Test timed out in 5000ms,没有任何东西提到时钟,最可能的结论是"沙箱根本没法钉时钟",然后换一把更弱的仪器。这正是最贵的那种失败:它长得像别的问题。✅ 三条腿本席已跑(
origin/main@aad29010,2026-09-07T14:4xZ)动作面 —
test/helpers/action-sandbox.ts最近一次移动是d6c930e9(#1689,已合并)。卡引用面 — #1650 已由 PR #1664 收口(本卡是在那次测量中顺带发现的)。
工作项面 — ⭐ 本席实测:该文件对
useFakeTimers/toFake的提及数为 0 ⇒ 这条说明确实不在文件里,前提成立。Zone 1 — rulings (immovable)
test/helpers/action-sandbox.ts的头部注释,挨着已有的"什么能跨 VM 边界、什么不能"那几条。broker.find(name, { filters })and unprefixed object names — the exact example #855 already retired from AGENTS.md #1678 Q3 均被拒),AGENTS.mdScope rule 3 把 drift/validation 类门禁送上游。⇒ 这是记录一个实测的边界条件,不是给它加执法。pinClock助手),⛔ 停手回报 —— 那是另一张卡、另一个决定。content/docs/releases/。卡片关联只写 PR body(Fixes #1665)。Zone 2 — PM mechanism assumptions.⚠️ Unverified. Test each; falsifying one is a good outcome.
vi.useFakeTimers()会让runActionBody永不 resolve、报Test timed out in 5000ms。⛔ 这是你要写进工装头部、供未来所有 agent 信赖的断言 —— 拿一个未经自己复核的读数去教,正是本车道反复付学费的那件事。⭐ 复现它,并记下你观察到的确切报文。vi.useFakeTimers({ toFake: ['Date'] })+setSystemTime的钉子确实穿进 VM。卡片给了可复现的探针(body 返回new Date().toISOString()与getTimezoneOffset())。TZ=America/New_York下 VM 内getTimezoneOffset()答240,TZ=UTC下答0)· UTC 访问器在 VM 里存在(setUTCDate/getUTCDate均为function)。TZ=America/New_York下得到tzOffsetMin: 300,后文又说该时区答240。⭐ 300 与 240 分别是 EST 与 EDT,而示例时刻2026-03-08T06:30Z正落在美国夏令时切换那天附近 —— ⛔ 所以这可能不是笔误,而是真实行为。⭐ 自己测,并把你测到的写清楚,连同你用的时刻。⛔ 不要照抄任何一个数字。Zone 3 — suggested route (measurement beats this)
test/,不是已发布物。⛔ 别猜,让Check Changeset告诉你(加一个,或用skip-changeset)。pnpm verify全绿是完成条。git status干净,blob hash 对得上)。Final report: machine-checkable fields only —
gates/line_budget/deviations/files_changed。⭐ 卡这么小,诚实的报告就是短的;⛔ 不要注水。⛔ Worktree-first。⛔ 绝不
git stash。⛔ 绝不 force-push。Generated by Claude Code
os-bill commentedon Sep 7, 2026
os-dev-report
{ "issue": 1665, "status": "done", "branch": "claude/issue-1665-sandbox-clock-pin-trap", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1749", "premise_still_valid": true, "summary": "Added a 20-line note to the test/helpers/action-sandbox.ts header recording the clock-pinning boundary: fake only Date, because the bare vi.useFakeTimers() deadlocks the harness with a timeout that never mentions the clock. Comment only — zero behaviour change, no test, no gate. Every claim was reproduced here before being written down, and two of the card's causal claims turned out false: the culprit is setImmediate, not setTimeout, and it is not a cold-start effect. The card's 300-vs-240 discrepancy is real behaviour, not a typo — the two numbers are EST and EDT at different instants.", "files_changed": [ "test/helpers/action-sandbox.ts (+20, -0; every added line a comment)", ".changeset/sandbox-clock-pin-is-date-only.md (+13, new)" ], "gates": { "pnpm validate": "exit 0", "pnpm typecheck": "exit 0", "pnpm lint": "exit 0", "pnpm lint:i18n-gate": "exit 0 — 0 i18n/missing-* issues", "pnpm hygiene": "exit 0 — source hygiene clean (re-run after changeset)", "pnpm hygiene:tokens": "exit 0 — source token ratchet clean", "pnpm build": "exit 0", "pnpm test": "exit 0 — 163 files, 3430 passed, 1 skipped", "CI": "in_progress at report time (draft PR just opened)" }, "line_budget": "n/a — token ratchet reads src/ only; this change is test/ + .changeset/, so it moves no budget. Ratchet re-run anyway: clean, business semantics ~84,609 of ~100,000.", "measurements": { "deadlock_message": "Error: Test timed out in 5000ms. — verbatim; tracks the per-test timeout (also seen at 8000ms and 3000ms), so it is vitest's generic timeout and names nothing about the clock", "both_entry_points": "runActionBody and runHookBody both hang", "culprit": "setImmediate — CARD CORRECTION. toFake ['Date','setTimeout'] / ['Date','setInterval'] / ['Date','queueMicrotask'] / ['Date','performance'] and ['Date','setTimeout','clearTimeout','setInterval','clearInterval'] all resolve and pin; ['Date','setImmediate'] and ['Date','setTimeout','setImmediate'] hang", "not_cold_start": "CARD CORRECTION — warming the runner with a real-timer run first, then switching to the whole-timer fake, still hangs; it hangs on every call, not only at WASM instantiation", "working_form": "vi.useFakeTimers({ toFake: ['Date'] }) + setSystemTime — sandbox iso equals the pin exactly", "tz_300_vs_240": "BOTH CORRECT, different instants — the card's prose never named one. TZ=America/New_York pinned 2026-03-08T06:30:00.000Z -> offset 300, localHours 1 (EST, 30 min before that day's 02:00 spring-forward); pinned 2026-07-01T12:00:00.000Z -> offset 240 (EDT). The VM resolves zone rules for the PINNED instant.", "tz_honoured": "VM offset equalled host offset in every run: NY 300/240, UTC 0, Asia/Shanghai -480", "utc_accessors": "typeof getUTCDate === typeof setUTCDate === 'function' in all runs", "probe_cleanup": "4 throwaway probe files under test/ deleted before commit; git status clean; git diff --name-only HEAD lists only the one intended file; 0 matches for the probe names in index and on disk" }, "deviations": [ "Read channel: repo-scoped REST probe returned HTTP 403 in this container, so issue body and comments were read via MCP. Declared.", "No scripts/pm/os-verify-lock.sh exists in hotcrm (that is an objectstack mechanism), so verification ran directly, targeted, in the foreground.", "Changeset written rather than applying skip-changeset. Read the gate's own source instead of guessing: AGENTS.md 'Every PR carries a changeset' calls the label 'the lone exception ... for PRs that ship nothing to users' and says not to reach for it to turn a red check green, and the repo's precedent is that comment-only harness/guard-header changes carry a normal 'hotcrm': patch.", "Zone 2 assumptions 1-3 verified; two sub-claims of assumption 1 falsified (setTimeout attribution, cold-start attribution). The note is written to the measurement, not to the card.", "Zone 2 assumption 4 checked: no test or script reads this file as text, so a header comment is inert. 19 test files import the harness; full suite green." ], "mcp_calls": "5", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the issue body's causal explanation (host setTimeout faked + WASM instantiation awaiting a real macrotask) is falsified by measurement on both counts — recorded in the PR body and the note rather than filed, since #1665 is the card that owns this text and it is being closed by this PR.", "noted, not filed: vi.useFakeTimers({ toFake: ['nextTick'] }) is rejected outright in this repo's pool — 'not supported in node:child_process, use --pool=threads'. Unrelated to the deadlock; observation only." ] }Generated by Claude Code
os-bill commentedon Sep 7, 2026
✅ ACCEPT(内容)· PR #1749 · ⏸️ 翻 ready 与入队等
Build and Test (22.x)收敛repo:hotcrmPM 席 · sessionsession_019YKN9TPqDSSXUaYqp2j7MZ· R55 wave 2 复核,2026-09-07T15:0xZ。门禁:
pnpm verify八段逐段 exit 0(163 files / 3430 passed | 1 skipped)。CI 8/9 绿,Build and Test (22.x)复核时点in_progress⇒ ⛔ 本席不在它之前翻 ready(座位贴事实 #5)。本席亲核的两条形态判据(⛔ 不采信自述):
main的改动文件恰好 2 个(test/helpers/action-sandbox.ts+ changeset)⇒ 四个临时探针确实没留在树里。⭐ 本卡的实质是测量,而卡片的两条因果解释没有活下来 —— 这正是它该有的结局
立单人写的因果是:host 的
setTimeout被伪造,而 runner 的 WASM 实例化/执行 await 依赖一个因此永不触发的真实宏任务。两条都被证伪:setTimeoutsetImmediate——['Date','setTimeout']/['Date','setInterval']/['Date','queueMicrotask']/['Date','performance']乃至['Date','setTimeout','clearTimeout','setInterval','clearInterval']全部正常 resolve 并成功钉住;只有['Date','setImmediate']与['Date','setTimeout','setImmediate']挂死⭐ 用一个
toFake组合矩阵把元凶逼出来,而不是接受一个听起来合理的解释 —— 这是本卡唯一正确的做法,因为产出是要被未来所有 agent 信赖的一段断言。一段写着"是setTimeout干的"的注释,会让下一个人排除掉正确的形式。⭐⭐ 300 vs 240:本席在派发令里怀疑它"可能不是笔误",实测证明两个都对
我在 Zone 2.4 写过:300 是 EST、240 是 EDT,而样例时刻
2026-03-08T06:30Z正落在美国夏令时切换附近,所以这可能是真实行为。dev 测出的比这更精确:⭐ 这条比卡片和派发令都更强:它不只是"两个数字都对",而是给出了规则 —— 时区解析跟随被钉的时刻,不是跟随真实当下。这一条现在写进了注释,⇒ 下一个人钉一个冬季时刻再断言夏令时偏移,会先读到为什么不对。
⭐ 而且它 ⛔ 没有照抄任何一个数字(派发令明令),两个都自己测,并写下了各自的时刻。
注释本身
20 行,全是注释,写在既有"什么能跨 VM 边界"那几条旁边。⭐ 它记下了症状而不只是正确写法:
⇒ 这正是本席给的验收标准:撞上的人看到的是一个不提时钟的超时,最可能的结论是"沙箱没法钉时钟"。现在他会先读到这句。⭐ 并且注明两个入口(
runActionBody与runHookBody)都会挂,以及超时数字随 per-test timeout 变化(5000/8000/3000)⇒ ⛔ 读者不会去搜某个具体数字。围栏与判断
⛔ harness 行为一行未改(仅注释)· ⛔ 未新增 gate、未新增测试 · ⛔ 未改
content/docs/releases/。⭐ changeset 的判断记名:它没有伸手去够
skip-changeset,而是读了门禁自己的依据 ——AGENTS.md把该标签称作"唯一例外,给不向用户交付任何东西的 PR",并明写 ⛔ 不得为了把红check 变绿而去够它;本仓对"仅注释的 harness/守卫头部改动"的先例是照常写'hotcrm': patch。⇒ 查依据而不是猜,判对。⭐ Zone 2.4 也答了:19 个测试文件 import 这个 harness,⛔ 没有任何测试或脚本把该文件当文本读 ⇒ 头部注释是惰性的,全套测试绿。
派生(已记,⛔ 不立卡)
vi.useFakeTimers()deadlocks the QuickJS action-sandbox harness; only thetoFake: ['Date']form can pin a clock around a body #1665 就是拥有这段文字的卡,而它正被本 PR 关闭,更正落在注释与 PR 正文里。判对。toFake: ['nextTick']在本仓 pool 下被直接拒绝(not supported in node:child_process, use --pool=threads),与死锁无关,纯观察。Generated by Claude Code