From 9a6822b83eb6f78696c85d3b049ca67efc1a58da Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 08:14:36 +0800 Subject: [PATCH 01/11] =?UTF-8?q?test(ci):=20=E7=BB=99=20server=20?= =?UTF-8?q?=E8=A1=A5=E4=B8=8A=E8=81=9A=E5=90=88=E5=8D=95=E6=B5=8B=E9=97=A8?= =?UTF-8?q?(69=20=E4=B8=AA=E5=8D=95=E6=B5=8B=E6=AD=A4=E5=89=8D=20CI=20?= =?UTF-8?q?=E5=8F=AA=E8=B7=91=206=20=E4=B8=AA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server/src 下 69 个 *.test.ts,CI 可达的只有 6 个(scripts/qa.sh 的 L0_TESTS 点名 5 个 + test686 引用 1 个),另外 63 个没有任何 job 会碰。server 是 hub 本体 —— 认证、token、网络隔离都在这里,盲区比 agent-network 那 46 个严重。 形状抄 test745/test725,但按 server 自己的契约做了两处改动: 1) 逐文件跑,每个文件一个独立 DB。scripts/qa.sh 的 L0 本来就是 `COMMHUB_DB=/tmp/qa-l0-$name.db bun test ` —— 这是既有契约。 用一个共享 DB 聚合跑会红 4 条(admin-networks 的 global-admin 可见性、 scheduled-tasks 三条),而这 4 条单跑全绿,是跨文件状态污染。 把"聚合能不能跑"当门等于给它加了一条它从没承诺过的性质。 2) cwd 必须是仓根。task-lifecycle-watcher 用 process.cwd() 拼 ./server/src/db.js,scheduled-tasks-http 按仓根相对路径 import tests/test601-.../race-worker.ts。从 server/ 目录跑会让这两个红在路径上, 看起来像产品坏了。 红线:COMMHUB_DB 不设默认指向生产库。容器里够不到宿主的库,但不靠"够不到" 保证 —— run.sh 显式钉到 /tmp 并断言钉住了。31/69 个测试引用 sqlite/COMMHUB_DB。 分母承重:executed_files 必须等于 find 出来的 test_files,少一个就红。 witnessed-red:把 auth.ts 注册密码下限 `< 8` 改成 `< 1`(7 位密码会被接受, 一条真的安全回退),先校验字节非 no-op,再要求红落在指名的 "rejects 7-char password" 上。 实测:test_files=69 executed_files=69 failed_files=0, MUTATION_RED registration-password-floor-weakened rc=1,RESULT: PASS,耗时 51s。 完整输出见 docs/tests/report-test798-server-unit-ci.txt。 --- .github/workflows/qa.yml | 12 ++ docs/tests/report-test798-server-unit-ci.txt | 29 +++++ tests/test798-server-unit-ci/Dockerfile | 44 +++++++ tests/test798-server-unit-ci/run.sh | 122 +++++++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 docs/tests/report-test798-server-unit-ci.txt create mode 100644 tests/test798-server-unit-ci/Dockerfile create mode 100755 tests/test798-server-unit-ci/run.sh diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index de59ed39d..37bf88e0f 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -22,6 +22,7 @@ on: - 'tests/test725-agent-node-unit-ci/**' - 'tests/test745-agent-network-unit-ci/**' - 'tests/test746-setup-bun-pin/**' + - 'tests/test798-server-unit-ci/**' push: branches: [main] paths: @@ -34,6 +35,7 @@ on: - 'tests/test725-agent-node-unit-ci/**' - 'tests/test745-agent-network-unit-ci/**' - 'tests/test746-setup-bun-pin/**' + - 'tests/test798-server-unit-ci/**' # Older runs on the same ref get cancelled — saves minutes when a PR is # updated rapidly. main pushes run independently. @@ -59,6 +61,16 @@ jobs: - name: Run complete agent-network unit domain run: docker run --rm anet-test745-agent-network-unit + - name: Build exact server unit image + run: | + docker build \ + --build-arg SOURCE_COMMIT="$GITHUB_SHA" \ + -t anet-test798-server-unit \ + -f tests/test798-server-unit-ci/Dockerfile . + + - name: Run complete server unit domain + run: docker run --rm anet-test798-server-unit + agent-node-unit: name: agent-node unit (Docker, non-root) runs-on: ubuntu-latest diff --git a/docs/tests/report-test798-server-unit-ci.txt b/docs/tests/report-test798-server-unit-ci.txt new file mode 100644 index 000000000..bce99d7c2 --- /dev/null +++ b/docs/tests/report-test798-server-unit-ci.txt @@ -0,0 +1,29 @@ +# test798 — server 聚合单测门 +source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 + +## 落地前的实测(为什么需要它,以及踩过的三层坑) +server/src 69 个测试,CI 此前只点名跑 6 个(L0_TESTS 5 + test686 引用 1)。 + +第一版(共享一个 DB、cwd=server/):895 pass / 6 fail。逐条查下来全不是产品坏: + - 5 条 harness:hub↔daemon 漂移门要读 agent-node/src/shared,镜像里没有 + - 1 条 harness:task-lifecycle-watcher 用 process.cwd() 拼 ./server/src/db.js,要求 cwd=仓根 + - 3 条 harness:scheduled-tasks 要 tests/test601-.../race-worker.ts + - 剩下的是共享 DB 造成的跨文件污染(单跑全绿,聚合红) + +第二版(按既有契约逐文件独立 DB):红的换成另外 2 个,与第一版不相交 —— +它们跨包 import agent-node 的 reply-reliability / inbox-dispatch,镜像里没带。 +补齐后 69/69 全绿。 + +## 最终输出 +``` +# test798 — complete server unit domain +source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +bun=1.3.14 node=v22.23.2 uid=1000 +commhub_db=/tmp/test798-server-unit.db +test_files=69 +[L0] every server/src unit file, one DB each, as non-root (cwd=repo root) +executed_files=69 discovered_files=69 failed_files=0 +[L1] witnessed-red: weaken the registration password floor +MUTATION_RED registration-password-floor-weakened rc=1 +RESULT: PASS +``` diff --git a/tests/test798-server-unit-ci/Dockerfile b/tests/test798-server-unit-ci/Dockerfile new file mode 100644 index 000000000..4e91a2a04 --- /dev/null +++ b/tests/test798-server-unit-ci/Dockerfile @@ -0,0 +1,44 @@ +ARG SOURCE_COMMIT +FROM node:22-bookworm-slim +ARG BUN_VERSION=1.3.14 +ARG BUN_LINUX_X64_SHA256=951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f + +RUN apt-get update \ + && apt-get install -y --no-install-recommends bash build-essential ca-certificates curl git python3 unzip util-linux \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --fail --silent --show-error --location \ + --retry 3 --retry-delay 2 --retry-all-errors \ + "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64.zip" \ + --output /tmp/bun-linux-x64.zip \ + && echo "${BUN_LINUX_X64_SHA256} /tmp/bun-linux-x64.zip" | sha256sum --check --strict \ + && unzip -j /tmp/bun-linux-x64.zip 'bun-linux-x64/bun' -d /usr/local/bin \ + && chmod 0755 /usr/local/bin/bun \ + && test "$(bun --version)" = "$BUN_VERSION" \ + && rm -f /tmp/bun-linux-x64.zip + +WORKDIR /workspace +COPY server/package.json ./server/ +RUN cd server && npm install + +COPY server ./server +# RFC-026 G9 / RFC-028 P1 的漂移门比对 hub 与 daemon 的同名共享源码, +# 要读同级 agent-node/src/shared。只带这个目录,不带整个包。 +# 若干 server 测试跨包 import agent-node 的源码(hub↔daemon 漂移门比对 +# shared/*.ts;peer-reply-atomic 用 reply-reliability;dashboard-slash-routing +# 用 inbox-dispatch)。带整个 src,不带它的依赖 —— 这些是纯源码 import。 +COPY agent-node/package.json ./agent-node/package.json +COPY agent-node/src ./agent-node/src +# scheduled-tasks-http.test.ts 起两个真 Hub 进程抢同一个 occurrence, +# worker 脚本在 tests/test601-hub-scheduled-tasks 下,按仓根相对路径 import。 +COPY tests/test601-hub-scheduled-tasks ./tests/test601-hub-scheduled-tasks +COPY tests/test798-server-unit-ci/run.sh ./tests/test798-server-unit-ci/run.sh + +ARG SOURCE_COMMIT +ENV TEST798_SOURCE_COMMIT=$SOURCE_COMMIT + +RUN chmod 0755 ./tests/test798-server-unit-ci/run.sh \ + && install -d -o node -g node -m 0700 "/run/user/$(id -u node)" \ + && chown -R node:node /workspace + +ENTRYPOINT ["bash", "tests/test798-server-unit-ci/run.sh"] diff --git a/tests/test798-server-unit-ci/run.sh b/tests/test798-server-unit-ci/run.sh new file mode 100755 index 000000000..33672258b --- /dev/null +++ b/tests/test798-server-unit-ci/run.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +# test798 — server 的聚合单测门 +# +# server/src 下有 69 个 *.test.ts,而在这之前 CI 只点名跑其中 6 个 +# (scripts/qa.sh 的 L0_TESTS 5 个 + test686 引用 1 个),另外 63 个没有任何 +# CI job 会碰。server 是 hub 本体 —— 认证、token、网络隔离都在这里。 +# +# 形状抄 tests/test745-agent-network-unit-ci。 + +ROOT=/workspace +SOURCE_COMMIT=${TEST798_SOURCE_COMMIT:-} +[[ "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { + echo "FAIL: SOURCE_COMMIT must be one full lowercase Git SHA" >&2 + exit 1 +} + +# 🔴 红线:COMMHUB_DB 不设的话默认指向生产库。容器里够不到宿主的库, +# 但不能靠"够不到"来保证 —— 显式钉到容器内临时路径,并断言它真的被钉住了。 +# 31/69 个 server 测试引用了 sqlite/COMMHUB_DB,这条不是形式主义。 +export COMMHUB_DB=/tmp/test798-server-unit.db +[[ "$COMMHUB_DB" == /tmp/* ]] || { + echo "FAIL: COMMHUB_DB must point inside the container tmpdir, got '$COMMHUB_DB'" >&2 + exit 1 +} + +echo "# test798 — complete server unit domain" +echo "source_commit=$SOURCE_COMMIT" +echo "bun=$(bun --version) node=$(node --version) uid=$(id -u node)" +echo "commhub_db=$COMMHUB_DB" + +test_files=$(find "$ROOT/server/src" -type f -name '*.test.ts' | wc -l | tr -d ' ') +echo "test_files=$test_files" +[[ "$test_files" -gt 0 ]] || { + echo "FAIL: server test-file denominator is empty" >&2 + exit 1 +} + +# 逐文件跑,每个文件一个独立 DB —— 这是 server 测试的既有契约: +# scripts/qa.sh 的 L0 就是 `COMMHUB_DB=/tmp/qa-l0-$name.db bun test `。 +# 用一个共享 DB 聚合跑会红 4 条(admin-networks 的 global-admin 可见性、 +# scheduled-tasks 的三条),而这 4 条单跑全绿 —— 是跨文件状态污染,不是产品坏。 +# 所以这道门按契约逐文件跑,而不是把"聚合能不能跑"这个它从没承诺过的性质当门。 +# +# cwd 必须是仓根:task-lifecycle-watcher 用 process.cwd() 拼 ./server/src/db.js, +# scheduled-tasks-http 按仓根相对路径 import tests/test601-.../race-worker.ts。 +echo "[L0] every server/src unit file, one DB each, as non-root (cwd=repo root)" +ran=0; failed=0; failed_names="" +while IFS= read -r f; do + rel=${f#"$ROOT"/} + name=$(basename "$f" .test.ts) + db="/tmp/test798-$name.db" + rm -f "$db" + if runuser -u node -- env HOME=/home/node COMMHUB_DB="$db" \ + bash -lc "cd $ROOT && bun test '$rel'" >"/tmp/test798-$name.log" 2>&1; then + ran=$((ran+1)) + else + ran=$((ran+1)); failed=$((failed+1)); failed_names="$failed_names $name" + echo "--- FAILED: $rel ---" + tail -25 "/tmp/test798-$name.log" + fi +done < <(find "$ROOT/server/src" -type f -name '*.test.ts' | sort) + +echo "executed_files=$ran discovered_files=$test_files failed_files=$failed" + +# 分母承重:跑过的文件数必须等于磁盘上的数。少一个都说明 find 的范围塌了, +# 而"跑了 2 个全绿"和"跑了 69 个全绿"打印出来是同一片绿色。 +[[ "$ran" -eq "$test_files" ]] || { + echo "FAIL: executed $ran file(s) but $test_files exist under server/src" >&2 + exit 1 +} +[[ "$failed" -eq 0 ]] || { + echo "FAIL: $failed file(s) failed:$failed_names" >&2 + exit 1 +} + +# --------------------------------------------------------------------------- +# witnessed-red:证明这道门真的在跑 server 的测试,而不是空转。 +# 靶点是注册时的密码下限 —— 把 `< 8` 改成 `< 1`,7 位密码就会被接受。 +# 这是一条真的安全回退,不是随手改个字符串。 +# --------------------------------------------------------------------------- +echo "[L1] witnessed-red: weaken the registration password floor" +TARGET='if (!password || password.length < 8) return `${label} must be at least 8 characters`;' +MUTATED='if (!password || password.length < 1) return `${label} must be at least 8 characters`;' +SRC="$ROOT/server/src/auth.ts" +before=$(sha256sum "$SRC" | cut -d' ' -f1) +python3 - "$SRC" "$TARGET" "$MUTATED" <<'__MUT__' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +source = path.read_text() +target, replacement = sys.argv[2], sys.argv[3] +if source.count(target) != 1: + raise SystemExit("mutation target count changed") +path.write_text(source.replace(target, replacement, 1)) +__MUT__ +after=$(sha256sum "$SRC" | cut -d' ' -f1) +[ "$before" != "$after" ] || { echo "FAIL: mutation was a byte no-op" >&2; exit 1; } + +rm -f /tmp/test798-mut.db +set +e +runuser -u node -- env HOME=/home/node COMMHUB_DB=/tmp/test798-mut.db \ + bash -lc "cd $ROOT && bun test server/src/auth-validate.test.ts" \ + >/tmp/test798-mutation.log 2>&1 +mutation_rc=$? +set -e +[ "$mutation_rc" -ne 0 ] || { + cat /tmp/test798-mutation.log + echo "FAIL: password-floor mutation survived" >&2 + exit 1 +} +# 红必须落在指名的那条行为上,而不是红在导入失败之类的别处。 +grep -Fq 'rejects 7-char password' /tmp/test798-mutation.log || { + cat /tmp/test798-mutation.log + echo "FAIL: mutation red did not reach the named password-floor assertion" >&2 + exit 1 +} + +echo "MUTATION_RED registration-password-floor-weakened rc=$mutation_rc" +echo "RESULT: PASS" From 46e752c3d9815a31902c5cc0a57b34deb898ffa2 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 08:31:29 +0800 Subject: [PATCH 02/11] =?UTF-8?q?ci:=20server=20=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=E9=97=A8=E6=8A=BD=E6=88=90=E7=8B=AC=E7=AB=8B=20job,=E5=88=AB?= =?UTF-8?q?=E6=8C=82=E5=9C=A8=20agent-network=20=E5=90=8D=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版把 build/run 两步插进了 agent-network-unit job 里,所以它确实跑了 (CI 日志实测 test_files=69 executed_files=69 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS), 但会以 "agent-network unit (Docker, non-root)" 的名义显示 —— server 挂了会归错帐,而且两个重 Docker build 串在一个 job 里。 抽成 server-unit job,显示名 "server unit (Docker, non-root)"。 --- .github/workflows/qa.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 37bf88e0f..5c159a600 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -61,6 +61,13 @@ jobs: - name: Run complete agent-network unit domain run: docker run --rm anet-test745-agent-network-unit + server-unit: + name: server unit (Docker, non-root) + runs-on: ubuntu-latest + timeout-minutes: 12 + steps: + - uses: actions/checkout@v4 + - name: Build exact server unit image run: | docker build \ From a4fd375f2b2e4f35e1a1dcea0a5f093f1439796a Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 08:28:22 +0800 Subject: [PATCH 03/11] =?UTF-8?q?test(ci):=20=E8=AE=A9=20test725/test745?= =?UTF-8?q?=20=E8=A6=86=E7=9B=96=20tests/=20=E7=9B=AE=E5=BD=95,=E5=85=91?= =?UTF-8?q?=E7=8E=B0"complete=20unit=20domain"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两个门的抬头都写着 "complete agent-node/agent-network unit domain", 但只跑 src/,把 tests/ 下 25 个文件排除在外 —— 其中不乏安全相关的: feishu-markdown-image-ssrf、secret-mask ×3、vendor-error-sanitize、feishu-tool-deny。 这些正是静默失效代价最高的那类。 这个目录里混着两种测试,任何单一命令都跑不全: - 脚本式(16+6 个):自己打 "N/N passed",失败 process.exit(1),必须 bun ; 用 bun test 跑会因为 top-level 的 process.exit 把整个 run 打断在第一个文件 (实测:bun test tests/ 只跑完第一个就结束)。 - bun:test 式(3 个):describe/it,必须 bun test ;用 bun 跑会报 "Cannot use describe outside of the test runner"。 所以按文件内容分派,并把两条判据都写进注释。 退出码可用已先验:这些脚本失败时确实 process.exit(1),不是 fail-open。 落地前实测: agent-node/tests 6/6 直接过 agent-network/tests 单命令 14/19 → 按内容分派 17/19 → 补两处环境契约 19/19 两处契约都在 Dockerfile 内解决,并写明原因: - feishu-envelope-compat 跨包 import agent-node/src/runtime/feishu-envelope - feishu-bridge-ipc 硬编码绝对路径 /work/feishu-attachments,容器里 node 建不了 分母承重:tests_dir_executed 必须等于 find 出来的数,且 >0。 实测:test725 tests_dir 6/6/0 + MUTATION_RED + PASS; test745 tests_dir 19/19/0 + MUTATION_RED + PASS。 --- docs/tests/report-pkg-tests-dir-gate.txt | 2091 +++++++++++++++++ tests/test725-agent-node-unit-ci/run.sh | 37 + .../test745-agent-network-unit-ci/Dockerfile | 7 +- tests/test745-agent-network-unit-ci/run.sh | 37 + 4 files changed, 2171 insertions(+), 1 deletion(-) create mode 100644 docs/tests/report-pkg-tests-dir-gate.txt diff --git a/docs/tests/report-pkg-tests-dir-gate.txt b/docs/tests/report-pkg-tests-dir-gate.txt new file mode 100644 index 000000000..5f21938e0 --- /dev/null +++ b/docs/tests/report-pkg-tests-dir-gate.txt @@ -0,0 +1,2091 @@ +# test725/test745 扩到 tests/ 目录 +source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 + +## 为什么 +两个门的抬头都写着 complete X unit domain,却把 tests/ 下 25 个文件排除在外。 + +## 这个目录的形状(混着两种测试,任何单一命令都跑不全) + agent-network/tests 19 个:bun:test 式 3 + 脚本式 16 + agent-node/tests 6 个:全是脚本式 + 脚本式用 bun test 跑 → top-level process.exit 把整个 run 打断在第一个文件 + bun:test 式用 bun 跑 → Cannot use describe outside of the test runner + +## 落地前实测 + agent-node/tests:6/6 直接过 + agent-network/tests:单命令跑 14/19;按内容分派后 17/19;补两处环境契约后 19/19 + - feishu-envelope-compat 跨包 import agent-node/src/runtime/feishu-envelope(镜像没带) + - feishu-bridge-ipc 硬编码绝对路径 /work/feishu-attachments(容器里 node 建不了 → EACCES) + +## 最终输出 +### test725 +``` +# test725 — complete agent-node unit domain +source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +bun=1.3.14 node=v22.23.2 uid=1000 +[L0] full agent-node/src unit suite as non-root +bun test v1.3.14 (0d9b296a) + +src/inbox-message-policy.test.ts: +(pass) atomic peer reply inbox policy > ordinary work still expects a response [0.21ms] +(pass) atomic peer reply inbox policy > a peer reply is actionable but cannot start reply ping-pong [0.05ms] +(pass) atomic peer reply inbox policy > plain informational messages retain ack-only behavior [0.05ms] + +src/external-schedules.test.ts: +(pass) external schedule manifest > reports an exact bounded shape and strips host paths to basename [1.18ms] +(pass) external schedule manifest > missing manifest is an explicit empty observation; config-less legacy stays omitted [1.01ms] +(pass) external schedule manifest > unknown keys, duplicate ids, invalid timestamps, and oversized lists fail closed [0.72ms] +(pass) external schedule manifest > symlink manifest never follows the target [0.65ms] +(pass) external schedule manifest > editable/revision are derived only from a verified managed crontab under the process gate [2.66ms] + +src/inbox-skip-log.test.ts: +(pass) formatInboxSkipLog > self-message diagnostics identify the routing layer and full task [0.09ms] +(pass) formatInboxSkipLog > all inbound filter reasons produce an actionable INFO-safe line [0.13ms] +(pass) formatInboxSkipLog > the formatter has no message-content input [0.05ms] + +src/owner-schedule-consumer.test.ts: +(pass) process-gated owner schedule consumer > disabled process registers no poll and makes zero network/host calls [0.83ms] +(pass) process-gated owner schedule consumer > exact node intent applies once, ACKs, and deletes journal only after ACK [8.13ms] +(pass) process-gated owner schedule consumer > foreign-node intent and invalid authority shape never reach crontab [0.73ms] +(pass) process-gated owner schedule consumer > lost ACK keeps journal; same delivered intent recovers without a second install [6.01ms] + +src/codex-model-default.test.ts: +(pass) agent-node Codex model resolution > missing model uses the verified supported default [0.05ms] +(pass) agent-node Codex model resolution > explicit model remains authoritative [0.02ms] + +src/claude-tool-aliases.test.ts: +(pass) Claude CommHub tool aliases > pins the exact registered in-process CommHub tool set [0.09ms] +(pass) Claude CommHub tool aliases > does not advertise aliases when the in-process server failed [0.05ms] + +src/reply-reliability.test.ts: +(pass) classifyCommHubResponse > returns ok with parsed application payload (the happy path) [0.26ms] +(pass) classifyCommHubResponse > JSON-RPC error envelope → retryable CommHubError [0.16ms] +(pass) classifyCommHubResponse > MCP result.isError → retryable CommHubError [0.20ms] +(pass) classifyCommHubResponse > real legacy Hub unknown-tool result preserves the MCP code [0.10ms] +(pass) classifyCommHubResponse > application-level ok:false → appLevel CommHubError (NON-retryable) [0.14ms] +(pass) classifyCommHubResponse > non-JSON tool text is passed through verbatim [0.20ms] +(pass) classifyCommHubResponse > data with neither error nor result returns ok with the raw data [0.06ms] +(pass) CommHubError > instances are distinguishable from generic Error via instanceof [0.07ms] +(pass) CommHubError > appLevel flag survives the throw/catch round trip [0.10ms] +(pass) PendingReplyQueue > load() returns empty array when file does not exist [0.65ms] +(pass) PendingReplyQueue > persist + load round-trips an entry with attempts=0 [4.01ms] +(pass) PendingReplyQueue > final persistence boundary scrubs known, shaped, assignment and error credentials [3.34ms] +(pass) PendingReplyQueue > direct save cannot bypass scrub and leaves no sibling temp artifact [2.51ms] +(pass) PendingReplyQueue > load migrates an old broad-mode queue without leaving raw credential bytes [3.30ms] +(pass) PendingReplyQueue > load repairs a broad mode even when content needs no rewrite [0.70ms] +(pass) PendingReplyQueue > accepts the same process-wide redactor used by ordinary log call sites [2.29ms] +(pass) PendingReplyQueue > invalid legacy content is securely replaced with an empty 0600 queue [2.40ms] +(pass) PendingReplyQueue > persist is idempotent on (to, taskId) — attempts counter preserved [5.76ms] +(pass) PendingReplyQueue > clear removes only the matching (to, taskId) [8.57ms] +(pass) PendingReplyQueue.drain > delivers every entry on success and persists an empty queue [6.96ms] +(pass) PendingReplyQueue.drain > transient failure requeues with attempts++ and lastError [4.97ms] +(pass) PendingReplyQueue.drain > transient error text is scrubbed before it reaches disk [4.57ms] +(pass) PendingReplyQueue.drain > app-level CommHubError is dropped loud — not retried, not requeued [6.19ms] +(pass) PendingReplyQueue.drain > drain on empty queue is a no-op and does not write the file [0.53ms] +(pass) PendingReplyQueue.drain > file format is stable JSON — readable by an operator after a crash [2.15ms] +(pass) quickHash > is deterministic [0.27ms] +(pass) quickHash > differs across inputs [0.07ms] +(pass) quickHash > returns 32-char hex [0.10ms] + +src/controlled-upload.test.ts: +(pass) normalizeUploadName > strips directories and control chars [0.78ms] +(pass) resolveControlledUploadPath — NUL live guard > rejects embedded NUL before any fs access [0.71ms] +(pass) resolveControlledUploadPath — NUL live guard > rejects NUL-only / leading NUL [0.47ms] +(pass) resolveControlledUploadPath > accepts regular file under root [0.83ms] +(pass) resolveControlledUploadPath > rejects path outside roots [0.55ms] +(pass) resolveControlledUploadPath > rejects absolute foreign path /etc/passwd [0.44ms] +(pass) resolveControlledUploadPath > rejects traversal that escapes root [0.49ms] +(pass) resolveControlledUploadPath > rejects missing path [0.50ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > reads small PNG via same-fd path [1.44ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > rejects oversize without allocating full max+1 into a single slurp beyond cap [15.00ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > rejects symlink leaf at open (O_NOFOLLOW) [1.14ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > fstat is on the same opened fd (structural pin) [0.55ms] +(pass) uploadControlledLocalFile > uploads PNG fixture via mock fetch and returns file_id [2.61ms] +(pass) uploadControlledLocalFile > refuses oversize before network [15.99ms] +(pass) uploadControlledLocalFile > never falls back to path when file_id missing [1.21ms] +(pass) uploadControlledLocalFile > rejects untrusted path without calling hub [0.65ms] +(pass) uploadControlledLocalFile > rejects NUL path without calling hub [0.43ms] +(pass) defaultControlledUploadRoots > includes grok sessions and attachment cache [0.64ms] +(pass) source contracts (adversarial pins) > same-fd pin: fstatSync(fd) + openSync; no path re-stat/readFileSync in reader [0.35ms] +(pass) source contracts (adversarial pins) > NUL guard pin: rawPath.includes NUL marker present [0.28ms] +(pass) source contracts (adversarial pins) > bounded-read pin: extra-byte probe after maxBytes [0.35ms] + +src/commhub-mcp.test.ts: +(pass) injectAgentFromSession > adds current alias to outbound task calls [0.17ms] +(pass) injectAgentFromSession > adds current alias to outbound message calls [0.08ms] +(pass) injectAgentFromSession > overrides stale or model-supplied from_session on ntok outbound calls [0.05ms] +(pass) injectAgentFromSession > does not add from_session to read-only calls [0.04ms] + +src/inbox-dispatch.test.ts: +(pass) isInteractiveDashboardTask > accepts a Hub-authenticated dashboard chat task [0.35ms] +(pass) isInteractiveDashboardTask > pre-stamp admin rows stay FIFO because aliases are not auth facts [0.13ms] +(pass) isInteractiveDashboardTask > rejects node-authenticated spoofing, malformed ids, and plain messages [0.08ms] +(pass) dispatchInboxBatch > awaited batches preserve legacy runtime serialization [1.85ms] +(pass) dispatchInboxBatch > a later SSE snapshot enters while the first detached turn is still running [2.14ms] +(pass) dispatchInboxBatch > the real serialized drain lane can fetch a later SSE snapshot before the active turn ends [1.37ms] +(pass) dispatchInboxBatch > detached completion failures remain observable [1.41ms] +(pass) dispatchInboxBatch > settling detached work emits a wake for the next Hub inbox window [1.31ms] +(pass) dispatchInboxBatch > a throwing settle callback cannot strand queued N+1 work [1.50ms] +(pass) dispatchInboxBatch > same-tick duplicate kicks claim one row exactly once [0.54ms] +(pass) dispatchInboxBatch > bounded admission waits N+1 and starts it after a slot settles [1.82ms] +(pass) dispatchInboxBatch > durable reply drain waits until detached Codex rows finish [0.06ms] +(pass) dispatchInboxBatch > active Codex direct delivery and durable drain send one reply, not two [6.23ms] + +src/reply-routing-source.test.ts: +(pass) #698 peer reply runtime wiring > peer replies negotiate the atomic tool and retain only a terminal legacy fallback [1.02ms] +(pass) #698 peer reply runtime wiring > every actionable inbox turn crosses the behavior-tested reply-policy seam [0.63ms] +(pass) #698 peer reply runtime wiring > new_reply SSE events wake the actionable work inbox [0.32ms] + +src/task-runtime-evidence.test.ts: +(pass) logicalTaskIdFromInbox > retry/reassign task rows use stable task_id, not fresh inbox.id [0.09ms] +(pass) logicalTaskIdFromInbox > legacy task rows and non-task rows retain transport identity [0.05ms] +(pass) createTaskRuntimeEvidenceReporter > construction and process admission report no evidence [0.21ms] +(pass) createTaskRuntimeEvidenceReporter > submission and many runtime events produce one exact report per level [0.33ms] +(pass) createTaskRuntimeEvidenceReporter > a consumed-only runtime remains honest and lets the Hub imply submission [0.19ms] +(pass) createTaskRuntimeEvidenceReporter > missing logical task identity is a fail-closed no-op [0.13ms] +(pass) createTaskRuntimeEvidenceReporter > an old-Hub failure is visible but never breaks the model turn [0.37ms] +(pass) agent-node inbox wiring > keeps transport ACK separate from stable task evidence and replies [2.89ms] +(pass) agent-node inbox wiring > all runtime dispatch families receive the same task-lifetime reporter [0.96ms] +(pass) agent-node inbox wiring > SDK and direct-stdio boundaries preserve their distinct evidence semantics [1.26ms] + +src/grok-isolated-cwd.test.ts: +(pass) prepareGrokIsolatedCwd (#204 preview.7) > creates per-node grok-cwd directory under home/.anet/nodes//grok-cwd [1.75ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to alias when nodeId is absent [1.03ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > sanitises nodeKey to avoid path traversal / weird chars [1.55ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > skips .mcp.json (does NOT symlink it into isolated cwd) [1.04ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > symlinks top-level files (README.md) and directories (docs/, src/) [1.27ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > is idempotent — second run sees existing symlinks and counts 0 new [1.11ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > picks up new entries on re-run (snapshot freshness) [2.01ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to userCwd (isolated=false) when mkdir fails [1.26ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to userCwd when userCwd does not exist (readdir fails) [1.64ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > does NOT throw on per-entry symlink failure — warns and continues [2.11ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > two different nodes get fully isolated dirs (concurrency safe by construction) [2.12ms] + +src/inbox-dispatch-wiring.test.ts: +(pass) Codex app-server live inbox kick wiring > a Codex snapshot releases the serialized fetch lane after submission [0.22ms] +(pass) Codex app-server live inbox kick wiring > Codex detached admission is explicitly bounded and completion wakes the Hub window [0.38ms] +(pass) Codex app-server live inbox kick wiring > pending reply drain is fenced while detached Codex rows are active [0.15ms] + +src/owner-schedule-control.test.ts: +(pass) owner schedule managed-cron control > parses only exact managed markers and publishes bounded inventory [0.98ms] +(pass) owner schedule managed-cron control > changes timing/enabled while preserving command and unmanaged bytes [4.50ms] +(pass) owner schedule managed-cron control > command replacement, wrong node, wrong revision, and unknown patch fail before install [1.84ms] +(pass) owner schedule managed-cron control > install/readback failure restores and verifies the exact old crontab [2.46ms] +(pass) owner schedule managed-cron control > unsafe node directory and symlink journal fail closed with zero host write [0.96ms] +(pass) owner schedule managed-cron control > local audit is minimal, private and idempotent [2.24ms] + +src/owner-schedule-wiring.test.ts: +(pass) owner schedule process wiring > capability is pinned from config once and never exposed as a model tool [1.15ms] +(pass) owner schedule process wiring > SSE is only a doorbell and snapshots are editable only under the same gate [0.80ms] +(pass) owner schedule process wiring > new token mint paths bind the immutable node id and opt-in is explicit [2.70ms] + +src/runtime-effective-label.test.ts: +(pass) #491 startup banner reports the EFFECTIVE runtime > alias input 'codex-tui' → banner names the effective runtime (codex-app-server), not just the raw input [7178.00ms] +(pass) #491 startup banner reports the EFFECTIVE runtime > canonical input stays readable (no regression for the common case) [7150.08ms] +(pass) #491 regression lock — unknown runtime fails closed > unknown runtime → non-zero exit, error names the value AND the supported list [128.93ms] +(pass) #553 Grok startup banner reports model ownership truthfully > unset model on Grok ACP names the Grok CLI as owner, not the runtime alias as a model id [7149.05ms] +(pass) #553 Grok startup banner reports model ownership truthfully > unset model on Grok CLI uses the same non-versioned ownership statement [7142.68ms] +(pass) #553 Grok startup banner reports model ownership truthfully > an explicit Grok model is still reported exactly [7134.50ms] + +src/peer-reply-send.test.ts: +(pass) peer reply capability fallback > capable Hub uses only the atomic terminal route [0.46ms] +(pass) peer reply capability fallback > old Hub wire error terminalizes through send_reply, never send_task [0.53ms] +(pass) peer reply capability fallback > every explicit capability downgrade preserves terminal reply semantics [0.49ms] +(pass) peer reply capability fallback > transport ambiguity and unrelated hard errors never choose a second route [0.37ms] +(pass) peer reply capability fallback > negative capability is rechecked instead of cached [0.35ms] +(pass) peer reply capability fallback > legacy terminalization failure stays visible to the pending queue [0.22ms] +(pass) peer reply capability fallback > classifier accepts only explicit capability signals [0.09ms] + +src/private-log.test.ts: +(pass) Grok preview private ordinary logs > scrubs and repairs legacy logs before appending through a 0600 file [4.30ms] +(pass) Grok preview private ordinary logs > rejects a symlinked directory or final log file [1.26ms] +(pass) Grok preview private ordinary logs > rejects a multiply-linked log instead of rewriting another pathname [0.59ms] +(pass) Grok preview private ordinary logs > does not follow a log-directory symlink introduced after preparation [0.57ms] + +src/owner-schedule-system-crontab.test.ts: +(pass) owner schedule real crontab adapter > round-trips an exact managed marker through the container crontab [26.36ms] + +src/credential-redaction.test.ts: +(pass) credential persistence redactor > removes exact caller-known values regardless of punctuation or context [0.26ms] +(pass) credential persistence redactor > redacts network, GitHub, AWS and provider token shapes in free text [0.20ms] +(pass) credential persistence redactor > redacts credential assignments while preserving keys and valid JSON [0.30ms] +(pass) credential persistence redactor > redacts shell/error assignment forms including quoted values [0.16ms] +(pass) credential persistence redactor > redacts an unlabelled connection URI with embedded userinfo [0.07ms] +(pass) credential persistence redactor > does not over-delete normal prose and non-credential settings [0.08ms] +(pass) credential persistence redactor > deep-redacts JSON-like values without mutating the input [0.31ms] +(pass) credential value collection > collects exact sensitive values and shaped values under unknown keys [1.06ms] +(pass) credential value collection > key classifier is exact enough not to treat ordinary AWS settings as credentials [0.10ms] + +src/inbox-skip-log-wiring.test.ts: +(pass) processInbox logs skipped messages at INFO before acknowledging [1.60ms] + +src/peer-reply-inbox.test.ts: +(pass) inbox turn reply-policy enforcement > delivers once, ACKs once, and exposes no outbound reply dependency [0.41ms] +(pass) inbox turn reply-policy enforcement > ordinary request returns its outcome without ACKing in this seam [0.26ms] +(pass) inbox turn reply-policy enforcement > runtime failure does not ACK a result that was never consumed [0.29ms] +(pass) peer reply SSE routing > new_reply schedules exactly one drain [0.11ms] +(pass) peer reply SSE routing > unrelated events do not schedule a drain [0.05ms] + +src/grok-artifact-extractor.test.ts: +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > returns empty when grokSessionDir is undefined [0.47ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > returns empty when videos/ subdir is missing [0.27ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > enumerates .mp4 files in videos/ as absolute paths [0.73ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > matches mp4 case-insensitively [0.57ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > does not throw on permission errors — returns [] [0.41ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > returns empty string for empty list [0.13ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > formats one path [0.09ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > formats multiple paths [0.10ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > skips paths already mentioned in existingReply (no duplication) [0.03ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > only appends paths NOT already mentioned, even when some are [0.07ms] + +src/explicit-task-lifecycle.test.ts: +(pass) explicit delegation lifecycle trace > keeps the production delegation loop wired through the tested state machine [1.16ms] +(pass) explicit delegation lifecycle trace > emits ack, start, and reply from the production polling state machine [0.88ms] +(pass) explicit delegation lifecycle trace > emits both bounded stale warnings and expiry when delivery never advances [0.31ms] +(pass) explicit delegation lifecycle trace > pins the production poll, stale-warning, and timeout defaults [0.62ms] +(pass) explicit delegation lifecycle trace > maps failed and cancelled terminal states to a failed trace without retrying [0.37ms] + +src/task-trace.test.ts: +(pass) task trace contract > renders missing parent and lifecycle scope honestly [0.27ms] +(pass) task trace contract > redacts credentials from errors [0.14ms] +(pass) task trace contract > emits parseable JSON and neutralizes human log injection [0.13ms] +(pass) task trace contract > recognizes the real MCP content envelope before cli parsing [0.58ms] +(pass) task trace contract > uses stable event names for send and observed lifecycle phases [0.10ms] + +src/sse-recovery-guidance.test.ts: +(pass) sseAbandonGuidance > states that abandon leaves the current process alive [0.09ms] +(pass) sseAbandonGuidance > requires stop-and-replace instead of starting a duplicate [0.05ms] +(pass) sseAbandonGuidance > preserves the co-presence launch shape in recovery guidance [0.05ms] +(pass) sseAbandonGuidance > the production SSE abandon hook uses the honest guidance [1.06ms] + +src/cli-explicit-delegation.test.ts: +(pass) extractExplicitDelegation > matches send_task alias/task call [0.68ms] +(pass) extractExplicitDelegation > matches mcp send_task positional call [0.12ms] +(pass) extractExplicitDelegation > matches 给 X 发任务 [0.13ms] +(pass) extractExplicitDelegation > matches 和 X 沟通一下 [0.19ms] +(pass) extractExplicitDelegation > matches bare 和 X 沟通一下 [0.12ms] +(pass) extractExplicitDelegation > matches 和 X send_task 一下 [0.06ms] +(pass) extractExplicitDelegation > matches 和 X send_task 一下 with no punctuation before body [0.09ms] +(pass) extractExplicitDelegation > matches bare 和 X send_task 一下 [0.06ms] +(pass) extractExplicitDelegation > matches 让 X 做 [0.10ms] +(pass) extractExplicitDelegation > matches 交给 X [0.05ms] +(pass) extractExplicitDelegation > does not match no alias [0.03ms] +(pass) extractExplicitDelegation > does not match normal Q&A [0.04ms] +(pass) extractExplicitDelegation > matches bare send_task (MCP-like) [0.04ms] +(pass) extractExplicitDelegation > matches bare send_task with multi-word task body [0.04ms] +(pass) extractExplicitDelegation > matches 你去给 X 打个招呼 [0.06ms] +(pass) extractExplicitDelegation > matches 你去给 X with longer body [0.05ms] +(pass) extractExplicitDelegation > matches 给 X 发个消息 BODY (verb-suffix stripped) [0.04ms] +(pass) extractExplicitDelegation > matches 给 X 发 BODY (bare verb) [0.06ms] +(pass) extractExplicitDelegation > matches 给 X 沟通一下 BODY [0.07ms] +(pass) extractExplicitDelegation > matches 给 X 说 BODY [0.05ms] +(pass) extractExplicitDelegation > matches 给 X 发任务 (regression — specific pattern still wins) [0.07ms] + +src/util/timeout.test.ts: +(pass) withTimeout — happy path (factory wins) > resolves with factory value when fn settles before deadline [0.51ms] +(pass) withTimeout — happy path (factory wins) > passes a non-aborted signal when fn finishes promptly [0.17ms] +(pass) withTimeout — happy path (factory wins) > returns objects, not just strings [0.12ms] +(pass) withTimeout — happy path (factory wins) > propagates fn's rejection unchanged (not wrapped) [0.24ms] +(pass) withTimeout — timeout path (timer wins) > rejects with TimeoutError when fn outlasts deadline [31.93ms] +(pass) withTimeout — timeout path (timer wins) > TimeoutError message includes label + ms [0.06ms] +(pass) withTimeout — timeout path (timer wins) > TimeoutError without label still works [0.05ms] +(pass) withTimeout — timeout path (timer wins) > fires AbortSignal on timeout so factory can cancel in-flight work [43.82ms] +(pass) withTimeout — zero / negative deadline sentinel > timeoutMs=0 disables the timer (CLAUDE_TIMEOUT_MS=0 sentinel) [51.64ms] +(pass) withTimeout — zero / negative deadline sentinel > timeoutMs<0 also disables (defensive) [0.48ms] +(pass) withTimeout — zero / negative deadline sentinel > untimed call still receives a non-aborted signal [0.19ms] +(pass) withTimeout — externalSignal propagation > forwards external abort into factory signal [212.14ms] +(pass) withTimeout — externalSignal propagation > already-aborted external signal aborts immediately [0.59ms] +(pass) withTimeout — cleanup > clears timer on successful return (no dangling handles) [21.89ms] +(pass) resolveTimeoutMs — precedence > env wins over flag and default [0.34ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is missing [0.04ms] +(pass) resolveTimeoutMs — precedence > default wins when env and flag both missing [0.11ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is empty string (treated as unset) [0.05ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is non-numeric garbage [0.05ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is negative [0.06ms] +(pass) resolveTimeoutMs — precedence > default wins when flag is NaN [0.07ms] +(pass) resolveTimeoutMs — precedence > zero is honoured (not treated as unset) — env=0 disables timeout [0.10ms] +(pass) resolveTimeoutMs — precedence > zero is honoured at flag level too [0.06ms] +(pass) resolveTimeoutMs — clamping > clamps below minMs and reports clamped=true [0.07ms] +(pass) resolveTimeoutMs — clamping > clamps above maxMs and reports clamped=true [0.05ms] +(pass) resolveTimeoutMs — clamping > in-bounds value is not clamped [0.05ms] +(pass) resolveTimeoutMs — clamping > default value also gets clamped (configuration sanity) [0.05ms] +(pass) resolveTimeoutMs — defensive null handling > null envValue is treated as unset [0.04ms] +(pass) resolveTimeoutMs — defensive null handling > null flagValue is treated as unset [0.04ms] + +src/util/single-flight.test.ts: +(pass) single-flight resource initialization > concurrent callers share exactly one initializer [0.75ms] +(pass) single-flight resource initialization > a rejected initializer is cleared and can be retried [0.39ms] + +src/util/supervise-child.test.ts: +(pass) superviseChild — shutdown gate stops the loop > shutdownGate=true from the start → runOnce never called [0.70ms] +(pass) superviseChild — shutdown gate stops the loop > shutdownGate flips true after first iteration → exactly one runOnce [0.31ms] +(pass) superviseChild — backoff growth + cap > waits double the delay each iteration, capping at maxDelayMs [2.93ms] +(pass) superviseChild — runOnce that returns WITHOUT markStable is treated as failed (regression pin) > runOnce that returns cleanly without markStable → backoff doubles [0.63ms] +(pass) superviseChild — markStable resets backoff > after iteration that calls markStable, next wait is baseDelayMs again [0.54ms] +(pass) superviseChild — markStable resets backoff > markStable called multiple times in one iteration is idempotent [0.56ms] +(pass) superviseChild — abandonAfterMs > calls onAbandon and returns after cumulative downtime exceeds threshold [0.51ms] +(pass) superviseChild — abandonAfterMs > markStable in any iteration resets downtime — abandon never fires [0.49ms] +(pass) superviseChild — runOnce error handling > runOnce throws → onError fires, loop continues [0.68ms] +(pass) superviseChild — runOnce error handling > runOnce throws AND shutdownGate goes true → loop exits, no further iteration [0.27ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=0 → -25% of delay (lower bound) [0.48ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=1 → +25% of delay (upper bound) [0.42ms] +(pass) superviseChild — jitter range > jitterRatio=0 → deterministic waits at exact delay [0.36ms] +(pass) superviseChild — jitter range > waitMs floor 100 enforces minimum wait even with tiny base + negative jitter [0.45ms] +(pass) superviseChild — defensive contract > returns (does not throw) when runOnce never resolves and shutdown flips [1.39ms] + +src/util/access-resolve.test.ts: +(pass) normalizeAllowFrom — input shapes > real string[] passes through deduped (filter empty strings) [0.22ms] +(pass) normalizeAllowFrom — input shapes > undefined → empty + not malformed [0.04ms] +(pass) normalizeAllowFrom — input shapes > null → empty + not malformed [0.03ms] +(pass) normalizeAllowFrom — input shapes > non-array object → empty + malformed (corrupted access.json shape) [0.05ms] +(pass) normalizeAllowFrom — input shapes > string instead of array → malformed [0.03ms] +(pass) normalizeAllowFrom — input shapes > array with non-string elements drops them [0.08ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > empty array → deny with empty-fail-closed kind [0.20ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > undefined → deny [0.05ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > malformed → deny + reason mentions malformed [0.12ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*'] alone allows any sender [0.06ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*', 'specific_id'] still wildcard-allows (wins precedence) [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderId in list → allow [0.07ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderUsername match (no id match) → allow [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > neither id nor username in list → deny [0.06ms] +(pass) resolveTelegramAccess — explicit id / username matching > empty senderUsername doesn't accidentally match empty list entry [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > blank-string id with username match still allows [0.05ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > empty allowFrom → deny [0.27ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > wildcard allows [0.08ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > specific id allows [0.08ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > sender not in list → deny [0.05ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > empty allowChats → fail-closed [0.08ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=all → allow [0.06ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=observe → deny [0.05ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat NOT in allowChats → deny (even with policy=all) [0.08ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > wildcard chats opens any chat (with groupPolicy=all) [0.04ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > groupPolicy=mention allows (caller decides at message inspect time) [0.04ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for empty allowFrom [0.16ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for malformed allowFrom + mentions malformed [0.04ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null when allowFrom has at least one entry [0.04ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null for wildcard-allow (channel intentionally open) [0.03ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader stores raw allowFrom verbatim — no normalization at load time [0.09ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is missing [0.05ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is malformed (non-array) [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader is silent when allowFrom has at least one entry (even if numeric) [0.09ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123] alone (numeric sender id from a misformatted access.json) → loader+resolver fail-closed [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null] (corrupted access.json) → loader+resolver fail-closed [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [{}] (object instead of id string) → loader+resolver fail-closed [0.08ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123, '@vansin'] (mixed) → '@vansin' still allowed, numeric '123' rejected [0.09ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null, '*'] (mixed wildcard) → wildcard wins despite garbage entries [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > missing access.json entirely (loader gets null) → fail-closed [0.06ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > empty array NEVER allows [0.04ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > undefined NEVER allows [0.03ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > null NEVER allows [0.04ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > object-shape (corrupted) NEVER allows [0.03ms] + +src/runtime/fetch-attachment.test.ts: +(pass) FILE_ID_REGEX matches server contract > accepts the same shapes the hub accepts [0.21ms] +(pass) FILE_ID_REGEX matches server contract > rejects path-traversal + length-out-of-range [0.09ms] +(pass) resolveAttachmentToLocalPath — file_id path > hub 200 OK → bytes written to cache + chmod 600 + Bearer auth attached [9.30ms] +(pass) resolveAttachmentToLocalPath — file_id path > file_id_invalid before any HTTP call (path traversal attempt) [0.47ms] +(pass) resolveAttachmentToLocalPath — file_id path > hub 404 → not_found code [0.53ms] +(pass) resolveAttachmentToLocalPath — file_id path > hub 401 → auth_failed code [0.47ms] +(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > Content-Length > cap → size_exceeded with declared-and-cap surfaced + no cache file written [0.62ms] +(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > Content-Length lies (says small, sends big) → size_exceeded MID-STREAM with cleanup [1.47ms] +(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > DEFAULT_MAX_BYTES is 50 MiB unless COMMHUB_ATTACHMENT_MAX_BYTES is set (current process is unset → 50 MiB) [0.05ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id + path inside cache root → returns canonical path, no HTTP call [0.80ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > configured Feishu root remains a compatible trusted drop-zone [0.62ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > existing file outside trusted roots is rejected [0.58ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > symlink inside a trusted root cannot escape to another host file [0.56ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id + path does NOT exist → not_found error [0.51ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id AND no path → no_file_id_no_path error [0.35ms] +(pass) resolveAttachmentToLocalPath — cache hit > same file_id + same size → no HTTP call, returns cached:true [0.54ms] +(pass) resolveAttachmentToLocalPath — cache hit > same file_id + different size → cache miss, re-fetches [4.42ms] +(pass) sweepAttachmentCacheOnce > purges files older than TTL, keeps fresh [0.89ms] +(pass) sweepAttachmentCacheOnce > no-op when cache dir doesn't exist [0.26ms] + +src/runtime/readable-attachment-prompt.test.ts: +(pass) readable attachment prompt > pins the exact runtime set without changing structured-image SDK lanes [0.17ms] +(pass) readable attachment prompt > pins the readable extension allowlist as an exact value set [0.35ms] +(pass) readable attachment prompt > injects absolute deduplicated paths and escapes control characters [0.24ms] +(pass) readable attachment prompt > leaves text byte-identical when no attachment resolved [0.05ms] +(pass) readable attachment prompt > path-prompt runtimes reject sender-local paths while structured lanes retain legacy behavior [0.18ms] +(pass) readable attachment prompt > the inbox choke point feeds the augmented text into processTask [2.38ms] + +src/runtime/create-node-daemon.test.ts: +(pass) #633 daemon private state > global config repair and replacement converge to private state [3.74ms] +(pass) #633 daemon private state > global config read refuses a symlink without touching its target [0.90ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > permissionMode enum [0.40ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > dangerouslySkipPermissions boolean (string 'true' must be rejected) [0.15ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > maxTurns integer range — 'DROP TABLE' / float / out-of-range rejected [0.25ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > budget number with decimals allowed; out-of-range rejected [0.23ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > timeout integer range [0.15ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > unknown key rejected [0.07ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > happy path with mixed flags [0.48ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > smuggled string maxTurns rejected by daemon even if hub missed [0.41ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > smuggled string dangerouslySkipPermissions rejected [0.10ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > name shell-metachar still rejected (existing validateName, F2) [0.11ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > runtime enum still enforced [0.09ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > channels non-empty rejected (P1 fail-closed) [0.10ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > happy path with hash witness [1.04ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: no ANET_BIN_ABS at all [0.16ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: relative path [0.16ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: symlink (contains symlink component) [0.57ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: world-writable (mode 0o777) [0.48ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: group-writable (mode 0o775) [0.42ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: not executable (mode 0o644) [0.45ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: owner not root (no opt-out) [0.48ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > ACCEPT: owner not root WHEN ANET_DAEMON_ALLOW_NON_ROOT_BIN=1 (explicit opt-out) [0.37ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: sha256 mismatch with install witness [0.55ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > happy path: no extra → PATH includes daemon's own node bin dir + SAFE_PATH (issue #301 nvm fix) [0.40ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > legitimate extra key passes + fixed PATH keeps execPath prepend (issue #301) [0.22ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > THROWS on reserved key in extra (LD_PRELOAD smuggled by attacker) [0.22ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > THROWS on fixed key in extra (PATH smuggled — caller cannot override the trust-root execPath prepend) [0.11ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > C1 invariant — issue #301 fix does NOT widen attacker surface: PATH source is process.execPath (daemon's already-resolved node), NOT env.PATH (attacker C1 surface) [0.30ms] +(pass) FAIL_FAST_MS primitive — real subprocess kill-0 lifecycle > child that exits within window → process.kill(pid, 0) raises ESRCH after wait [502.11ms] +(pass) FAIL_FAST_MS primitive — real subprocess kill-0 lifecycle > child that survives window → process.kill(pid, 0) succeeds [203.27ms] +(pass) RFC-027 BLOCKER-1 — childrenMap key shape matches hub canonical node_id > derive key from request_id, not alias [0.22ms] +(pass) RFC-027 BLOCKER-1 — childrenMap key shape matches hub canonical node_id > recordSpawnedChild end-to-end with the canonical key — stop-daemon can find it [8.45ms] + +src/runtime/claude-native-binary.test.ts: +(pass) Claude native binary version pin > uses a directly exported package manifest when available [0.47ms] +(pass) Claude native binary version pin > walks from the resolved entrypoint when package exports hide package.json [0.35ms] +(pass) Claude native binary version pin > fails closed instead of installing latest when the SDK cannot be attested [0.18ms] +(pass) Claude native binary version pin > missing-binary fallback invokes npm with the installed SDK exact version [0.26ms] + +src/runtime/stop-daemon.test.ts: +(pass) recordSpawnedChild + map shape > records + snapshot returns entry [0.51ms] +(pass) recordSpawnedChild + map shape > re-record overwrites pid [0.24ms] +(pass) handleStopDoorbell — noop_not_my_child > unknown child_node_id → degraded ack (not error) [1.42ms] +/bin/sh: 1: pgrep: not found +(pass) handleStopDoorbell — happy stop (SIGTERM-reaped quickly) > child reaped after SIGTERM → ack stopped + SIGTERM signal recorded [8.99ms] +/bin/sh: 1: pgrep: not found +(pass) handleStopDoorbell — SIGKILL escalation > child ignores SIGTERM → grace exceeded → SIGKILL → ack stopped w/ SIGKILL [33.29ms] +/bin/sh: 1: pgrep: not found +(pass) handleStopDoorbell — delete action with delete_config > mv child workdir to ~/.anet/deleted/-/ + chmod 700 + ack backup_path [2.58ms] +/bin/sh: 1: pgrep: not found +(pass) handleStopDoorbell — delete action with delete_config > delete_config=false → no backup dir, no source move [2.02ms] +(pass) handleStopDoorbell — real subprocess primitive (no mocks) > real subprocess: SIGTERM kills + kill-0 ESRCH after [304.47ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > happy: hub returns 2 children + each has unique matching pid → both recovered [2.52ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > alias substring collision: pgrep finds 'bot2' for alias 'bot' but cmdline argv exact-match rejects [0.77ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > zombie pid skipped (state=Z) [0.55ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > ambiguous: multiple verified pids → skipped (operator intervention) [0.59ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > hub-active but pgrep finds nothing → missing (warn, don't auto-nudge) [0.64ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > daemon's own pid is excluded from candidates [0.53ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > list_my_children failure → safe empty result (no throw, no map mutation) [0.51ms] +(pass) rebuildChildrenMapOnBoot — real subprocess primitive (no pgrep mocks, no proc mocks) > matcher accepts a real subprocess whose argv contains --alias [203.59ms] + +src/runtime/claude-error-classify.test.ts: +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > HTTP 429 standalone [0.28ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > HTTP 529 overloaded (Anthropic spec) [0.04ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate_limit_exceeded (Anthropic / OpenAI shape) [0.02ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate-limit hyphen variant [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate limit space variant [0.04ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > quota exceeded phrase [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > quota exhausted phrase [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > Anthropic spec overloaded_error [0.04ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > plain overloaded mention [0.05ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > too_many_requests OpenAI-compat [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > too many requests space form [0.04ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > insufficient_quota OpenAI shape [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > usage_limit hit [0.02ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > MiniMax Chinese Token Plan 上限 [0.17ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > capacity exceeded vendor message [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 401 unauthorized (auth, not quota) [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 403 forbidden (auth, not quota) [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > plain timeout (not quota) [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 400 bad request (not quota) [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 499 client closed (not quota) [0.02ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > ETIMEDOUT network error (not quota) [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > HTTP 4290 not a real status (avoid false positive on substring) [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > empty string [0.02ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > null / undefined [0.03ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result null + output_tokens 0 [0.11ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result undefined (M3 incident shape) [0.04ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result empty string but usage non-zero [0.03ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result has text but output_tokens 0 (suspicious) [0.04ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > usage missing entirely (defaulting to 1 = non-zero) but result empty [0.04ms] +(pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > normal success — result + non-zero tokens [0.04ms] +(pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > short single-char reply still counts as success [0.03ms] +(pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > usage entirely missing but result non-empty [0.03ms] +(pass) quotaRemediationHint — vendor URL routing > intern-ai routing [0.18ms] +(pass) quotaRemediationHint — vendor URL routing > minimax routing [0.06ms] +(pass) quotaRemediationHint — vendor URL routing > deepseek routing [0.05ms] +(pass) quotaRemediationHint — vendor URL routing > anthropic-native routing [0.05ms] +(pass) quotaRemediationHint — vendor URL routing > unknown vendor falls back to generic hint [0.06ms] +(pass) quotaRemediationHint — vendor URL routing > empty / undefined → generic [0.07ms] + +src/runtime/grok-build-cli.test.ts: +(pass) buildGrokCliArgs > rejects an older Grok CLI before it can ignore required safety flags [0.50ms] +(pass) buildGrokCliArgs > uses streaming headless mode and resumes an existing session [0.32ms] +(pass) buildGrokCliArgs > fails closed instead of auto-approving when permission bypass is disabled [0.11ms] +(pass) buildGrokCliArgs > maps an explicit node tool allowlist and keeps MCP unavailable [0.22ms] +(pass) buildGrokCliArgs > intersects explicit tools with the read-only set when auto-approval is off [0.09ms] +(pass) buildGrokCliArgs > rejects unknown node tool names instead of silently widening access [0.08ms] +(pass) buildGrokCliArgs > rejects an explicit empty tool allowlist instead of widening to all tools [0.07ms] +(pass) buildGrokCliArgs > denies model reads of runtime credential and node-state paths [0.10ms] +(pass) runGrokCliTurn > reports spawn submission before first exact JSONL event consumption [70.47ms] +(pass) runGrokCliTurn > reduces streaming JSON text and persists the end-event session [41.33ms] +(pass) runGrokCliTurn > spawns with exactly the projected environment and no ambient credentials [45.58ms] +(pass) runGrokCliTurn > keeps the production-shaped setpriv/sh launcher on the exact PWD-bound env [48.45ms] +(pass) runGrokCliTurn > refuses a shell launcher when PWD is missing from the reviewed env [0.89ms] +(pass) runGrokCliTurn > removes the prompt when spawn rejects a malformed allowed env value [1.31ms] +(pass) runGrokCliTurn > surfaces non-zero exits and stderr [43.58ms] +(pass) runGrokCliTurn > fails fast when headless Grok asks for an interactive login [41.17ms] +(pass) runGrokCliTurn > rejects cancelled turns [44.82ms] +(pass) runGrokCliTurn > rejects a formal error event even if the process exits zero [46.95ms] +(pass) runGrokCliTurn > rejects max-turn truncation instead of reporting a partial reply as success [47.68ms] +(pass) runGrokCliTurn > terminates the process group when the caller aborts [35.18ms] +(pass) runGrokCliTurn > kills a silent child after the idle timeout [36.63ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > passes when the probe succeeds [0.46ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > throws with the real stderr and an actionable next step when uid_map is refused [0.20ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > still throws when the probe fails with no stderr at all [0.21ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > honours a custom unshare binary path [0.09ms] + +src/runtime/grok-child-env.test.ts: +(pass) Grok child environment boundary > builds the exact reviewed key set and drops every unreviewed credential [0.37ms] +(pass) Grok child environment boundary > re-projects a beforeSpawn result instead of trusting arbitrary keys [0.11ms] +(pass) Grok child environment boundary > rejects a beforeSpawn callback that changes a controlled value [1.01ms] +(pass) Grok child environment boundary > keeps the inherited list exact and reviewable [0.06ms] +(pass) Grok child environment boundary > keeps PTY PWD equal and adds only reviewed terminal/sandbox controls [0.57ms] +(pass) Grok child environment boundary > builds the narrower helper environment from an empty object [0.23ms] + +src/runtime/node-id-source.test.ts: +(pass) resolveNodeIdSource > configured identity wins over a polluted supervisor env [0.24ms] +(pass) resolveNodeIdSource > matching launcher env is accepted without a warning [0.07ms] +(pass) resolveNodeIdSource > legacy config without node_id keeps the env fallback [0.04ms] +(pass) resolveNodeIdSource > missing identity remains empty [0.02ms] +(pass) resolveNodeIdSource > warning escapes control characters from inherited env [0.14ms] + +src/runtime/inbox-drain-lane.test.ts: +(pass) inbox drain lanes > an informational lane drains while the work lane is busy [0.50ms] +(pass) inbox drain lanes > each lane remains serial [0.32ms] +(pass) inbox drain lanes > repeated wakeups for the same drain coalesce into one dirty rerun [0.32ms] +(pass) inbox drain lanes > a failed drain is reported and does not poison later retries [0.33ms] +(pass) inbox drain lanes > retry mode backs off and eventually completes the same drain [3.33ms] +(pass) inbox drain lanes > one failed inbox item does not starve later items in the same snapshot [0.49ms] +(pass) inbox drain lanes > ack-only retry does not duplicate the first notification or delay the second [1.41ms] + +src/runtime/codex-app-server-client.test.ts: +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > reverse request (method + id) routes to `reverse_request`, NOT orphan_response [15.08ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > reverse request also fires `reverse:` targeted event [11.01ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > notification (method + no id) routes to method-keyed event [8.33ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > response (id + result) resolves the matching pending request [10.02ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > response (id + error) rejects with codex-formatted Error [8.26ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > orphan response (id present, no matching pending) fires `orphan_response` [9.48ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > malformed messages fire `malformed` [7.96ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > parse errors on non-JSON payload fire `parse_error` [10.30ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > request timeout rejects the pending promise and cleans up the entry [45.40ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > close rejects any in-flight request cleanly (no unhandled rejection) [4.30ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > respondToReverseRequest emits a well-formed response envelope [13.01ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > errorReverseRequest emits a JSON-RPC error envelope [10.09ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > wraps an empty TypeError with endpoint and remediation [0.71ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > scrubs nested causes and bearer credentials independently of runtime shape [0.27ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > synchronous WebSocket constructor failure uses the same safe boundary [0.50ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > real dead loopback with query credential rejects/emits without leaking it [1.09ms] + +src/runtime/delegation-precheck.test.ts: +(pass) delegationTargetExists > imperative happy path — real other session is found [0.17ms] +(pass) delegationTargetExists > #230 — descriptive-text false positive no longer self-reflects [0.07ms] +(pass) delegationTargetExists > self-only match — only the calling node has this alias [0.05ms] +(pass) delegationTargetExists > typo alias — caller meant a real agent but mistyped [0.05ms] +(pass) delegationTargetExists > empty sessions array → empty_sessions [0.10ms] +(pass) delegationTargetExists > missing sessions field (caller did not destructure correctly) → no_sessions_field [0.06ms] +(pass) delegationTargetExists > empty target alias is defensively reported as not_in_sessions [0.03ms] +(pass) delegationTargetExists > whitespace padding is trimmed before comparison [0.04ms] +(pass) delegationTargetExists > sessions with missing / non-string alias fields are skipped without throwing [0.05ms] + +src/runtime/classify-result.test.ts: +(pass) classifyRuntimeResult — error precedence > quota error msg → soft-fail-quota (highest precedence) [0.16ms] +(pass) classifyRuntimeResult — error precedence > non-quota error → hard error [0.04ms] +(pass) classifyRuntimeResult — error precedence > auth error msg (401) → hard error (NOT quota — auth has its own path) [0.02ms] +(pass) classifyRuntimeResult — error precedence > error msg outranks empty result (don't double-classify) [0.04ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > all three zero → soft-fail-empty (even when result text present) [0.05ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > in=0 & out=0 but cost field MISSING + non-empty result → success (codex usage unreliable) [0.03ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > in=0 & cost=0 but out>0 → NOT silent reject (vendor returned something) [0.03ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > normal turn (all signals positive) → success [0.05ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > non-empty result + output_tokens=0 + cost missing → success (codex false-positive guard) [0.05ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > empty string result + non-zero tokens → soft-fail-empty [0.04ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > null result + non-zero tokens → soft-fail-empty [0.04ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > undefined result, missing usage → soft-fail-empty (empty result alone is enough) [0.04ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > single-char '0' result + tokens → success (not empty) [0.04ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > result text present + missing usage → success (don't penalise unreported usage) [0.04ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > empty string result + cost present + tokens → soft-fail-empty (text emptiness is the signal) [0.04ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > quota error with deepseek baseUrl → deepseek dashboard hint [0.07ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > quota error with intern baseUrl → intern hint [0.05ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > empty result with anthropic baseUrl → anthropic hint [0.05ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > missing baseUrl → generic hint [0.05ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > soft-fail-quota → 执行出错: [额度用尽][] : — [0.45ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > soft-fail-empty → 执行出错: 返回空响应 with in/out [0.07ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > error kind → 执行出错: [0.05ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > success kind → empty string (caller should not call this; defensive) [0.04ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > missing usage in context → in=0 out=0 fallback [0.05ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > missing hint on quota → no trailing dash artifact [0.10ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > reason longer than 80 chars is truncated on quota path [0.08ms] + +src/runtime/codex-app-server-bridge.test.ts: +(pass) CodexAppServerBridge — bootstrap + task mapping > bootstrap sends initialize + initialized + thread/resume in order [5.95ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > empty threadId → bootstrap creates a thread (thread/start) and adopts its id [8.22ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > stale threadId with no rollout → resume fails, bootstrap falls back to thread/start [6.33ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > startTaskTurn returns the server-assigned turnId and marks bridge working [4.55ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed for OUR turn fires task_reply mapped back to the task_id [14.64ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > only exact owned-turn item events emit task_activity [16.26ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > authenticated Dashboard native /goal text reaches the shared thread unchanged and replies [17.28ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > clientUserMessageId rebinds a task when a goal successor replaces the turn/start response id [54.40ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > client-id ownership observed before the RPC response wins without reversing task event order [25.98ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > real bridge + runtime bounds a deferred terminal when exact client identity never arrives [38.44ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > real bridge + runtime bounds an unresolved turn/start through the left-FIFO fallback [61.72ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > agentMessage/delta accumulates when server omits finalText [17.12ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed for a HUMAN-TUI-initiated turn is dropped (§7.5) [15.98ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > events for a DIFFERENT thread are dropped (defense in depth) [16.60ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > startTaskTurn refuses a second task while one is active [7.15ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed with an error field fires task_error, NOT task_reply [17.11ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed with interrupted status cannot become a successful reply [16.64ms] +(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > reverse-request approval records waiting_human and sends NO response [18.33ms] +(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > serverRequest/resolved clears waiting_human and status recovers [28.77ms] +(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > multiple concurrent approvals: bridge stays waiting_human until all resolve [37.97ms] +(pass) CodexAppServerBridge — two-client race for idle > only one bridge wins turn/start; the other observes and does not reply [22.05ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect recovers an active human turn and keeps it steerable [6.99ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect provenance keeps an orphaned network turn FIFO-only [27.23ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect provenance ignores leading whitespace before the network prefix [5.78ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect stays FIFO-only when real-wire active history omits userMessage [4.57ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > uses exact turn/steer contract and maps the human turn final answer [25.41ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > multiple Dashboard rows steer one human turn while ordinary agent work stays queued [40.11ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > steer mismatch fails closed and preserves the task in the normal FIFO [40.20ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > turn completion cannot attribute a task before turn/steer acceptance [43.03ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconciliation recovers a missed human turn completion and exact steered reply [15.62ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > concurrent startTaskTurn: exactly ONE turn/start reaches the server even with a slow response [55.86ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > submitTask queues the second task and drains it after turn/completed (order preserved) [118.01ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > cancelQueuedTask removes only the named FIFO row before it can execute [56.89ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read recovers a completed owned turn while a successor keeps the thread active [107.35ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read uses clientUserMessageId to recover a replacement turn when all live item events were lost [4.96ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > slow full-history fallback recovers when both terminal and successor notifications are lost [3.87ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > full history never attributes a different completed turn to the owned task [3.91ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read never recovers an interrupted turn as success [2.80ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > drain losing the idle race requeues at the FRONT and retries on next idle [166.89ms] + +src/runtime/probe-daemon.test.ts: +(pass) createPinnedLookup — Node/Bun lookup callback contract > single-address callback honors requested family [0.58ms] +(pass) createPinnedLookup — Node/Bun lookup callback contract > all-address callback returns only pinned copies [0.20ms] +(pass) createPinnedLookup — Node/Bun lookup callback contract > wrong hostname and unavailable family fail closed without fallback [0.35ms] +(pass) assertSecureTlsEnv (boot guard) > clean env passes [0.10ms] +(pass) assertSecureTlsEnv (boot guard) > NODE_TLS_REJECT_UNAUTHORIZED=0 throws [0.14ms] +(pass) classifyProbeResponse — status enum mapping > 200 → ok [0.16ms] +(pass) classifyProbeResponse — status enum mapping > 401 → auth_fail [0.03ms] +(pass) classifyProbeResponse — status enum mapping > 403 → auth_fail [0.03ms] +(pass) classifyProbeResponse — status enum mapping > 429 → quota [0.03ms] +(pass) classifyProbeResponse — status enum mapping > 500 → vendor_5xx [0.04ms] +(pass) classifyProbeResponse — status enum mapping > 404 → other_4xx [0.03ms] +(pass) classifyProbeResponse — status enum mapping > errorKind=redirect_forbidden surfaces directly [0.04ms] +(pass) classifyProbeResponse — status enum mapping > errorKind=timeout surfaces [0.05ms] +(pass) classifyProbeResponse — status enum mapping > errorKind=probe_resolve_unsafe_ip → returned status string passes through [0.04ms] +(pass) classifyProbeResponse — status enum mapping > ack has NO error_message / response_body / url fields (zod whitelist on hub side will reject; we just don't include) [0.08ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with private IP literal (169.254.169.254) → probe_resolve_unsafe_ip [1.45ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with private IP literal (10.0.0.1) → probe_resolve_unsafe_ip [0.13ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with localhost without ALLOW_LOOPBACK env → probe_resolve_unsafe_ip [0.13ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with localhost WITH ALLOW_LOOPBACK env → permitted to proceed (will fail on real network but not on IP guard) [5.25ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > NODE_TLS_REJECT_UNAUTHORIZED=0 → tls_error before any fetch [0.18ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > non-allowlist host for anthropic → daemon-level reject + ack probe_target_forbidden, no fetch [1.32ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > unknown vendor → daemon rejects, ack probe_target_forbidden [0.27ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > bad URL (not parseable) → daemon rejects, ack probe_target_forbidden [0.27ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > plain HTTP scheme on non-loopback host → daemon rejects, ack probe_target_forbidden [0.20ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > get_probe_request returns ok:false → no ack pushed (hub sweeper handles) [0.20ms] + +src/runtime/current-alias.test.ts: +(pass) CurrentAliasResolver — startup snapshot > current() returns the initial alias before any refresh() [0.20ms] +(pass) CurrentAliasResolver — startup snapshot > ageMs() reports Infinity before first fetch (cache is cold) [0.14ms] +(pass) CurrentAliasResolver — refresh() cache behaviour > warm cache short-circuits — no fetch fired within TTL [0.63ms] +(pass) CurrentAliasResolver — refresh() cache behaviour > expired cache hits the server and updates the alias + fires onDrift [0.38ms] +(pass) CurrentAliasResolver — refresh() cache behaviour > concurrent refresh() calls dedupe onto one fetch [11.96ms] +(pass) CurrentAliasResolver — graceful fetch failure > fetch throwing keeps the cached value and emits a warn [0.55ms] +(pass) CurrentAliasResolver — graceful fetch failure > fetch returning null is treated as 'server does not know yet' [0.20ms] +(pass) CurrentAliasResolver — graceful fetch failure > fetch returning empty string is also treated as 'server does not know' [0.15ms] +(pass) CurrentAliasResolver — graceful fetch failure > after a failed fetch the cache timestamp still bumps — no hammering [0.29ms] +(pass) CurrentAliasResolver — set() force install > set() updates the alias and fires onDrift with source 'snapshot' [0.22ms] +(pass) CurrentAliasResolver — set() force install > set() with the same value is a no-op (no drift event, but cache timestamp bumps) [0.09ms] +(pass) CurrentAliasResolver — set() force install > set('') is ignored (defends against caller forgetting to validate) [0.06ms] +(pass) CurrentAliasResolver — edge cases > nodeId = null short-circuits refresh() and never calls the fetch hook [0.17ms] +(pass) CurrentAliasResolver — edge cases > cacheTtlMs = 0 disables caching — every refresh() fetches [0.22ms] +(pass) CurrentAliasResolver — edge cases > ageMs() reflects elapsed time after a refresh [0.16ms] + +src/runtime/feishu-outbound-dir.test.ts: +(pass) Feishu legacy outbound directory > prefers the canonical worker value verbatim [0.12ms] +(pass) Feishu legacy outbound directory > reconstructs a legacy envelope from the explicit channel binding [0.07ms] +(pass) Feishu legacy outbound directory > does not consult a stale ambient node alias [0.10ms] +(pass) Feishu legacy outbound directory > passes the same explicit binding name to the worker [0.10ms] + +src/runtime/deleted-sweeper.test.ts: +(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > backup older than RETENTION_MS → physically removed [1.37ms] +(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > backup younger than 30d → KEPT [0.51ms] +(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > mixed: 2 old + 1 recent → only the 2 olds purged [0.85ms] +(pass) sweeper safety invariants (D7 nit) > skips dir names that don't match - pattern (no accidental purge) [0.48ms] +(pass) sweeper safety invariants (D7 nit) > log function receives ONLY the dir name — never any inner file path [0.50ms] +(pass) sweeper safety invariants (D7 nit) > dir-listing error (deletedRoot missing) → returns clean empty result, no throw [0.39ms] +[deleted-sweeper] failed to purge 1783988499555-bad: simulated EACCES +(pass) sweeper safety invariants (D7 nit) > rmDir throw → counted as error, sweep continues for siblings [0.91ms] + +src/runtime/config-apply.test.ts: +(pass) RESTART_SENTINEL — exact value pin > equals 75 (BSD EX_TEMPFAIL semantics, parent supervisor checks this exact code) [0.24ms] +(pass) #633 private text writer > replaces a leaf symlink without following it [2.30ms] +(pass) validateLocalPatch — defense-in-depth > undefined model + empty flags passes (no-op patch) [0.41ms] +(pass) validateLocalPatch — defense-in-depth > valid full patch passes [0.20ms] +(pass) validateLocalPatch — defense-in-depth > unknown flag rejected (even if hub validator drifts loose) [0.18ms] +(pass) validateLocalPatch — defense-in-depth > permissionMode invalid enum rejected [0.17ms] +(pass) validateLocalPatch — defense-in-depth > dangerouslySkipPermissions non-boolean rejected [0.19ms] +(pass) validateLocalPatch — defense-in-depth > maxTurns out of range rejected [0.26ms] +(pass) validateLocalPatch — defense-in-depth > timeout invalid rejected [0.22ms] +(pass) validateLocalPatch — defense-in-depth > empty-string model rejected [0.15ms] +(pass) computeApplyMode — tier classifier > empty patch → restart_only (restart_node) [0.21ms] +(pass) computeApplyMode — tier classifier > model only → restart [0.14ms] +(pass) computeApplyMode — tier classifier > permissionMode → restart [0.15ms] +(pass) computeApplyMode — tier classifier > dangerouslySkipPermissions → restart [0.15ms] +(pass) computeApplyMode — tier classifier > teammateMode no longer in allowlist → ignored by classifier (returns hot since no restart-required flag matches) [0.22ms] +(pass) computeApplyMode — tier classifier > timeout → restart [0.16ms] +(pass) computeApplyMode — tier classifier > maxTurns only → hot [0.18ms] +(pass) computeApplyMode — tier classifier > budget only → hot [0.19ms] +(pass) computeApplyMode — tier classifier > mixed (model + maxTurns) → restart (strictest wins) [0.17ms] +(pass) atomicWriteJson — temp + rename > creates file with JSON content + trailing newline [2.52ms] +(pass) atomicWriteJson — temp + rename > overwrites existing file atomically (no .tmp left behind) [2.34ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 0 [2.32ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 2 [2.65ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 22 [2.07ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 77 [2.04ms] +(pass) #472 private config permissions > repairs existing primary, backup, and parent before token read [0.66ms] +(pass) #472 private config permissions > custom --config parent is never chmodded [0.41ms] +(pass) #472 private config permissions > atomic custom --config write preserves parent mode [2.19ms] +(pass) #472 private config permissions > backup atomically replaces a legacy broad .prev [2.26ms] +(pass) backupConfigPrev — pre-write snapshot > copies existing config to .prev [2.17ms] +(pass) backupConfigPrev — pre-write snapshot > returns backedUp=false when no config exists yet (first-write case) [0.24ms] +(pass) backupConfigPrev — pre-write snapshot > overwrites previous .prev (single-generation rotation) [3.97ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary parses → returns primary [0.41ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary corrupted + .prev valid → restores .prev + reports source=prev [2.41ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary corrupted + no .prev → throws (truly bricked, caller surfaces) [0.50ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary AND .prev corrupted → throws with both errors [0.53ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary missing entirely → throws (caller will skip / first-boot path) [0.26ms] +(pass) mergePatch — patch + existing → new config (no mutation) > model replace [0.34ms] +(pass) mergePatch — patch + existing → new config (no mutation) > flags merge (does not replace whole flags obj) [0.25ms] +(pass) mergePatch — patch + existing → new config (no mutation) > empty existing + patch → patch only [0.16ms] +(pass) mergePatch — patch + existing → new config (no mutation) > empty patch → existing unchanged (deep clone) [0.21ms] +(pass) buildConfigSnapshot — pure helper contract (#290 final, drain-omit guard) > buildConfigSnapshot returns a valid snapshot regardless of caller drain state (pure) [0.57ms] +(pass) validateLocalPatch — teammateMode dropped (#290 review) > teammateMode rejected (was: allowed boolean; now: not-in-allowlist) [0.25ms] +(pass) computeApplyMode — teammateMode is no longer restart-required (#290 review) > teammateMode-only patch → hot (no longer in RESTART_REQUIRED_FLAGS) [0.16ms] +(pass) buildConfigSnapshot — masked report (no secrets) > includes model + ALLOWED_FLAGS only [0.23ms] +(pass) buildConfigSnapshot — masked report (no secrets) > missing model → null (not undefined, dashboard renders explicitly) [0.15ms] +(pass) buildConfigSnapshot — masked report (no secrets) > config_update_capable=false signals bare node (no supervisor wrapper) [0.15ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: host_supervisor passes through (string) [0.15ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: member passes through [0.15ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: missing → null (not undefined; dashboard distinguishes) [0.14ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: non-string narrowed to null (typeof guard) [0.21ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > nests runtimes_supported + allowed_secret_keys + max_concurrent_children [0.25ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > matches hub canonical path snap.daemon_capabilities.* — NOT at top level [0.21ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > partial declare: only runtimes_supported emits, others omitted [0.17ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > missing → daemon_capabilities undefined (regular non-daemon node) [0.14ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: non-array runtimes_supported dropped silently [0.17ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: array with non-string element dropped silently [0.18ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: max_concurrent_children non-finite or non-positive dropped [0.22ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > partial valid + partial invalid: only valid fields included [0.20ms] +(pass) channels — validateLocalPatch > valid keys pass [0.28ms] +(pass) channels — validateLocalPatch > commhub rejected — not a fork target (cli.ts:673 UNSUPPORTED_CHANNEL guard) [0.23ms] +(pass) channels — validateLocalPatch > unknown channel key rejected (defense-in-depth vs hub drift) [0.23ms] +(pass) channels — validateLocalPatch > non-array rejected [0.26ms] +(pass) channels — validateLocalPatch > non-string element rejected [0.19ms] +(pass) channels — validateLocalPatch > more than 16 entries rejected [0.23ms] +(pass) channels — computeApplyMode > channels-present patch is restart-tier [0.18ms] +(pass) channels — computeApplyMode > channels: [] still a state change → restart [0.18ms] +(pass) channels — computeApplyMode > channels + hot flag upgrades to restart [0.17ms] +(pass) channels — computeApplyMode > model + channels → restart [0.15ms] +(pass) channels — computeApplyMode > empty patch → restart_only [0.16ms] +(pass) channels — mergePatch replaces, does not merge > channels absent in patch: existing.channels preserved [0.22ms] +(pass) channels — mergePatch replaces, does not merge > channels present: existing.channels REPLACED wholesale [0.35ms] +(pass) channels — mergePatch replaces, does not merge > channels: [] disables all editable channels [0.24ms] +(pass) channels — mergePatch replaces, does not merge > first-write case (existing has no channels key) [0.16ms] +(pass) channels — mergePatch replaces, does not merge > defensive clone — patch mutation does not leak into merged [0.26ms] +(pass) mergePatch — path-qualified specs preserved > bare-type patch preserves existing telegram:/abs/path [0.21ms] +(pass) mergePatch — path-qualified specs preserved > bare-type patch keeps both when both were path-qualified [0.21ms] +(pass) mergePatch — path-qualified specs preserved > bare-type patch adds new bare key when existing had no matching spec [0.18ms] +(pass) mergePatch — path-qualified specs preserved > disable-all still works — empty patch wipes even path-qualified specs [0.19ms] +(pass) mergePatch — path-qualified specs preserved > first-write no existing channels: bare types stay bare [0.16ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > empty config emits channels=[] [0.21ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > bare-type list emitted verbatim + sorted [0.21ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > path-qualified specs collapse to bare type [0.17ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > dupes deduped, unparseable dropped [0.18ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > non-array channels field yields [] [0.15ms] + +src/runtime/codex-dep-loader.test.ts: +(pass) loadCodexSdk > returns the imported module without installing when already present [0.67ms] +(pass) loadCodexSdk > auto-installs and retries when the first import fails [0.61ms] +(pass) loadCodexSdk > throws a friendly multi-line error when install fails — includes pasteable npm command + module path + both root causes [0.77ms] +(pass) loadCodexSdk > install succeeds but post-install import still fails → terminal error names the install-then-resolve mismatch [0.45ms] +(pass) loadCodexSdk > module dir with shell metacharacters is single-quoted in the recovery hint [0.42ms] + +src/runtime/create-node-daemon-private-wiring.test.ts: +(pass) #633 daemon secret writers all use the private atomic choke point [0.36ms] + +src/runtime/reply-routing.test.ts: +(pass) codex-app-server reply routing > dashboard/user sender that is not a session falls back to send_reply [0.61ms] +(pass) codex-app-server reply routing > agent sender with a real session keeps send_task wake path [0.19ms] +(pass) codex-app-server reply routing > missing task id does not create an unparented reply task [0.11ms] +(pass) codex-app-server reply routing > roster load failure fails closed to send_reply [0.28ms] +(pass) codex-app-server reply routing > short ttl cache avoids repeated roster fetches and refreshes after expiry [0.42ms] +(pass) codex-app-server reply routing > failed send_task replies keep the peer-visible failure marker and high priority [0.07ms] + +src/runtime/grok-build-cli-home.test.ts: +(pass) prepareGrokCliHome > derives an opaque path segment and rejects dot identities [0.38ms] +(pass) prepareGrokCliHome > accepts only the pinned Grok regular-file copy of source agent_id [5.88ms] +(pass) prepareGrokCliHome > isolates config/trust, preserves a shared auth path, and creates stable sandbox profiles [2.92ms] +(pass) prepareGrokCliHome > refuses broad-mode or symlinked source auth without repairing it [1.44ms] +(pass) prepareGrokCliHome > repairs an existing Grok session store to owner-only modes [2.48ms] +(pass) prepareGrokCliHome > does not follow a symlink while repairing an existing session store [1.20ms] +(pass) prepareGrokCliHome > keeps the post-stop cleanup policy exact and reviewable [0.11ms] +(pass) prepareGrokCliHome > removes exact empty read-only project placeholders before resume without admitting executable sources [4.33ms] +(pass) prepareGrokCliHome > validates every exact project placeholder before unlinking any sibling [1.34ms] +(pass) prepareGrokCliHome > does not let a fatal project counterexample starve independent state containment [2.01ms] +(pass) prepareGrokCliHome > preserves nonempty, linked, wrong-mode, and wrong-type project counterexamples [4.47ms] +(pass) prepareGrokCliHome > preserves real project extension directories and still rejects executable contents on resume [1.83ms] +(pass) prepareGrokCliHome > removes only exact transient state and hardens retained post-stop state [5.10ms] +(pass) prepareGrokCliHome > hardens only the native lock derived from the exact leader socket [1.05ms] +(pass) prepareGrokCliHome > retains a non-empty leader log and rejects post-stop link attacks [2.16ms] +(pass) prepareGrokCliHome > refuses a non-empty exact sandbox placeholder [1.19ms] +(pass) prepareGrokCliHome > reclaims an empty mode-000 sandbox marker under a foreign pid without aborting [1.57ms] +(pass) prepareGrokCliHome > keeps a non-empty foreign sandbox marker unreadable so it fails closed [1.86ms] +(pass) prepareGrokCliHome > validates exact TUI process ids before mutation and refuses a placeholder symlink [1.47ms] +(pass) prepareGrokCliHome > enables the single TUI leader only for explicit copresence mode [12.29ms] +(pass) prepareGrokCliHome > admits only canonical owner-held commhub MCP artifacts [3.21ms] +(pass) prepareGrokCliHome > rejects a shared auth path covered by a required sandbox deny before state mutation [0.70ms] +(pass) prepareGrokCliHome > refuses to claim sandbox isolation when no deny target exists [0.67ms] +(pass) prepareGrokCliHome > rejects a source GROK_HOME reached through an ancestor symlink before state mutation [0.66ms] +(pass) prepareGrokCliHome > removes runtime-owned native hooks before every turn [1.13ms] +(pass) prepareGrokCliHome > unlinks a runtime-owned hook symlink without touching its external target [1.17ms] +(pass) prepareGrokCliHome > fails closed when a project native hook path exists [0.69ms] +(pass) prepareGrokCliHome > trusts only the exact canonical nested cwd and atomically replaces stale grants [3.36ms] +(pass) prepareGrokCliHome > rejects broad or symlinked folder-trust targets before writing trust state [1.23ms] +(pass) prepareGrokCliHome > refuses a planted trust-store symlink and leaves its target untouched [1.66ms] +(pass) prepareGrokCliHome > rejects every project executable source before granting folder trust [9.76ms] +(pass) prepareGrokCliHome > does not impose the shared-folder strict policy on legacy headless mode [2.28ms] +(pass) prepareGrokCliHome > rejects repo-root hooks from a nested cwd and dangling hook links [1.07ms] +(pass) prepareGrokCliHome > rejects a symlinked project .grok directory [0.75ms] +(pass) prepareGrokCliHome > rejects symlinked isolated homes and generated state without changing targets [1.49ms] +(pass) prepareGrokCliHome > rejects a state-home path escape before chmod, removal, or writes [0.84ms] +(pass) prepareGrokCliHome > requires a valid zero-hook inspect response [0.51ms] +(pass) prepareGrokCliHome > flocks the canonical project inode across symlink aliases and releases cleanly [118.03ms] +(pass) prepareGrokCliHome > gives the real flock holder only the exact helper environment [55.43ms] + +src/goals/format.test.ts: +(pass) formatSelfLoopsBlock — empty / omit semantics > no goals + omitWhenEmpty=true (default) → empty string [0.31ms] +(pass) formatSelfLoopsBlock — empty / omit semantics > no goals + omitWhenEmpty=false → explicit '无活跃循环' block [0.12ms] +(pass) formatSelfLoopsBlock — empty / omit semantics > only terminal goals (cancelled/complete/failed) → empty (same as no goals) [0.24ms] +(pass) formatSelfLoopsBlock — content shape > single active goal: header + id8 + cadence + text [0.36ms] +(pass) formatSelfLoopsBlock — content shape > paused goals shown with status='paused' [0.13ms] +(pass) formatSelfLoopsBlock — content shape > mix active + paused + terminal → only active+paused appear [0.12ms] +(pass) formatSelfLoopsBlock — cron-lite cadence rendering > time_of_day cadence: '每天 09:00' [0.10ms] +(pass) formatSelfLoopsBlock — cron-lite cadence rendering > weekday cadence: 'mon/wed/fri 18:30' [0.11ms] +(pass) formatSelfLoopsBlock — cron-lite cadence rendering > new-format interval cadence renders same as legacy interval_ms [0.07ms] +(pass) formatSelfLoopsBlock — cap + truncation > more than maxGoals → truncates with '...' summary [0.29ms] +(pass) formatSelfLoopsBlock — cap + truncation > text is one-line truncated at 100 chars [0.12ms] +(pass) formatSelfLoopsBlock — cap + truncation > multi-line text is rendered as single line [0.13ms] +(pass) formatSelfLoopsBlock — relative time rendering > next_wake_at far in the future → ISO-shortened [0.10ms] +(pass) formatSelfLoopsBlock — relative time rendering > next_wake_at in past → '已到期' [0.08ms] +(pass) formatSelfLoopsBlock — relative time rendering > malformed ISO doesn't crash, falls back to raw [0.09ms] + +src/goals/routing.test.ts: +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > authenticated Dashboard /goal and /loop pass through for every agent-node runtime [0.21ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > authenticated Dashboard /agoal and /aloop always select the ANet scheduler [0.09ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > non-Dashboard traffic retains /goal and /loop during the compatibility window [0.15ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > near matches and slash text away from the start never select the scheduler [0.17ms] +(pass) appendLegacyScheduledGoalNotice > non-Dashboard /goal and /loop replies carry a deterministic migration notice [0.10ms] +(pass) appendLegacyScheduledGoalNotice > new namespaced commands, Dashboard pass-through, and near matches are not warned [0.07ms] +(pass) appendLegacyScheduledGoalNotice > the migration notice is first so the outer reply cap cannot truncate it [0.10ms] +(pass) Dashboard native slash migration notice > interval-shaped /goal and /loop replies explain that ANet scheduling moved to /aloop [0.63ms] +(pass) Dashboard native slash migration notice > ordinary native commands, namespaced commands, and non-Dashboard paths are untouched [0.07ms] +(pass) Dashboard native slash migration notice > the notice survives low-value filtering and the outer reply cap [0.22ms] +(pass) Dashboard native slash migration notice > failed native replies still surface the migration notice and the failure [0.10ms] +(pass) reply filtering uses authenticated message provenance > a short presence reply to an authenticated Dashboard human task is delivered [0.11ms] +(pass) reply filtering uses authenticated message provenance > the same low-value class remains filtered for agent-to-agent tasks [0.06ms] +(pass) reply filtering uses authenticated message provenance > a provenance flag cannot bypass filtering for a non-task message type [0.07ms] + +src/goals/loops-http-server.test.ts: +(pass) localhost binding (通信龙 hard constraint #1+#2) > server bound to 127.0.0.1, not 0.0.0.0 [7.93ms] +(pass) localhost binding (通信龙 hard constraint #1+#2) > port is reachable [8.58ms] +(pass) localhost binding (通信龙 hard constraint #1+#2) > random port (different runs get different ports) [6.48ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > missing Authorization header → 401 [5.97ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > wrong token → 401 [5.76ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > non-Bearer scheme → 401 [8.20ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > correct Bearer → 200 [6.02ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > path other than /mcp → 404 [5.02ms] +(pass) MCP protocol — initialize / tools/list / tools/call > initialize returns serverInfo + tools capability [7.65ms] +(pass) MCP protocol — initialize / tools/list / tools/call > tools/list returns all 6 self-loop tools [6.76ms] +(pass) MCP protocol — initialize / tools/list / tools/call > tools/list each tool has description + inputSchema [5.29ms] +(pass) MCP protocol — initialize / tools/list / tools/call > unknown method → JSON-RPC -32601 [9.78ms] +(pass) MCP protocol — initialize / tools/list / tools/call > malformed JSON → -32700 [9.03ms] +(pass) tools/call — handler dispatch into parent ctx > list_my_loops on empty store [6.01ms] +(pass) tools/call — handler dispatch into parent ctx > create_my_loop with interval string writes to parent goalStore [7.60ms] +(pass) tools/call — handler dispatch into parent ctx > unknown tool name → JSON-RPC -32601 [5.36ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > batch-cancel via HTTP triggers confirm-back on 4th call [13.55ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > cooldown via HTTP — edit within 30s of upsert rejected [11.07ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > max-active-goals cap honored across HTTP [11.27ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > preflight invalid timezone rejected via HTTP (M1 #302 round-2 still works) [13.26ms] +(pass) custom token override (for tests) > explicit token honored [10.51ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp (exact) accepted → 200 [7.11ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp?foo=bar (with query string) accepted → 200 [4.84ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcpXYZ (suffix) rejected → 404 (not auth-checked) [7.26ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp/ (trailing slash) rejected → 404 [6.28ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp-leak (dash suffix) rejected → 404 [4.51ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > / (root) rejected → 404 [8.69ms] + +src/goals/failure-counter.test.ts: +(pass) resolveMaxConsecutiveFailures > default 5 when env unset [0.08ms] +(pass) resolveMaxConsecutiveFailures > env override honored [0.03ms] +(pass) resolveMaxConsecutiveFailures > invalid env falls back to default [0.04ms] +(pass) getFailureCount > legacy undefined → 0 [0.10ms] +(pass) getFailureCount > explicit 0 → 0 [0.04ms] +(pass) getFailureCount > explicit N → N [0.05ms] +(pass) bumpFailure > first failure: undefined → 1, shouldPause=false at default threshold [0.10ms] +(pass) bumpFailure > 4 → 5 at default threshold: shouldPause=true [0.08ms] +(pass) bumpFailure > 3 → 4 at threshold 5: shouldPause=false (below threshold) [0.06ms] +(pass) bumpFailure > custom threshold — 2 → 3 at threshold 3: shouldPause=true [0.04ms] +(pass) bumpFailure > beyond threshold: count continues to increment but shouldPause stays true [0.05ms] +(pass) resetFailure > legacy undefined stays undefined (no unnecessary write) [0.09ms] +(pass) resetFailure > 0 stays 0 (no unnecessary write) [0.05ms] +(pass) resetFailure > N > 0 → 0 [0.05ms] +(pass) resetFailure > threshold value → 0 [0.04ms] +(pass) applyAutoPause > status flipped to paused + counter preserved for observability [0.14ms] +(pass) applyAutoPause > progress_log entry recorded with count + reason [0.07ms] +(pass) applyAutoPause > long reason truncated to 300 chars in summary [0.11ms] +(pass) integration: full cycle > 5 bumps → pause → unpause reset → 5 more bumps → pause again [0.14ms] + +src/goals/parser.test.ts: +(pass) parseGoalCommand — English intervals > `5 min` form [0.12ms] +(pass) parseGoalCommand — English intervals > `5min` joined form [0.04ms] +(pass) parseGoalCommand — English intervals > `5 minutes` long form (plural wins over `min`) [0.03ms] +(pass) parseGoalCommand — English intervals > `1 hour` [0.05ms] +(pass) parseGoalCommand — English intervals > `hourly` keyword [0.08ms] +(pass) parseGoalCommand — English intervals > `daily` [0.05ms] +(pass) parseGoalCommand — English intervals > `1 day` [0.15ms] +(pass) parseGoalCommand — English intervals > `/goal` prefix is optional [0.04ms] +(pass) parseGoalCommand — English intervals > `/loop` alias [0.07ms] +(pass) parseGoalCommand — English intervals > `/aloop` strips the namespaced canonical prefix [0.09ms] +(pass) parseGoalCommand — English intervals > `/agoal` strips the namespaced compatibility prefix [0.12ms] +(pass) parseGoalCommand — Chinese intervals > `每5分钟` [0.24ms] +(pass) parseGoalCommand — Chinese intervals > `每 5 分钟` with spaces [0.06ms] +(pass) parseGoalCommand — Chinese intervals > `5分钟` bare (no 每) [0.13ms] +(pass) parseGoalCommand — Chinese intervals > `每小时` [0.05ms] +(pass) parseGoalCommand — Chinese intervals > `每天` [0.06ms] +(pass) parseGoalCommand — Chinese intervals > `每2小时` [0.13ms] +(pass) parseGoalCommand — rejection paths > no interval — reject [0.13ms] +(pass) parseGoalCommand — rejection paths > empty input — reject [0.05ms] +(pass) parseGoalCommand — rejection paths > seconds rejected with informative error [0.10ms] +(pass) parseGoalCommand — rejection paths > Chinese 秒 rejected [0.08ms] +(pass) parseGoalCommand — rejection paths > text becomes empty after stripping interval — reject [0.07ms] +(pass) parseGoalCommand — rejection paths > `/goal hourly` alone — reject (no text) [0.04ms] +(pass) parseGoalCommand — rejection paths > MIN_INTERVAL_MS is 60s [0.04ms] +(pass) parseGoalCommand — defence-in-depth > `1 min` exact minimum is accepted [0.06ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `5m` parses to 5 × 60_000 ms (the canonical CLI emission) [0.06ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `30m` / `90m` arbitrary minutes parse correctly [0.10ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `2h` parses to 2 hours [0.08ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `1d` parses to 24 hours [0.07ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > single-letter and word-form yield the same interval (no semantic drift) [0.07ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `5min` still wins over `5m` (longest-prefix declaration order) [0.07ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > single-letter inside a larger word is NOT swallowed (lookahead guard) [0.05ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `30s` is rejected with sub-minute error (parser + CLI aligned) [0.08ms] + +src/goals/loops-grok-wire.test.ts: +(pass) grok ACP MCP injection — RFC-025 M3 wire > when LOOPS env unset, only commhub server (back-compat) [0.27ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > when LOOPS env set, commhub + loops servers both present [0.10ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops server entry: ACP http schema (type+url+headers array) [0.08ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops headers: Authorization Bearer + transport tag + alias hint [0.16ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops entry localhost URL only (per security constraint) [0.12ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops + commhub independent: commhub headers don't leak token, loops headers don't leak ntok [0.15ms] +(pass) grok ACP MCP injection — #693 upload stdio > adds stdio commhub_upload when uploadMcpCommand provided [0.19ms] + +src/goals/completion-detect.test.ts: +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel on its own line [0.11ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel at end of text without trailing newline [0.03ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel at start of text [0.02ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > English GOAL_COMPLETE underscore on its own line [0.06ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > English GOAL COMPLETE (space) on its own line [0.02ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > sentinel with leading/trailing whitespace on the line [0.02ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > bare 'completed' in progress report [0.02ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > 'X completed' phrase mid-sentence [0.02ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > Chinese '已完成' as section header (not the goal-complete sentinel) [0.03ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > Chinese '已完成 X 项' enumeration in body [0.04ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > 'goal completed' as a phrase inside prose (was caught by old regex) [0.04ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > '目标已完成' substring without standalone line (old regex would match) [0.02ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > lowercased 'goal_complete' (sentinel is case-sensitive on English) [0.02ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > empty / null / undefined input [0.04ms] + +src/goals/schedule.test.ts: +(pass) computeNextWakeAt — interval mode > interval 5min from a baseline returns baseline + 5min [0.07ms] +(pass) computeNextWakeAt — interval mode > interval 24h returns +24h [0.04ms] +(pass) computeNextWakeAt — interval mode > interval is timezone-independent (UTC anchor same result regardless of node TZ) [0.05ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called at 10:00 Asia/Shanghai → tomorrow 09:00 (already past today) [4.37ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called at 08:00 Asia/Shanghai → today 09:00 (still upcoming) [0.44ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called AT 09:00 exactly → today (boundary include) [0.55ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > falls back to node default TZ if schedule has no timezone [0.69ms] +(pass) computeNextWakeAt — weekday mode > Monday 09:00 Asia/Shanghai, called Sun 10:00 → tomorrow (Mon) 09:00 [0.65ms] +(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30 Asia/Shanghai, called Sun 10:00 → Monday 18:30 (next eligible) [0.40ms] +(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30, called Mon 18:00 → today 18:30 (today eligible AND time still upcoming) [0.29ms] +(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30, called Mon 19:00 → today is Mon but past 18:30 → Wed 18:30 [0.76ms] +(pass) computeNextWakeAt — weekday mode > Friday 09:00, called Saturday → next Friday (full week wrap-around) [0.78ms] +(pass) computeNextWakeAt — weekday mode > workdays ['mon','tue','wed','thu','fri'] for daily standup is supported [0.39ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > 09:00 America/New_York in summer (EDT) → 13:00 UTC [0.51ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > 09:00 America/New_York in winter (EST) → 14:00 UTC [0.66ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > daily 02:30 wake DOES NOT skip on DST spring-forward day (just shifts that day) [0.57ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > daily 03:30 exists on spring-forward day (post-jump, unambiguous EDT) [0.74ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called Sat noon → fires at FIRST 01:30 EDT (before fall-back) [0.63ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AT first 01:30 EDT boundary → NEXT DAY (not second 01:30 EST same day) [0.57ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called between the two occurrences (05:45 UTC) → next day [0.51ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AT fall-back moment (06:00 UTC) → next day (skip 2nd occurrence) [0.46ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AFTER second occurrence (06:30 UTC) → next day [0.51ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 02:30 (post-fallback UNAMBIGUOUS) still fires on fall-back day — was buggy before P1.3 [0.53ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 03:00 (fully post-fallback) on fall-back day — regression for iterated offset [0.66ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 01:30 on fall-back Sunday → first occurrence EDT [0.46ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 02:30 on fall-back Sunday → same day (was CRASH before P1.3) [0.47ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 01:30 called AT first fire → NEXT Sunday (not same-day 2nd occurrence) [0.98ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > time_of_day 09:00 on fall-back day (outside ambiguous window) unchanged [0.33ms] +(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule → uses interval_ms from goal context, returns now + interval [0.07ms] +(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule + zero fallback interval → still returns now (no negative offset) [0.08ms] +(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule + missing fallback interval throws (programmer error) [0.15ms] +(pass) computeNextWakeAt — parser-rejected edge cases (defensive) > invalid time format '25:99' throws [0.16ms] +(pass) computeNextWakeAt — parser-rejected edge cases (defensive) > empty weekday list throws (caught by parser too, defense in depth) [0.14ms] +(pass) computeNextWakeAt — parser-rejected edge cases (defensive) > unknown weekday name throws [0.14ms] + +src/goals/self-loop-tools.test.ts: +(pass) list_my_loops > empty store → {goals: [], total: 0} [2.42ms] +(pass) list_my_loops > includes goal_id_short + cadence schedule shape [0.81ms] +(pass) create_my_loop > interval string '5m' creates goal [0.64ms] +(pass) create_my_loop > cron-lite time_of_day creates goal with schedule field [1.64ms] +(pass) create_my_loop > missing task → invalid_args [0.32ms] +(pass) create_my_loop > missing both schedule and interval → invalid_schedule [0.36ms] +(pass) create_my_loop > sub-minute interval rejected (parser 60s floor) [0.37ms] +(pass) create_my_loop > max active goals cap (3 cap → 4th rejected) [1.46ms] +(pass) edit_my_loop > change interval + report new value [1.61ms] +(pass) edit_my_loop > paused=true → status=paused [1.17ms] +(pass) edit_my_loop > cooldown — edit within 30s of last update rejected [0.56ms] +(pass) edit_my_loop > unknown goal_id → goal_not_found [0.32ms] +(pass) edit_my_loop > P0.3 unpause resets consecutive_failures (fresh 5-strike window) [1.13ms] +(pass) edit_my_loop > P0.3 paused=false when already active does NOT wipe mid-failure counter [1.33ms] +(pass) edit_my_loop > P0.3 paused=true does NOT reset consecutive_failures [1.19ms] +(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > pushes next_wake_at forward, interval_ms unchanged [1.50ms] +(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > invalid next_wake_in → invalid_interval [0.67ms] +(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > cooldown applies [0.53ms] +(pass) complete_my_loop (★ 达标归档) > status → 'complete' [1.27ms] +(pass) complete_my_loop (★ 达标归档) > unknown goal_id → goal_not_found [0.31ms] +(pass) cancel_my_loop > status → 'cancelled' [1.12ms] +(pass) cancel_my_loop > batch cancel (3 in 30s) triggers confirm-back on 4th [3.35ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad timezone in schedule → invalid_schedule, NOT written [0.58ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad weekday → invalid_schedule, NOT written [0.43ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad time format → invalid_schedule, NOT written [0.41ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > edit_my_loop: bad timezone on edit → invalid_schedule, EXISTING goal untouched [0.85ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: VALID structured schedule still works (regression) [1.44ms] +(pass) SELF_LOOP_TOOL_SPECS — registration table > exports 6 tools with stable names [0.27ms] +(pass) SELF_LOOP_TOOL_SPECS — registration table > every spec has non-empty description (LLM-discoverable) [0.22ms] +(pass) SELF_LOOP_TOOL_SPECS — registration table > description guides per RFC-025 §3.2 (intent-parse + report-back + safety) [0.38ms] + +src/goals/codex-wake.test.ts: +(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > startThread path → captures threadId, returns text + failed=false [1.54ms] +(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > startThread with thread.id still null → threadId undefined (SDK didn't expose id yet) [0.20ms] +(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > empty agent_message stream → returns '(无回复)' fallback [0.16ms] +(pass) runCodexWakeForGoal — subsequent wake (has codex_thread_id) > resumeThread succeeds → captures (possibly updated) threadId [0.25ms] +(pass) runCodexWakeForGoal — subsequent wake (has codex_thread_id) > resume returns thread whose .id was updated by SDK → reflects new id [0.20ms] +(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > resumeThread throws → startThread fallback, threadRebuilt=true, rebuildReason populated [0.47ms] +(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > startThread fallback also throws → failed=true with both errors surfaced [0.27ms] +(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > first wake + startThread throws → failed=true, threadRebuilt=false [0.32ms] +(pass) runCodexWakeForGoal — run-time error after thread obtained > runStreamed throws on first wake → failed=true, threadId still captured if SDK set it [0.29ms] +(pass) runCodexWakeForGoal — run-time error after thread obtained > runStreamed throws on resume → failed=true, threadRebuilt=false (resume itself worked) [0.27ms] +(pass) runCodexWakeForGoal — DI plumbing > newCodex called per wake (not cached across wakes — fresh client each time) [0.33ms] +(pass) runCodexWakeForGoal — DI plumbing > buildOpts passed verbatim to start/resume Thread [0.42ms] +(pass) runCodexWakeForGoal — DI plumbing > warn callback fires on resume-fail; log callback fires on success [0.44ms] +(pass) runCodexWakeForGoal — DI plumbing > missing log/warn deps → no throw (defaults are noops) [0.17ms] + +src/goals/scheduler.test.ts: +(pass) decideTickWork — basic selection > empty list → empty buckets [0.16ms] +(pass) decideTickWork — basic selection > single active goal due now → due [0.26ms] +(pass) decideTickWork — basic selection > single active goal due 1ms ago → due [0.07ms] +(pass) decideTickWork — basic selection > single active goal due 1ms in future → pending, not due [0.08ms] +(pass) decideTickWork — basic selection > multiple active goals: only the overdue ones wake; pending stay [0.19ms] +(pass) decideTickWork — status filtering > each non-active status is skipped (never appears in due) [0.13ms] +(pass) decideTickWork — status filtering > mixed batch: only active+due appear in due bucket [0.16ms] +(pass) decideTickWork — status filtering > wake order preserves input order — deterministic, no shuffling [0.10ms] +(pass) decideTickWork — invalid timestamp recovery > missing next_wake_at → treated as overdue (surface to wake handler) [0.08ms] +(pass) decideTickWork — invalid timestamp recovery > empty string next_wake_at → treated as overdue [0.06ms] +(pass) decideTickWork — invalid timestamp recovery > garbage next_wake_at (Date.parse → NaN) → treated as overdue [0.05ms] +(pass) decideTickWork — invalid timestamp recovery > non-string next_wake_at (number 0 from corrupt JSON) → treated as overdue [0.05ms] +(pass) decideTickWork — invalid timestamp recovery > inactive + invalid timestamp → still skipped (status wins over wake check) [0.07ms] +(pass) decideTickWork — counter sanity > active + skipped sums to total goals; pending + due sums to active [0.13ms] + +src/goals/store.test.ts: +(pass) GoalStore — basic lifecycle > fresh store: load with no file → ok, empty list [0.66ms] +(pass) GoalStore — basic lifecycle > upsert → get → list roundtrip [0.77ms] +(pass) GoalStore — basic lifecycle > delete → flushes to disk [1.44ms] +(pass) GoalStore — basic lifecycle > setStatus → in-memory + persisted [1.32ms] +(pass) GoalStore — basic lifecycle > setStatus on unknown id → undefined, no throw [0.29ms] +(pass) GoalStore — basic lifecycle > mutate applies in-place + bumps updated_at [6.64ms] +(pass) GoalStore — basic lifecycle > mutate on unknown id → undefined, mutator NOT invoked [0.40ms] +(pass) GoalStore — restart persistence > two instances see the same goals (= restart simulation) [0.91ms] +(pass) GoalStore — restart persistence > status change survives reload [1.17ms] +(pass) GoalStore — corruption recovery (#2) > invalid JSON → ok=false, .corrupt backup, empty store [2.00ms] +(pass) GoalStore — corruption recovery (#2) > unknown schema version → recovery [0.67ms] +(pass) GoalStore — corruption recovery (#2) > malformed shape (goals not array) → recovery [0.61ms] +(pass) GoalStore — Grok preview persistence boundary > recursively migrates task/progress/error, final writes, and archives at 0600 [3.40ms] +(pass) GoalStore — Grok preview persistence boundary > scrubs a broad-mode corrupt backup and replaces the live file with an empty safe store [1.47ms] +(pass) GoalStore — Grok preview persistence boundary > recursively scrubs a parseable unsupported-schema backup [1.38ms] +(pass) P0 runtime gate — name resolution > isClaudeRuntime accepts every claude alias [0.16ms] +(pass) P0 runtime gate — name resolution > isClaudeRuntime rejects codex / grok / unknown / empty [0.10ms] +(pass) P0 runtime gate — name resolution > runtimeBucket maps to canonical buckets [0.16ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > newGoal({runtime: 'claude-agent-sdk'}) succeeds (was the load-bearing bug) [0.10ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > newGoal succeeds for every recognized runtime alias (no per-bucket carve-out) [0.17ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > GoalStore.upsert accepts a claude-runtime goal end-to-end [0.61ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > isClaudeRuntime still classifies (kept for cross-bucket detection, not gating) [0.06ms] +(pass) P0 runtime gate — archiveAndClear > with live goals: backup file created, store emptied, reload sees empty [1.64ms] +(pass) P0 runtime gate — archiveAndClear > with no live file: returns undefined, no throw, store still flushes empty [0.53ms] +(pass) P0 runtime gate — archiveAndClear > backup filenames are unique across rapid calls [14.77ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + empty → ok (scheduler runs; was 'skip' pre-#144) [0.31ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + only claude-active goals → ok (scheduler runs) [0.25ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + empty → ok [0.08ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + only codex goals → ok [0.15ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > grok + only grok goals → ok [0.08ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + active codex/grok goals → archive + runScheduler=true (recover after archive) [0.30ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + grok-active leftover → archive (NOT fatal exit anymore) [0.10ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > grok + codex-active leftover → archive [0.07ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > inactive foreign-bucket goals do NOT trigger archive (only `active` counts) [0.13ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude with only inactive foreign leftover → ok (just cleanup pending) [0.06ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > unknown bucket → skip (no scheduler, no auto-archive) [0.07ms] +(pass) GoalStore — mutex serialisation (#1+#3) > 50 concurrent upserts → all 50 persist (no torn writes) [19.69ms] +(pass) GoalStore — mutex serialisation (#1+#3) > interleaved upsert + setStatus + delete stays consistent [12.45ms] + +src/runtime/grok-copresence/profile-process.test.ts: +(pass) Grok co-presence profile is pinned for the whole process > same input yields two exact, non-overlapping process capabilities [125.31ms] + +src/runtime/grok-copresence/jsonl.test.ts: +(pass) Grok copresence envelope and user parsing > parses only an exact, query-anchored Agent Network envelope [0.45ms] +(pass) Grok copresence envelope and user parsing > extracts the first authoritative user_query from string or Grok text-array content [0.53ms] +(pass) Grok copresence envelope and user parsing > does not trust a syntactically valid prefix unless the bridge registered it [2.45ms] +(pass) Grok copresence envelope and user parsing > nested user_query text cannot turn an owned network task into human delegation [0.41ms] +(pass) Grok copresence turn reducer > waits for completion and replies with the last non-empty assistant record [0.54ms] +(pass) Grok copresence turn reducer > keeps the last no-tool assistant when later tool-bearing chatter exists [0.27ms] +(pass) Grok copresence turn reducer > handles completion/chat-history polling order without returning an empty reply [0.35ms] +(pass) Grok copresence turn reducer > does not finalize an intermediate assistant visible before the completion event [0.24ms] +(pass) Grok copresence turn reducer > retains a completion observed before even the network user line [0.39ms] +(pass) Grok copresence turn reducer > retains an event-first human completion only for a trusted PTY submission [0.28ms] +(pass) Grok copresence turn reducer > never carries an unowned idle completion into a later network task [0.27ms] +(pass) Grok copresence turn reducer > binds an event-first completion to the exact registered network task [0.38ms] +(pass) Grok copresence turn reducer > consumes sanitized sample A block content and turn_number boundary [0.26ms] +(pass) Grok copresence turn reducer > consumes sanitized sample B and selects only the 14th no-tool assistant [0.63ms] +(pass) Grok copresence turn reducer > ignores standalone system-reminder user records without abandoning a network turn [0.25ms] +(pass) Grok copresence turn reducer > fails a terminal record without turn_started and never binds it to the next user [0.21ms] +(pass) Grok copresence turn reducer > never maps a human turn or failed network turn to a network reply [0.46ms] +(pass) Grok copresence turn reducer > abandons an unfinished network turn rather than attaching its answer to a human turn [0.39ms] +(pass) Grok copresence turn reducer > pairs events correctly when chat_history leads by two unnumbered turns [0.61ms] +(pass) Grok copresence turn reducer > does not let a new start overtake an abandoned numbered terminal [0.43ms] +(pass) Grok completion compatibility and defensive parsing > recognizes only top-level turn_ended with an exact successful outcome [0.34ms] +(pass) Grok completion compatibility and defensive parsing > binds turn_started turn_number while permission lifecycle remains inert [0.28ms] +(pass) Grok completion compatibility and defensive parsing > fails a started turn when turn_ended has no outcome [0.24ms] +(pass) Grok completion compatibility and defensive parsing > fails closed on an overlapping turn_started epoch [0.24ms] +(pass) Grok completion compatibility and defensive parsing > retains only a bounded tail of raw completion candidates [0.19ms] +(pass) Grok completion compatibility and defensive parsing > contains malformed and overlong lines instead of parsing or retaining them [1.27ms] +(pass) Grok completion compatibility and defensive parsing > incrementally joins split lines and drops a fragmented oversized line once [1.38ms] +(pass) persistent JSONL tail cursor > starts fresh at end by default, with an explicit start override [0.33ms] +(pass) persistent JSONL tail cursor > continues and fails closed on truncate or inode rotation [0.21ms] +(pass) persistent JSONL tail cursor > treats corrupt persisted state as non-replayable and advances JSON-safely [0.21ms] + +src/runtime/grok-copresence/attach.test.ts: +(pass) Grok co-presence local attach server > serves one owner-only client and cleans its socket on close [19.08ms] +(pass) Grok co-presence local attach server > rejects a second client without disturbing the attached human [7.98ms] +(pass) Grok co-presence local attach server > routes input and resize frames only through serialized arbiter callbacks [2.82ms] +(pass) Grok co-presence local attach server > fails closed when an inbound frame exceeds the configured bound [6.47ms] +(pass) Grok co-presence local attach server > refuses symlinks and regular files at the socket path [0.76ms] + +src/runtime/grok-copresence/state.test.ts: +(pass) Grok co-presence arbitration > lets the first human byte win a simultaneous human/network race [1.55ms] +(pass) Grok co-presence arbitration > gives a newly active human composer priority over an existing FIFO [0.35ms] +(pass) Grok co-presence arbitration > dequeues network tasks FIFO and never preempts an active turn [0.70ms] +(pass) Grok co-presence arbitration > cancels only queued timeouts and rejects duplicate task ids [0.45ms] +(pass) Grok co-presence arbitration > retains the active network task and FIFO across disconnect/reconnect [0.95ms] +(pass) Grok co-presence arbitration > marks approvals waiting for the human without emitting a response [0.26ms] +(pass) Grok co-presence arbitration > clears an already-waiting preview todo resolution in either active turn without completing it [0.32ms] + +src/runtime/grok-copresence/profile-wiring.test.ts: +(pass) Grok co-presence profile wiring > pins validated config before dynamically loading the runtime [1.84ms] +(pass) Grok co-presence profile wiring > cannot mutate the capability according to a logical turn owner [0.14ms] + +src/runtime/grok-copresence/leader-lifecycle.test.ts: +(pass) Grok auto-Leader lifecycle identity > rejects a different kernel executable hidden behind a pinned argv0 [0.89ms] +(pass) Grok auto-Leader lifecycle identity > rejects a live native listener whose argv0 forges the pinned executable [358.36ms] +(pass) Grok auto-Leader lifecycle identity > terminates one exact generation and removes only its stale socket [96.15ms] +(pass) Grok auto-Leader lifecycle identity > does not adopt a listener whose generation marker differs [156.01ms] +(pass) Grok auto-Leader lifecycle identity > does not signal or unlink after the socket pathname is replaced [53.40ms] +(pass) Grok auto-Leader lifecycle identity > revalidates the exact identity before escalating a TERM-resistant Leader [615.14ms] +(pass) Grok auto-Leader lifecycle identity > does not escalate when a TERM-resistant Leader replaces its listener [388.61ms] +(pass) Grok auto-Leader lifecycle identity > does not signal after the configured binary inode is replaced [55.52ms] +(pass) Grok auto-Leader lifecycle identity > retains the stale socket when another process from the generation remains [282.49ms] + +src/runtime/grok-copresence/allowlist-near-miss.test.ts: +(pass) grok copresence preview tool profile is an exact value set > a profile tool with an otherwise valid tuple is accepted [0.51ms] +(pass) grok copresence preview tool profile is an exact value set > refuses "todo_write2" [0.04ms] +(pass) grok copresence preview tool profile is an exact value set > refuses "search_tool2" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool2" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool_v2" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool-admin" +(pass) grok copresence preview tool profile is an exact value set > refuses "Use_Tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "USE_TOOL" +(pass) grok copresence preview tool profile is an exact value set > refuses "Todo_Write" +(pass) grok copresence preview tool profile is an exact value set > refuses " use_tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool " +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool\n" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_too" +(pass) grok copresence preview tool profile is an exact value set > refuses "tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "not_use_tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "xuse_toolx" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "use​tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_to​ol" +(pass) grok copresence preview tool profile is an exact value set > refuses "" +(pass) grok copresence preview tool profile is an exact value set > refuses " " +(pass) grok copresence preview tool profile is an exact value set > refuses "Search_Tool" [0.02ms] +(pass) grok copresence preview tool profile is an exact value set > refuses "TODO_WRITE" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool\r" +(pass) grok copresence preview tool profile is an exact value set > refuses "use_tool\u0000" +(pass) grok copresence preview tool profile is an exact value set > refuses "x_todo_write" +(pass) grok copresence preview tool profile is an exact value set > refuses "use-tool" +(pass) grok copresence preview tool profile is an exact value set > refuses "todo-write" +(pass) grok copresence preview tool profile is an exact value set > the profile is exactly the three pinned tools [0.12ms] + +src/runtime/grok-copresence/profile-selection.test.ts: +(pass) Grok co-presence process capability profile > accepts only the two exact startup profiles [0.38ms] +(pass) Grok co-presence process capability profile > defaults closed and rejects an invalid process profile [0.14ms] + +src/runtime/grok-copresence/runtime.test.ts: +(pass) Grok copresence launch and injection policy > keeps the fixed-tool auto-resolution exception exact and limited to active turns [0.50ms] +(pass) Grok copresence launch and injection policy > admits exact automatic lifecycles only for the fixed preview tool boundary [0.34ms] +(pass) Grok copresence launch and injection policy > exposes only reviewed value-free task failure codes and exact JSONL subcodes [0.64ms] +(pass) Grok copresence launch and injection policy > keeps the JSONL subcode allowlist direct, frozen, and actual-path-only [0.32ms] +(pass) Grok copresence launch and injection policy > locks the probed Grok TUI build exactly [0.22ms] +(pass) Grok copresence launch and injection policy > pins one TUI-effective commhub-only agent profile and hard-denies fallback routes [1.03ms] +(pass) Grok copresence launch and injection policy > rejects terminal escape injection and reserved origin markup [0.40ms] +(pass) Grok copresence launch and injection policy > recognizes the pinned TUI composer footer across ANSI fragments [0.24ms] +(pass) Grok copresence launch and injection policy > rejects external permission sources and noninteractive modes [1.45ms] +(pass) Grok copresence runtime integration > terminates the independently persistent auto-Leader and its unchanged stale socket [580.11ms] +(pass) Grok copresence runtime integration > cleans and hardens the exact pinned footprint only after confirmed close [579.89ms] +(pass) Grok copresence runtime integration > cleans each exact sandbox placeholder at its confirmed recovery boundary [894.29ms] +(pass) Grok copresence runtime integration > removes an old placeholder before a recovery generation reuses its PID [1188.53ms] +(pass) Grok copresence runtime integration > queues network input until the pinned TUI composer is ready [1229.43ms] +(pass) Grok copresence runtime integration > maps keyless fake-writer file mutations to exact value-free tail subcodes [3984.24ms] +(pass) Grok copresence runtime integration > continues exactly once across prefix-preserving atomic chat rewrites [2337.40ms] +(pass) Grok copresence runtime integration > rejects an atomic replacement that preserves only the consumed prefix [700.57ms] +(pass) Grok copresence runtime integration > rejects a same-inode shrink below the highest observed size even when offset remains valid [575.20ms] +(pass) Grok copresence runtime integration > does not expose an intermediate atomic generation before its successor preserves it [1077.85ms] +(pass) Grok copresence runtime integration > does not expose a pinned generation unlinked between path check and read [1113.77ms] +(pass) Grok copresence runtime integration > maps chat and events reset callback failures and stops polling after fatal [1504.47ms] +(pass) Grok copresence runtime integration > maps keyless reducer, lifecycle, and combined flush invariants at their boundaries [2469.43ms] +(pass) Grok copresence runtime integration > close waits for and tears down a Leader spawned by in-flight recovery [932.12ms] +(pass) Grok copresence runtime integration > retains containment and lifetime locks when a closing recovery PTY will not stop [2913.69ms] +(pass) Grok copresence runtime integration > excludes a different runtime from the same canonical project for the full TUI lifetime [1099.34ms] +(pass) Grok copresence runtime integration > contains an exited recovery generation before reusing its PID [1784.74ms] +(pass) Grok copresence runtime integration > retains final-cleanup ownership after every failed recovery PID is consumed [908.29ms] +(pass) Grok copresence runtime integration > reports exact submission and trusted consumption, never queued admission [1767.19ms] +(pass) Grok copresence runtime integration > arbitrates a live PTY, settles final JSONL, attaches once, and resumes [4051.67ms] +(pass) Grok copresence runtime integration > fails closed on automatic permission resolution without a human action [591.47ms] +(pass) Grok copresence runtime integration > accepts only the pinned preview todo_write automatic resolution tuple [1866.55ms] +(pass) Grok copresence runtime integration > keeps the shared TUI alive when the pinned preview auto-resolves todo_write in a human turn [1776.40ms] +(pass) Grok copresence runtime integration > keeps the shared TUI alive across exact search_tool then use_tool in a human turn [1672.64ms] +(pass) Grok copresence runtime integration > rejects every mutated preview todo_write automatic resolution tuple [4044.99ms] +(pass) Grok copresence runtime integration > preserves exact permission lifecycle order across coalesced and split event reads [2340.82ms] +(pass) Grok copresence runtime integration > fails closed on malformed or oversized permission lifecycle JSONL [1188.62ms] +(pass) Grok copresence runtime integration > rejects terminal reordering around automatic permission lifecycles [1717.17ms] +(pass) Grok copresence runtime integration > allows repeated fixed-tool automatic permission lifecycles in one network turn [1084.82ms] +(pass) Grok copresence runtime integration > never replies with a tool-bearing assistant when the final log is delayed past settling [1875.27ms] +(pass) Grok copresence runtime integration > rejects a completed turn that never resolved its approval [561.54ms] +(pass) Grok copresence runtime integration > does not resume a TUI that crashed at an approval prompt [608.36ms] +(pass) Grok copresence runtime integration > rejects a permission record that landed just before the crash poll [953.60ms] +(pass) Grok copresence runtime integration > refuses process-level resume with a persisted unresolved approval [192.82ms] +(pass) Grok copresence runtime integration > permits process-level resume after a persisted approval was resolved [536.01ms] +(pass) Grok copresence runtime integration > arms both resume tails before spawn-time permission records can be skipped [299.42ms] +(pass) Grok copresence runtime integration > discards spawn-time orphan completions before accepting the first new network task [1108.69ms] +(pass) Grok copresence runtime integration > drains more than one tail chunk before attach and fully cleans a startup rejection [878.41ms] +(pass) Grok copresence runtime integration > accepts the pinned startup auto-approval transition [533.93ms] +(pass) Grok copresence runtime integration > reruns the spawn audit and refuses recovery when it fails [781.25ms] +(pass) Grok copresence runtime integration > keeps auto-approval across recovery before scheduling [1734.82ms] +(pass) Grok copresence runtime integration > jointly drains chat and events until both recovery cursors are stable [1851.03ms] +(pass) Grok copresence runtime integration > rejects a beforeSpawn callback that widens a controlled child setting [201.19ms] +(pass) Grok copresence runtime integration > gives every real lifetime-lock holder only the exact helper environment [552.75ms] + +src/runtime/opencode-acp/events.test.ts: +(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_message_chunk with text content → replyText += content.text [2.34ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_thought_chunk with text → thoughtText, NOT replyText (grok discipline) [0.20ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > tool_call and tool_call_update both bump toolCalls [0.08ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > usage_update snaps totalTokens into state.usage [0.11ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > available_commands_update consumed silently (session-init only) [0.07ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_message_chunk without text content adds a warning [0.08ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > unknown method returns ignored without mutating state [0.10ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > unknown sessionUpdate subtype returns ignored (forward-compat) [0.05ms] +(pass) reduceOpencodeAcpResponse — session/prompt terminal response > captures stopReason + usage from result [0.20ms] +(pass) reduceOpencodeAcpResponse — session/prompt terminal response > missing stopReason still marks turn complete [0.04ms] +(pass) reduceOpencodeAcpFrames — replay the Phase 0b captured turn > full one-word turn: 10 thought chunks + 1 message chunk + usage + response [0.36ms] +(pass) reduceOpencodeAcpFrames — replay the Phase 0b captured turn > thinking-only terminal turn (no agent_message_chunk) — replyText stays empty [0.14ms] + +src/runtime/opencode-acp/child-env.test.ts: +(pass) buildOpencodeChildEnv — deny-by-default boundary > locks the exact hardened ancestor candidate set [0.10ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects sticky world-writable /tmp instead of silently degrading [4.22ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > passes only runtime/network allowlist and controls all state roots [22.04ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > safe inline policy disables every local tool without replacing provider/model [12.94ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > unsafe opt-in explicitly overrides the wizard's persisted safe policy [7.67ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > detects exact managed config sources across Linux, Windows, and macOS [1.12ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > safe runtime renders ordinary same-uid config through a strict allowlist [13.50ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > copies only blessed API auth fields into fresh data and keeps persistent state outside the child [14.74ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > never exposes planted persistent DB/log/cache/state/tmp descendants in safe or unsafe mode [19.77ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > removes a partially built launch tree when env construction fails [11.02ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > pre-spawn revalidation hard-fails when an ancestor discovery candidate appears [15.36ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > keeps active roots but reclaims a dead-owner crash root without following symlinks [38.20ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > reclaims dead-owner roots after the node workDir is deleted or recreated [78.82ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > a transient cleanup pathname swap is retried after child exit [26.99ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > a dead owner marker is retained while an orphan child still references the root [45.14ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > an exact exited-process identity exemption never hides a live descendant or PID mismatch [52.70ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects symlinks at workDir and every security-sensitive state layer [15.29ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects permissive modes and foreign ownership without repairing them [1.40ms] + +src/runtime/opencode-acp/profile-state.test.ts: +(pass) OpenCode private profile state > loads, atomically updates, backs up, and writes a session [12.41ms] +(pass) OpenCode private profile state > a post-load config symlink cannot redirect session writeback [1.10ms] +(pass) OpenCode private profile state > boot refuses a config symlink before self-heal can write its target [1.05ms] +(pass) OpenCode private profile state > backup refuses a pre-planted .prev symlink [0.83ms] +(pass) OpenCode private profile state > runtime hint rejects suspicious config leaves for every runtime [1.10ms] + +src/runtime/opencode-acp/client.test.ts: +(pass) OpencodeAcpClient — request/response correlation > request() resolves with the matching response's result [63.31ms] +(pass) OpencodeAcpClient — request/response correlation > error response rejects the promise with a shaped message [57.57ms] +(pass) OpencodeAcpClient — streaming notifications > emits 'notification' for every session/update frame [56.06ms] +(pass) OpencodeAcpClient — streaming notifications > id-carrying reverse requests get an explicit method-not-found response [55.90ms] +(pass) OpencodeAcpClient — process lifecycle > child exit rejects all pending requests [58.57ms] +(pass) OpencodeAcpClient — process lifecycle > isRunning flips false after stop() [1.82ms] +(pass) OpencodeAcpClient — process lifecycle > explicit child env is not merged with the client's process.env [55.85ms] + +src/runtime/opencode-acp/runtime.test.ts: +[opencode-acp] session/new — ses_test... +(pass) openOpencodeRuntime — cwd and tool policy > safe default keeps spawn + ACP session in one external launch workspace [126.67ms] +[opencode-acp] session/new — ses_probe_au... +(pass) openOpencodeRuntime — cwd and tool policy > version probe root is credential-free and gone before runtime auth is materialized [130.36ms] +[opencode-acp] session/load ok — resumed ses_existing... +(pass) openOpencodeRuntime — cwd and tool policy > safe session/load reuses the exact spawn PWD as its ACP cwd [132.06ms] +[opencode-acp] session/new — ses_test... +(pass) openOpencodeRuntime — cwd and tool policy > explicit unsafe flag restores project cwd and emits a trusted-task warning [134.39ms] +[opencode-acp] session/new — ses_evidence... +(pass) openOpencodeRuntime — cwd and tool policy > reports submission before exact prompt-response consumption [134.60ms] +[opencode-acp] session/new — ses_test... +(pass) openOpencodeRuntime — opening lifecycle > normal stop removes the launch root and copied vendor auth [127.78ms] +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +[opencode-acp] session/new — ses_test... +(pass) openOpencodeRuntime — opening lifecycle > repeated open/stop cycles do not accumulate launch roots [3148.65ms] +(pass) openOpencodeRuntime — opening lifecycle > an ancestor candidate planted by the version probe hard-fails before ACP spawn [66.49ms] +(pass) openOpencodeRuntime — opening lifecycle > package replacement after credential-free probe is rejected and runtime auth root is discarded [65.78ms] +(pass) openOpencodeRuntime — opening lifecycle > in-place binary self-modification after probe is rejected before credential spawn [64.48ms] +(pass) openOpencodeRuntime — opening lifecycle > production rejects canonical same-version packages below project cwd or node workDir [24.65ms] +(pass) openOpencodeRuntime — opening lifecycle > initialize failure force-kills the child before rejecting [128.04ms] +(pass) openOpencodeRuntime — opening lifecycle > onClient exposes a stalled-handshake child synchronously for shutdown [53.09ms] +[opencode-acp] session/new — ses_idle... +(pass) opencodeThink — failed-turn lifecycle > prompt idle timeout force-kills the child before rejecting [188.10ms] +[opencode-acp] session/new — ses_rescue_i... +[opencode-acp] #383 thinking-only terminal turn (chunks=0 thoughtChunks=1) — re-prompting for plain-text final +(pass) opencodeThink — failed-turn lifecycle > a failed thinking-only rescue discards the child before returning [161.85ms] + +src/runtime/opencode-acp/binary.test.ts: +(pass) resolvePinnedOpencodeBinary > locks the non-root uid=gid umask-0002 compatibility policy [0.22ms] +(pass) resolvePinnedOpencodeBinary > accepts the canonical package entrypoint and probes it from the external cwd [44.18ms] +(pass) resolvePinnedOpencodeBinary > accepts an npm-style PATH shim but returns the canonical package binary [24.53ms] +(pass) resolvePinnedOpencodeBinary > rejects a same-version fake package inside the project before executing it [1.17ms] +(pass) resolvePinnedOpencodeBinary > rejects forged package metadata and noncanonical entrypoints [3.21ms] +(pass) resolvePinnedOpencodeBinary > rejects unsafe file, package-directory, ancestor, and owner modes [2.98ms] +(pass) resolvePinnedOpencodeBinary > still enforces exact --version output after package identity succeeds [24.23ms] +(pass) resolvePinnedOpencodeBinary > refuses a caller-selected version other than the vetted release pin [0.83ms] +(pass) resolvePinnedOpencodeBinary > rejects a same-version package in a monorepo ancestor before probing it [1.59ms] +(pass) resolvePinnedOpencodeBinary > discovers a workspace ancestor when the configured project leaf is absent [0.83ms] +(pass) resolvePinnedOpencodeBinary > launcher absolute path wins over a hostile search PATH [24.06ms] +(pass) resolvePinnedOpencodeBinary > rejects non-absolute overrides [0.18ms] + +src/runtime/grok-build-acp/events.test.ts: +(pass) Grok ACP event reducer — fixture replay > T6 prompt fixture accumulates final reply chunks [5.93ms] +(pass) Grok ACP event reducer — fixture replay > T8 session/load skips replay chunks from the previous turn [0.82ms] +(pass) Grok ACP event reducer — fixture replay > T9 abort + resume accumulates only the resumed turn reply [0.59ms] + +src/runtime/grok-build-acp/resume-hint.test.ts: +(pass) fetchUnresolvedOutbound > returns empty array when the hub has no outbound rows for this sender [0.49ms] +(pass) fetchUnresolvedOutbound > filters to only delivered/started status [0.38ms] +(pass) fetchUnresolvedOutbound > caps results at topN (preserves server-side recency order) [0.42ms] +(pass) fetchUnresolvedOutbound > forwards the sender alias and a sane limit to the listTasks hook (no node_id fallback path) [0.22ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — sends from_node_id ONLY when probe confirmed server supports it [0.21ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — without probe confirmation, never sends from_node_id (old-server safety) [0.19ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — when probe explicitly returned false, falls back even with node_id available [0.20ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 — empty / null nodeId falls back to from_name path [0.27ms] +(pass) fetchUnresolvedOutbound > graceful fallback when list_tasks throws — returns empty, does not propagate [0.29ms] +(pass) fetchUnresolvedOutbound > graceful fallback for malformed payloads — non-array tasks [0.54ms] +(pass) fetchUnresolvedOutbound > clamps absurd opts: topN > 50 is capped, limit > 100 is capped [0.23ms] +(pass) fetchUnresolvedOutbound > 二审 — drops rows whose from_node_id does not match ours (server bug defence) [0.33ms] +(pass) fetchUnresolvedOutbound > 二审 — when row has no from_node_id, falls back to from_name match [0.33ms] +(pass) fetchUnresolvedOutbound > 二审 — drops rows with NEITHER from_node_id nor from_name (conservative) [0.34ms] +(pass) fetchUnresolvedOutbound > 二审 — prefers from_node_id over from_name when both present (handles rename correctly) [0.29ms] +(pass) fetchUnresolvedOutbound > 二审 — when WE have no nodeId, identity check uses from_name only [0.29ms] +(pass) buildResumeHint > returns null for an empty list — caller skips the prepend with no noise [0.13ms] +(pass) buildResumeHint > single task is listed with target alias + task id (8-char) + content preview [0.31ms] +(pass) buildResumeHint > hint wording: explicit do-NOT-redispatch instruction in both Chinese phrasing and English keyword [0.17ms] +(pass) buildResumeHint > hint promotes send_message as the legitimate alternative for status check-ins [0.11ms] +(pass) buildResumeHint > hint mentions server-side dedup as a safety net but tells the LLM not to rely on it [0.11ms] +(pass) buildResumeHint > hint avoids to-do framing — would push the LLM into reprocessing [0.13ms] +(pass) buildResumeHint > long content is truncated to 120 chars including ellipsis [0.20ms] +(pass) buildResumeHint > content with triple-backticks is defanged (prevents code-fence injection from resumed task body) [0.07ms] +(pass) buildResumeHint > missing fields fall back gracefully without throwing [0.12ms] +(pass) buildResumeHint > multi-task list preserves order from the input (server-side recency) [0.11ms] + +src/runtime/grok-build-acp/client.test.ts: +(pass) GrokAcpClient > starts the ACP server as `grok agent stdio` without inventing a model flag [67.36ms] +(pass) GrokAcpClient > handles ACP server-to-client fs and permission requests [65.77ms] +(pass) GrokAcpClient > coerces non-integer fs error codes to numeric JSON-RPC codes [63.05ms] +(pass) GrokAcpClient > requestWithIdleTimeout does not fire while agent is streaming notifications [790.82ms] +(pass) GrokAcpClient > requestWithIdleTimeout fires when agent goes silent past threshold [1206.12ms] +(pass) GrokAcpClient > preserves valid integer error codes [59.53ms] + +src/runtime/grok-build-acp/timeout-resolve.test.ts: +(pass) resolveGrokAcpTimeout > env wins over flags and default (mirrors cli.ts precedence) [1.99ms] +(pass) resolveGrokAcpTimeout > flag wins over default when env is unset [0.10ms] +(pass) resolveGrokAcpTimeout > flag string is parsed (config.json values arrive as strings or numbers) [0.06ms] +(pass) resolveGrokAcpTimeout > default fires when neither env nor flag is set [0.11ms] +(pass) resolveGrokAcpTimeout > empty string env is ignored (operator unset the var) [0.05ms] +(pass) resolveGrokAcpTimeout > null and empty flag are ignored — falls through to default [0.05ms] +(pass) resolveGrokAcpTimeout > non-numeric / negative / NaN inputs fall through (the silent-default trap) [0.09ms] + +src/runtime/grok-build-acp/runtime.test.ts: +(pass) runGrokAcpTurn runtime evidence > separates prompt submission from exact prompt-response consumption [88.87ms] + +src/runtime/opencode-copresence/inbox-wiring.test.ts: +(pass) OpenCode copresence CommHub message wiring > work and informational drains are independent lanes [0.52ms] +(pass) OpenCode copresence CommHub message wiring > new_message SSE uses a non-blocking informational lane [0.19ms] +(pass) OpenCode copresence CommHub message wiring > message is displayed as a non-replying TUI notification in the fast drain [0.21ms] +(pass) OpenCode copresence CommHub message wiring > the task drain does not claim OpenCode copresence messages [0.19ms] +(pass) OpenCode copresence CommHub message wiring > network tasks pass their authenticated sender into the shared TUI turn [0.17ms] +(pass) OpenCode copresence CommHub message wiring > startup and SSE reconnect both recover pending informational messages [0.30ms] +(pass) OpenCode copresence CommHub message wiring > runtime startup is single-flight and shutdown waits for an in-flight open [0.14ms] +(pass) OpenCode copresence CommHub message wiring > tmux SIGHUP enters the same cleanup path as SIGTERM [0.38ms] + +src/runtime/opencode-copresence/runtime.test.ts: +(pass) OpenCode native serve+attach copresence > requires an explicit provider/model for production copresence [0.39ms] +(pass) OpenCode native serve+attach copresence > requires an explicit provider/model at the vetted launch seam too [1.85ms] +(pass) OpenCode native serve+attach copresence > wires one token-bound CommHub MCP without reopening local tools [2.00ms] +(pass) OpenCode native serve+attach copresence > uses one authenticated loopback session for FIFO network turns and emits an owner-only attach launcher [184.08ms] +(pass) OpenCode native serve+attach copresence > shows the network sender in both the toast title and message body [181.72ms] +(pass) OpenCode native serve+attach copresence > shows the normalized network-task sender in the shared TUI turn [200.67ms] +(pass) OpenCode native serve+attach copresence > waits for an already-busy human session before injecting a network turn [618.79ms] +(pass) OpenCode native serve+attach copresence > refuses a reply owned by a human turn that won the idle-to-submit race [207.23ms] +(pass) OpenCode native serve+attach copresence > uses OpenCode's ascending message ID shape across sequential network turns [229.07ms] +(pass) OpenCode native serve+attach copresence > does not treat a missing session status and missing session record as idle [437.97ms] +(pass) OpenCode native serve+attach copresence > binds teardown authority to a detached pid, pgrp, and process start ticks [2.01ms] + +src/runtime/codex-app-server/session-manager.test.ts: +(pass) createCodexSessionManager > the production Codex inbox path is wired through the shared holder [1.09ms] +(pass) createCodexSessionManager > concurrent Dashboard handlers share one complete open attempt [0.71ms] +(pass) createCodexSessionManager > a rejected open is cleared and the next row can retry [0.29ms] +(pass) createCodexSessionManager > stopped and explicitly invalidated sessions are never reused [0.29ms] +(pass) createCodexSessionManager > a session that dies during bootstrap is not published [0.14ms] + +src/runtime/codex-app-server/runtime.test.ts: +(pass) buildOwnedAppServerArgs > no opts → bare app-server (codex defaults apply) [0.11ms] +(pass) buildOwnedAppServerArgs > approval_policy only → single -c override before --listen [0.05ms] +(pass) buildOwnedAppServerArgs > sandbox_mode only → single -c override [0.08ms] +(pass) buildOwnedAppServerArgs > auto-approve posture (never + danger-full-access) → both overrides, policy first [0.06ms] +(pass) buildOwnedAppServerArgs > commhubMcpUrl → adds url + bearer-token-env-var -c overrides [0.06ms] +(pass) buildOwnedAppServerArgs > the CommHub bearer TOKEN never appears in argv (only the env-var NAME) [0.12ms] +(pass) buildOwnedAppServerArgs > full production posture (yolo + commhub MCP) → stable order, --listen last [0.11ms] +(pass) recoverSharedTurnOnAttach > invokes persisted active-turn recovery before shared runtime is returned [0.48ms] +(pass) recoverSharedTurnOnAttach > history read failure is visible and never reported as steerable [0.27ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > FIFO admission reports neither submission nor consumption [21.06ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > exact runtime submission and task_started report each level once [0.96ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > exact task activity resets the response idle deadline for a long-running turn [71.23ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > activity from another task cannot keep a silent owned task alive [57.04ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > a started task whose client identity never confirms has a bounded, distinct response timeout [27.69ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > a never-started FIFO task has its own finite, distinct queue deadline [80.85ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > lost task_started after FIFO removal remains finite [80.92ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > a failed start or steer requeued after the queue deadline cannot leave a ghost row [113.94ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > queued wait does not consume the model-response timeout budget [91.58ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > another task starting cannot arm this task's timeout [116.04ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > resolves from authoritative reconciliation when turn/completed is missed [7.41ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > forwards the authenticated Dashboard steering decision to the bridge [5.43ms] +(pass) codexAppServerReplyOrThrow > failed bridge outcomes enter processTask's thrown failure path [0.28ms] +(pass) codexAppServerReplyOrThrow > successful empty replies preserve the existing fallback [0.06ms] + + 1281 pass + 0 fail + 4365 expect() calls +Ran 1281 tests across 91 files. [114.57s] +[L0b] every agent-node/tests file, dispatched by kind +tests_dir_executed=6 tests_dir_discovered=6 tests_dir_failed=0 +[L1] witnessed-red: disconnect readable attachment content from runtime +MUTATION_RED readable-attachment-runtime-disconnected rc=1 +RESULT: PASS +``` +### test745 +``` +# test745 — complete agent-network unit domain +source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +bun=1.3.14 node=v22.23.2 git=git version 2.39.5 uid=1000 +test_files=46 +[L0] full agent-network/src unit suite as non-root +bun test v1.3.14 (0d9b296a) + +src/cli-args.test.ts: +(pass) CLI argument parsing > pins the complete presence-only flag set [0.22ms] +(pass) CLI argument parsing > --accept-dev-channels does not swallow a following positional operand [0.44ms] +(pass) CLI argument parsing > --accept-dev-channels works after a positional operand [0.09ms] +(pass) CLI argument parsing > --dev-open does not swallow a following positional operand [0.02ms] +(pass) CLI argument parsing > --dev-open works after a positional operand [0.02ms] +(pass) CLI argument parsing > --dry-run does not swallow a following positional operand +(pass) CLI argument parsing > --dry-run works after a positional operand +(pass) CLI argument parsing > --follow does not swallow a following positional operand +(pass) CLI argument parsing > --follow works after a positional operand +(pass) CLI argument parsing > --no-auto-self does not swallow a following positional operand +(pass) CLI argument parsing > --no-auto-self works after a positional operand [0.01ms] +(pass) CLI argument parsing > --no-yolo does not swallow a following positional operand +(pass) CLI argument parsing > --no-yolo works after a positional operand +(pass) CLI argument parsing > --resume-latest does not swallow a following positional operand +(pass) CLI argument parsing > --resume-latest works after a positional operand +(pass) CLI argument parsing > --self does not swallow a following positional operand [0.01ms] +(pass) CLI argument parsing > --self works after a positional operand +(pass) CLI argument parsing > --f does not swallow a following positional operand [0.01ms] +(pass) CLI argument parsing > --f works after a positional operand +(pass) CLI argument parsing > presence-only flags do not accept an explicit true or false value [0.09ms] +(pass) CLI argument parsing > value flags, repeatable flags, and multiple positionals retain their behavior [0.12ms] +(pass) CLI argument parsing > key=value remains unsupported and is treated as the complete key [0.07ms] + +src/normalize-runtime.test.ts: +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > legacy normalization: unknown string → claude-agent-sdk [1.05ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > empty string → claude-agent-sdk [0.04ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > undefined (no arg) → claude-agent-sdk [0.04ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > undefined profile arg → claude-agent-sdk [0.04ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > profile with missing runtime field → claude-agent-sdk [0.04ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > profile with empty-string runtime field → claude-agent-sdk [0.03ms] +(pass) normalizeRuntimeStrict — execution boundaries fail closed > missing and empty runtime still select the documented default [0.13ms] +(pass) normalizeRuntimeStrict — execution boundaries fail closed > canonical names and supported aliases are accepted [0.05ms] +(pass) normalizeRuntimeStrict — execution boundaries fail closed > a non-empty unknown runtime is rejected [0.21ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit 'claude-code-cli' → claude-code-cli (operator opt-in still works) [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit 'claude-agent-sdk' → claude-agent-sdk [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'claude' → claude-agent-sdk (existing canonicalization) [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'claude-sdk' → claude-agent-sdk [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'agent-sdk' (string form) → claude-agent-sdk [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > 'codex' / 'codex-sdk' → codex-sdk [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > 'grok' / 'grok-build' / 'grok-build-acp' → grok-build-acp [0.04ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit Grok co-presence names → grok-build-cli [0.05ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit 'opencode-cli' → opencode-cli (canonical launcher name) [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'opencode' → opencode-cli (short form) [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='opencode-cli' → opencode-cli [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='opencode' → opencode-cli [0.04ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit 'codex-app-server' → codex-app-server [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'codex-tui' → codex-app-server [0.22ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'codex-appserver' → codex-app-server [0.04ms] +(pass) normalizeRuntime — explicit choices are preserved > 'codex-sdk' still → codex-sdk (not shadowed by the app-server branch) [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > 'codex' still → codex-sdk (legacy short alias unchanged) [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='codex-app-server' → codex-app-server [0.03ms] +(pass) normalizeRuntime — profile object paths > profile with runtime='claude-code-cli' → claude-code-cli (explicit, preserved) [0.02ms] +(pass) normalizeRuntime — profile object paths > profile with runtime='agent-sdk' + codexRuntime='codex' → codex-sdk (legacy hybrid) [0.03ms] +(pass) normalizeRuntime — profile object paths > profile with runtime='agent-sdk' + no codexRuntime → claude-agent-sdk [0.03ms] +(pass) normalizeRuntime — profile object paths > legacy profile normalization keeps unknown → default for display/migration [0.61ms] + +src/batch-workdir-wiring.test.ts: +(pass) batch workdir wiring > normalizes create workdir before mkdir or chdir [0.66ms] +(pass) batch workdir wiring > normalizes cleanup workdir before filesystem mutation [0.29ms] + +src/top-level-help-contract.test.ts: +(pass) top-level help matches the implemented command parsers > advertises only the implemented config and batch shapes [183.53ms] +(pass) top-level help matches the implemented command parsers > includes the provider required by opencode auth-login [222.24ms] + +src/opencode-pin.test.ts: +(pass) opencode-pin — built-in fallback > release builtin pin is the revalidated opencode-ai@1.18.1 [0.30ms] +(pass) opencode-pin — built-in fallback > returns the built-in constant when no override file exists [0.48ms] +(pass) opencode-pin — built-in fallback > missing/untrusted package hint preserves detail and exact install command [0.24ms] +(pass) opencode-pin — override file write + read round-trip > a smoke marker for the exact release pin is recognized [1.43ms] +(pass) opencode-pin — override file write + read round-trip > a locally-smoked different version cannot override the release pin [0.35ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > hand-edited file with version but NO smokePassedAt → falls back to built-in [0.35ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > version string doesn't match semver → falls back to built-in [0.29ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > smokePassedAt not an ISO timestamp → falls back to built-in [0.32ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > malformed JSON → falls back to built-in without throwing [0.42ms] + +src/tmux-attach.test.ts: +(pass) tmux attach resolution > parses opaque IDs and Unicode names [1.01ms] +(pass) tmux attach resolution > selects the exact TUI instead of prefix siblings [0.27ms] +(pass) tmux attach resolution > does not fall back to a bridge or node session [0.08ms] + +src/owner-env-file.test.ts: +(pass) loadOwnerOnlyEnvFile > loads the isolated commhub credential without overriding explicit identity [1.15ms] +(pass) loadOwnerOnlyEnvFile > rejects relative, broad-mode, and symlinked credential files [0.89ms] + +src/opencode-owner-mode.test.ts: +(pass) OpenCode owner/mode policy > accepts umask-0002 modes only for a non-root uid=gid layout [0.17ms] +(pass) OpenCode owner/mode policy > always rejects world write and keeps root/foreign ownership strict [0.10ms] + +src/channel-attachments.test.ts: +(pass) Claude channel attachments > pins the readable extension allowlist as an exact value set [1.63ms] +(pass) Claude channel attachments > cache roots are alias-isolated even for path-shaped aliases [0.48ms] +(pass) Claude channel attachments > downloads an authenticated Dashboard PNG and surfaces an owner-local Read path [5.24ms] +(pass) Claude channel attachments > downloads an authenticated non-image file for the Read-capable channel [1.59ms] +(pass) Claude channel attachments > does not fetch or inject a non-allowlisted file type [0.24ms] +(pass) Claude channel attachments > download failure preserves the original text and exposes no token [0.70ms] +(pass) Claude channel attachments > rejects traversal-shaped file ids before any fetch [0.24ms] +(pass) Claude channel attachments > does not trust a sender-provided local path [0.65ms] + +src/codex-model-default.test.ts: +(pass) Codex model defaults > all Codex creation runtime spellings use the supported default [0.17ms] +(pass) Codex model defaults > shared Codex choice catalog has one supported default [0.17ms] + +src/copresence-identity.test.ts: +(pass) Test 1: UUID round-trip > writeMarker persists exactly the provided uuid (single source of truth) [7.97ms] +(pass) Test 1: UUID round-trip > writeMarker refuses empty uuid (guard against silent regeneration) [0.42ms] +(pass) Test 1: UUID round-trip > writeMarker refuses non-string uuid [0.32ms] +(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when listAllPids throws [1.06ms] +(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when a member's environ read throws [0.72ms] +(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when a stat read throws [0.36ms] +(pass) Test 3: foreign member in PGID → SKIP > group with unmarked co-resident refuses homogeneity [0.31ms] +(pass) Test 3: foreign member in PGID → SKIP > group where every member carries the marker is ok [0.31ms] +(pass) Test 4: main-dead-child-alive (environ scan is authority) > scan finds workers even when marker's stored pids are gone [0.81ms] +(pass) Test 5: child setsid → new PGID > detached child grouped under its current pgid, not marker's stored pgid [0.32ms] +(pass) Test 6: PID-reuse defense is the boot_id + environ-scan invariant > environ scan only returns pids whose current environ carries the uuid [0.29ms] +(pass) Test 7: partial-start rollback (marker gate) > MISSING marker after partial start prevents any process action [0.41ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > null body → SCHEMA_INVALID (no TypeError from `in` operator) [0.59ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > bare number → SCHEMA_INVALID [0.48ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > empty array → SCHEMA_INVALID [0.80ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > empty object → SCHEMA_INVALID (missing required fields) [0.65ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > wrong types in schema → SCHEMA_INVALID [0.56ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > syntactically invalid JSON → PARSE_ERROR [0.52ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > wrong mode → WRONG_MODE (even with valid JSON) [0.55ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > symlink → SYMLINK (refuses to follow) [0.51ms] +(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > NOT_REGULAR: directory at marker path with mode 0600 (skips SYMLINK+WRONG_MODE) [0.51ms] +(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > OWNER_MISMATCH: valid marker file whose lstat.uid differs from process.getuid() (SECURITY CRITICAL) [3.01ms] +(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > STALE_BOOT_ID: valid schema but boot_id differs from current /proc boot_id [0.99ms] +(pass) Test 9: self-context refuses stop from within the tree > caller's own environ carrying the marker is detected [0.48ms] +(pass) Test 9: self-context refuses stop from within the tree > ancestor carrying the marker is detected via PPID walk [0.46ms] +(pass) Test 9: self-context refuses stop from within the tree > clean caller (no marker in ancestry) returns self=false [0.32ms] +(pass) Test 10: non-copresence codex-app-server → legacy path (zero diff) > readMarker returns MISSING for an ordinary codex-app-server node dir [0.55ms] +(pass) Test 11: 二次 stop is idempotent (MISSING = already stopped) > 2nd read after successful removeMarker returns MISSING (no side effects) [3.48ms] +(pass) Test 11: 二次 stop is idempotent (MISSING = already stopped) > removeMarker on already-missing marker does not throw [0.31ms] +(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > verified groups get SIGTERM, still-alive groups then get SIGKILL [6.72ms] +(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > groups with foreign members are SKIPPED, never signaled [2.51ms] +(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > no marker-carrying pids anywhere → immediate success (idempotent) [0.54ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > other-user EACCES on environ → skip that pid (expected, not fail) [0.64ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Defect A defense: own-uid EACCES pid IN SCOPE (anchored) → reap refuses to delete marker [0.97ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 1: own-uid EACCES pid OUT OF SCOPE → informational only, teardown still succeeds [0.88ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 1: unreadable pid sharing a marker carrier's PGROUP is in scope (no anchors needed) [0.43ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 8/invariant 5: an anchor whose starttime no longer matches is REJECTED (pid reuse) [0.39ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 7: post-kill RESCAN unreadable half also preserves the marker [0.73ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > zombie process environ EACCES → skip (mm freed, expected) [0.39ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > EACCES-carrying process that vanishes during discrimination → skip [0.27ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > group containing a zombie same-uid member still verifies OK for the live marker members [0.33ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > group containing an other-user EACCES member still verifies OK for our members [0.27ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > empty group (no live marker members) → EMPTY_GROUP refuse (never ok:true) [0.20ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > own-uid non-zombie unreadable → ENUM_ERROR (fail-closed) [0.23ms] +(pass) Finding #2: killPgroup pgid<=0 guard > realKiller().killPgroup(0, TERM) throws — kill(-0) would target caller's own pgroup [0.46ms] +(pass) Finding #2: killPgroup pgid<=0 guard > realKiller().pgroupAlive(0) throws [0.34ms] +(pass) Finding #3: reapMarkerGroups uses async sleep (not busy-wait) > grace period is truly asynchronous — event loop ticks during it [103.65ms] +(pass) Finding #3: reapMarkerGroups uses async sleep (not busy-wait) > injected sleep function is used (tests can override with fast/deterministic version) [1.00ms] +(pass) Finding #7: readMarker PLATFORM_UNSUPPORTED on non-Linux > on non-Linux, readMarker refuses cleanly regardless of on-disk state [3.13ms] +(pass) Finding #4: writeMarker accepts partial sessions object > writeMarker with only appsrv session succeeds and readMarker returns ok [3.49ms] +(pass) Finding #4: writeMarker accepts partial sessions object > writeMarker with empty sessions object still succeeds (uuid is what matters) [3.01ms] +(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > an unrelated OTHER-uid pid whose stat is unreadable does NOT poison the group [0.55ms] +(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > a pid hidden so thoroughly that even its uid is unknown does NOT poison the group [0.65ms] +(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > an OWN-uid pid whose stat is unreadable still fails closed (we cannot rule out membership) [0.64ms] +(pass) Blocker 4: readMarker checks MISSING before PLATFORM_UNSUPPORTED > non-Linux + NO marker file → MISSING (silent legacy fall-through, no scary warning) [0.61ms] +(pass) Blocker 4: readMarker checks MISSING before PLATFORM_UNSUPPORTED > non-Linux + marker file present → PLATFORM_UNSUPPORTED (we genuinely cannot act on it) [8.15ms] +(pass) Blockers 5+6: prepareIdentityForStart > no marker on disk → writes the new marker, reaps nothing [1.09ms] +(pass) Blockers 5+6: prepareIdentityForStart > Blocker 6: a PRESERVED marker is reaped by its OWN uuid before the new one is written [0.93ms] +(pass) Blockers 5+6: prepareIdentityForStart > Blocker 6: if the old generation cannot be reaped, start is BLOCKED and nothing is overwritten [0.52ms] +(pass) Blockers 5+6: prepareIdentityForStart > a marker from a previous BOOT is discarded without a reap (its pids cannot exist) [0.49ms] +(pass) Blockers 5+6: prepareIdentityForStart > an unreadable/suspicious marker BLOCKS start rather than overwriting it [0.69ms] +(pass) Blockers 5+6: prepareIdentityForStart > Blocker 5: the marker is written with an EMPTY sessions object (before any session exists) [0.35ms] +(pass) Blockers 5+6: prepareIdentityForStart > refuses an empty uuid (guards against a silently regenerated identity) [0.34ms] + +src/claude-vendor-env-wiring.test.ts: +(pass) node create captures vendor shell env before profile construction [0.21ms] +(pass) every dotenv-writing create preflights before any node-state side effect [0.26ms] +(pass) the dotenv writer itself reuses the side-effect-free planner [0.17ms] + +src/copresence-cli-wiring.test.ts: +(pass) cli.ts copresence start ordering (structural gate) > the copresence start path really does create tmux sessions with -e (anchor for the tests below) [0.05ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 5: prepareIdentityForStart runs BEFORE the first tmux new-session [0.05ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 5: no marker write happens before the identity preparation call [0.04ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 6: a blocked preparation aborts the start (never falls through to session creation) [0.08ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 12: the tmux capability preflight runs BEFORE the first tmux new-session [0.04ms] +(pass) cli.ts copresence stop wiring (structural gate) > Blockers 1+2: the stop-time reap is given the marker's recorded pids as scope anchors [0.37ms] +(pass) cli.ts copresence stop wiring (structural gate) > marker removal happens only on a successful reap [0.23ms] + +src/grok-copresence-disclosure.test.ts: +(pass) grok co-presence disclosure > default profile reports the exact three tools and no web [0.18ms] +(pass) grok co-presence disclosure > WebSearch profile reports general web_search without widening other tools [0.12ms] +(pass) grok co-presence disclosure > near-match tools are disclosed as invalid rather than a reviewed profile [0.13ms] +(pass) grok co-presence disclosure > resume warns that a changed config cannot mutate the existing session [0.07ms] + +src/opencode-agent-node-pair.test.ts: +(pass) OpenCode agent-node release pairing > pins the exact versions being released together [0.11ms] +(pass) OpenCode agent-node release pairing > rejects latest 2.4.x-style help and accepts the RFC-029 capability [0.07ms] +(pass) OpenCode agent-node release pairing > admits only the exact preview package identity with safe file modes [12.82ms] +(pass) OpenCode agent-node release pairing > skips an exact project-local impersonator and selects the later global package [7.22ms] + +src/batch-workdir.test.ts: +(pass) normalizeBatchWorkdir > expands current-user tilde before a batch changes cwd [0.39ms] +(pass) normalizeBatchWorkdir > anchors a relative workdir once to the caller cwd [0.08ms] +(pass) normalizeBatchWorkdir > keeps an absolute workdir absolute [0.06ms] +(pass) normalizeBatchWorkdir > rejects empty and unsupported named-user shorthands [0.18ms] + +src/copresence-identity.real.test.ts: +(pass) REAL /proc integration (Linux only) > A: scan on real /proc with a nonce uuid does not throw and finds nothing [1.18ms] +(pass) REAL /proc integration (Linux only) > B: live marker member + REAL zombie sibling in the same pgroup → homogeneity ok:true (escalation stays possible) [77.39ms] +(pass) REAL /proc integration (Linux only) > C: readEnviron(1) EACCESes and readOwnerUid(1) is root (non-root only) [0.32ms] +(pass) REAL /proc integration (Linux only) > D: POSITIVE — spawned marker carrier is found by the scan [26.10ms] +(pass) REAL /proc integration (Linux only) > E: END-TO-END — scan → group → homogeneity all succeed on real /proc [30.84ms] +(pass) REAL /proc integration (Linux only) > F: REAL REAP — reapMarkerGroups(realEnumerator, realKiller) kills a real carrier and returns success [378.87ms] +(pass) REAL /proc integration (Linux only) > G: CLEAN-HOST REAP — nothing carries the uuid → success on THIS host (blocker 1 regression) [1.12ms] +(pass) REAL /proc integration (Linux only) > H: NON-DUMPABLE — marker-carrying non-dumpable child of a carrier is accounted for, not dropped (blocker 2) [97.54ms] +(pass) REAL /proc integration (Linux only) > I: readOwnerUid reports the REAL uid of a non-dumpable process (environ inode owner lies) [51.41ms] +(pass) REAL /proc integration (Linux only) > J: REAL START SEAM — prepareIdentityForStart reclaims a live previous generation and installs the new marker [341.06ms] +(pass) REAL /proc integration (Linux only) > K: REAL START SEAM — a previous generation that cannot be reaped BLOCKS the start and its marker survives [76.43ms] +(pass) REAL /proc integration (Linux only) > L: anchorsFromMarker feeds real recorded pane pids into the scope test [26.67ms] + +src/claude-code-cli-tty-preflight.test.ts: +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > body contains the claude-code-cli spawn (anchor for the assertions below) [0.09ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: non-TTY stdin preflight fires BEFORE the claude spawn [0.09ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: non-TTY branch exits non-zero [0.21ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: message names claude-code-cli and recommends --accept-dev-channels first (--tmux listed with its precondition) [0.25ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Success gate: 'session pinned' / 'session saved' only fires on exit code 0 [0.20ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Exit-code propagation: non-zero child exit calls process.exit(code) [0.13ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Spawn-error path: child.on('error') exits non-zero (was silent → false success) [0.19ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > body contains the --tmux branch (anchor) [0.10ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux branch has a headless (no-TTY) codepath (`new-session -d`) [0.17ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: does NOT inherit stdin on detached spawn (was `stdio:"inherit"`) [0.20ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: verifies session liveness after detached spawn [0.22ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: propagates non-zero exit on failure paths [0.40ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: prints attach hint after successful startup [0.32ms] + +src/dashboard-managed-process.test.ts: +(pass) managed Dashboard listener decisions > empty port starts; same healthy managed release remains untouched [0.55ms] +(pass) managed Dashboard listener decisions > only an exact managed stale npx listener may be terminated [0.08ms] +(pass) managed Dashboard listener decisions > unmanaged, ambiguous, reused, foreign, and global listeners fail closed [0.26ms] +(pass) record parser and command identity reject malformed state [0.19ms] + +src/token-cli.test.ts: +(pass) parseTokenCreateName > keeps the legacy positional form [0.26ms] +(pass) parseTokenCreateName > accepts separated and equals --name forms [0.11ms] +(pass) parseTokenCreateName > fails closed for missing, empty, unknown, mixed, or extra operands [0.22ms] + +src/cli-args-wiring.test.ts: +(pass) CLI option and positional parsing share cli-args.ts [3.02ms] + +src/private-state.test.ts: +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 0 [4.55ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 2 [3.35ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 22 [3.31ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 77 [3.79ms] +(pass) #472 private state writer > atomically replaces a legacy 0664 target with a 0600 inode [3.29ms] +(pass) #472 private state writer > replaces a leaf symlink instead of writing through it [7.13ms] +(pass) #472 private state writer > repairs a legacy file and parent before reading [0.66ms] +(pass) #472 private state writer > read repair refuses a symlink instead of chmod-following it [0.64ms] + +src/tmux-capability.test.ts: +(pass) parseTmuxVersion > parses the shapes real tmux builds print [0.43ms] +(pass) parseTmuxVersion > returns null when there is no version to find [0.23ms] +(pass) tmuxSupportsSessionEnv > 3.2 is the floor; the letter suffix is a patch marker and never lifts a version over it [0.14ms] +(pass) tmuxSupportsSessionEnv > major version dominates the minor comparison [0.05ms] +(pass) checkTmuxCapability > too old → actionable verdict naming the required version [0.27ms] +(pass) checkTmuxCapability > tmux absent → missing verdict, not a crash [0.14ms] +(pass) checkTmuxCapability > unparseable version → unknown (permissive: never refuse a tmux that may be fine) [0.07ms] +(pass) checkTmuxCapability > modern tmux → ok [0.05ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > old tmux aborts the start with an explanation [0.42ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > missing tmux aborts the start [0.10ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > modern tmux is silent and does not abort [0.05ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > unknown version warns but does NOT abort [0.46ms] + +src/opencode-launch-env.test.ts: +(pass) hardenOpencodeAgentNodeEnv > restores launcher PATH and strips every pre-entrypoint loader hook [0.41ms] +(pass) hardenOpencodeAgentNodeEnv > does not mutate the caller's env object [0.15ms] +(pass) hardenOpencodeAgentNodeEnv > strips case-variant loader and PATH keys for Windows semantics [0.18ms] + +src/secret-shell-guidance.test.ts: +(pass) #379 secret shell guidance > keeps the existing POSIX export form [0.27ms] +(pass) #379 secret shell guidance > uses PowerShell syntax and quote escaping on Windows [0.07ms] + +src/opencode-auth-login.test.ts: +(pass) OpenCode manual auth-login sandbox > builds deterministic provider-specific API-key login argv [0.50ms] +(pass) OpenCode manual auth-login sandbox > uses a fresh all-XDG tree and strips ambient credentials/config hooks [28.63ms] +(pass) OpenCode manual auth-login sandbox > strictly consumes only the selected provider API record through a private leaf [18.88ms] +(pass) OpenCode manual auth-login sandbox > refuses OAuth, mixed-provider and symlink auth shapes without disclosing secrets [20.12ms] +(pass) OpenCode manual auth-login sandbox > persistent planted DB/log links are never exposed and cleanup never follows descendant links [20.19ms] +(pass) OpenCode manual auth-login sandbox > cleanup unlinks a swapped root symlink but never removes its outside target [23.15ms] +(pass) OpenCode manual auth-login sandbox > cleanup quarantines the tracked inode but leaves a regular root-name replacement untouched [15.70ms] +(pass) OpenCode manual auth-login sandbox > a live tracked root whose literal name ends in deleted is still removed [16.64ms] +(pass) OpenCode manual auth-login sandbox > Linux reports nlink zero for a removed directory retained by fd [9.52ms] +(pass) OpenCode manual auth-login sandbox > cleanup retains inode ownership after bounded failure and succeeds on retry [18.54ms] +(pass) OpenCode manual auth-login sandbox > refuses a concurrent live owner marker [23.57ms] +(pass) OpenCode manual auth-login sandbox > refuses a provider that does not match the node's unique configured preset [10.15ms] +(pass) OpenCode manual auth-login sandbox > prunes a dead owner's stale root without following its planted links [29.51ms] +(pass) OpenCode manual auth-login sandbox > PID reuse does not retain a stale credential root [27.39ms] +(pass) OpenCode manual auth-login sandbox > stale sweep resumes a crash-left quarantine while its owner marker remains [35.39ms] +(pass) OpenCode manual auth-login sandbox > stale sweep removes an empty quarantine left after marker-last deletion [17.73ms] +(pass) OpenCode manual auth-login sandbox > spawn-time revalidation rejects a hostile ancestor discovery candidate [15.62ms] +(pass) OpenCode manual auth-login sandbox > with helper always cleans the fresh root when the action throws [15.89ms] + +src/node-start-help.test.ts: +(pass) #518 node start help exposes the recommended headless flag > real `anet node start --help` names --accept-dev-channels and its operating boundary [172.04ms] +(pass) #518 node start help exposes the recommended headless flag > asking for help performs no node-start work [165.22ms] + +src/claude-code-cli-dependency-preflight.test.ts: +(pass) #485 claude-code-cli dependency preflight > create remains a warning while start fails closed [0.14ms] +(pass) #485 claude-code-cli dependency preflight > dependency refusal runs before launch side effects [0.08ms] + +src/bootstrap-password-db.test.ts: +(pass) bootstrap password database binding > turns the local default into an explicit absolute path [0.62ms] +(pass) bootstrap password database binding > anchors a relative COMMHUB_DB to the hub launch cwd [0.26ms] +(pass) bootstrap password database binding > rejects an unusable default before opening a database [0.31ms] +(pass) bootstrap password database binding > does not invent a SQLite target for a PostgreSQL Hub [0.26ms] +(pass) bootstrap password database binding > updates only the explicitly resolved database, never ambient HOME [61.89ms] +(pass) bootstrap password database binding > child refuses a missing explicit path without falling back to HOME [40.36ms] + +src/gitignore-writeback.test.ts: +(pass) ensureGitignoreRule — file does not exist > creates file with the rule + trailing newline [2.53ms] +(pass) ensureGitignoreRule — file does not exist > trims surrounding whitespace from the rule before writing [0.35ms] +(pass) ensureGitignoreRule — file exists, rule absent > appends rule and reports 'appended' [0.78ms] +(pass) ensureGitignoreRule — file exists, rule absent > adds missing trailing newline before appending [0.72ms] +(pass) ensureGitignoreRule — file exists, rule absent > empty file → appended, not created [0.45ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > exact match returns already-present + does not modify file [0.41ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > trimmed match (rule with surrounding whitespace) treats as present [0.31ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > commented-out rule does NOT count as present [0.34ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > multiple invocations are idempotent (call 3 times) [0.43ms] +(pass) ensureGitignoreRule — multiple distinct rules don't collide > two different rules go to two different lines [0.36ms] +(pass) ensureGitignoreRule — multiple distinct rules don't collide > similar-but-different rules don't false-match (`.anet/` vs `.anet/foo`) [0.41ms] +(pass) ensureGitignoreRules — batch > empty rules list is a no-op [0.29ms] +(pass) ensureGitignoreRules — batch > creates file with all rules on first call [0.40ms] +(pass) ensureGitignoreRules — batch > second batch call is fully idempotent [0.40ms] +(pass) ensureGitignoreRules — batch > partial overlap — only new rules appended [0.42ms] +(pass) ensureGitignoreRule — defensive > empty rule throws [0.30ms] +(pass) ensureGitignoreRule — defensive > whitespace-only rule throws [0.24ms] + +src/secret-shell-guidance-wiring.test.ts: +(pass) #379 create and migrate both use platform-aware secret guidance [3.20ms] + +src/opencode-runtime-binding.test.ts: +(pass) external OpenCode runtime binding > survives regular config runtime downgrade and proves the original exact runtime [10.06ms] +(pass) external OpenCode runtime binding > read returns undefined only for absent state and deterministic keys separate nodes [15.05ms] +(pass) external OpenCode runtime binding > an absent exact leaf does not impose POSIX modes on ordinary runtime state [1.44ms] +(pass) external OpenCode runtime binding > unbound legacy symlink or junction-style node paths remain invisible [5.16ms] +(pass) external OpenCode runtime binding > Windows synthetic permission bits do not disable structural security checks [0.59ms] +(pass) external OpenCode runtime binding > secure removal is idempotent and removes the exact binding [6.35ms] +(pass) external OpenCode runtime binding > secure removal refuses tampered content without unlinking it [5.92ms] +(pass) external OpenCode runtime binding > rejects binding-directory and leaf symlinks [5.70ms] +(pass) external OpenCode runtime binding > rejects dangling binding-root and exact-leaf symlinks [2.77ms] +(pass) external OpenCode runtime binding > rejects permissive modes, hard links, and foreign ownership [6.19ms] +(pass) external OpenCode runtime binding > rejects private but tampered runtime, identity, and extra fields [6.60ms] +(pass) external OpenCode runtime binding > rejects binding roots that overlap the canonical project in either direction [4.02ms] +(pass) external OpenCode runtime binding > a symlinked node workDir cannot remove another project's binding [6.94ms] +(pass) assertOpencodeNodeStateUntracked > allows ordinary non-Git projects [1.64ms] +(pass) assertOpencodeNodeStateUntracked > allows ordinary untracked projects inside a Git worktree checkout [34.35ms] +(pass) assertOpencodeNodeStateUntracked > rejects forged Git worktree file markers [1.50ms] +(pass) assertOpencodeNodeStateUntracked > allows ignored/untracked state but rejects git add -f tracked state [18.22ms] +(pass) assertOpencodeNodeStateUntracked > rejects a force-added dotenv or any tracked file below the node directory [23.51ms] + +src/client.test.ts: +(pass) CommHub.reply calls send_reply MCP tool [2.71ms] + +src/supervise-child.test.ts: +(pass) superviseChild — shutdown gate stops the loop > shutdownGate=true from the start → runOnce never called [0.77ms] +(pass) superviseChild — shutdown gate stops the loop > shutdownGate flips true after first iteration → exactly one runOnce [0.38ms] +(pass) superviseChild — backoff growth + cap > waits double the delay each iteration, capping at maxDelayMs [16.76ms] +(pass) superviseChild — runOnce that returns WITHOUT markStable is treated as failed (regression pin) > runOnce that returns cleanly without markStable → backoff doubles [16.00ms] +(pass) superviseChild — markStable resets backoff > after iteration that calls markStable, next wait is baseDelayMs again [16.03ms] +(pass) superviseChild — markStable resets backoff > markStable called multiple times in one iteration is idempotent [38.63ms] +(pass) superviseChild — abandonAfterMs > calls onAbandon and returns after cumulative downtime exceeds threshold [16.04ms] +(pass) superviseChild — abandonAfterMs > markStable in any iteration resets downtime — abandon never fires [16.06ms] +(pass) superviseChild — runOnce error handling > runOnce throws → onError fires, loop continues [16.04ms] +(pass) superviseChild — runOnce error handling > runOnce throws AND shutdownGate goes true → loop exits, no further iteration [1.81ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=0 → -25% of delay (lower bound) [14.24ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=1 → +25% of delay (upper bound) [16.28ms] +(pass) superviseChild — jitter range > jitterRatio=0 → deterministic waits at exact delay [16.01ms] +(pass) superviseChild — jitter range > waitMs floor 100 enforces minimum wait even with tiny base + negative jitter [16.02ms] +(pass) superviseChild — defensive contract > returns (does not throw) when runOnce never resolves and shutdown flips [3.39ms] + +src/claude-vendor-env.test.ts: +(pass) collectClaudeVendorEnvForCreate > captures known vendor endpoint and credential for claude-agent-sdk [0.39ms] +(pass) collectClaudeVendorEnvForCreate > explicit --env value wins without duplicate capture [0.12ms] +(pass) collectClaudeVendorEnvForCreate > does not capture vendor variables for another runtime [0.04ms] +(pass) collectClaudeVendorEnvForCreate > rejects line-oriented dotenv injection [0.21ms] +(pass) collectClaudeVendorEnvForCreate > rejects line breaks in explicit --env for every runtime [0.19ms] +(pass) planPlainSecretEnvRewrites > plans the exact dotenv assignment without mutating the profile [0.37ms] +(pass) planPlainSecretEnvRewrites > rejects a secret dotenv value with CRLF before any caller mutation [0.22ms] + +src/locale-diagnostic-wiring.test.ts: +(pass) #68 doctor reports the pure locale diagnostic as a warning [5.65ms] + +src/primary-network.test.ts: +(pass) resolvePrimaryNetwork > uses current_network even when the network list is reversed and renamed [0.91ms] +(pass) resolvePrimaryNetwork > fails explicitly when current_network is missing instead of guessing networks[0] [0.51ms] +(pass) resolvePrimaryNetwork > turns transport and HTTP failures into explicit resolution errors [0.38ms] +(pass) debate, demo-social, and pr-review all use the shared resolver [2.60ms] + +src/opencode-preset.test.ts: +(pass) OPENCODE_PRESETS registry > exports the two blessed presets (anthropic + openai) [0.10ms] +(pass) OPENCODE_PRESETS registry > findOpencodePreset('anthropic') returns the record; unknown returns null [0.05ms] +(pass) readPresetKeyFromEnv — env-only, no interactive prompt > returns the trimmed key when the env var is set [0.14ms] +(pass) readPresetKeyFromEnv — env-only, no interactive prompt > returns null when the env var is missing / empty [0.10ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > body shape matches opencode auth.json convention [0.55ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > writes to /.local/share/opencode/auth.json with mode 0o600 [5.45ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > writeOpencodeConfigJson lands under .config/opencode with 0o600 [5.36ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > keyless create atomically clears a private pre-planted auth file [5.27ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > default tool policy disables filesystem, shell, task, and skill tools [0.25ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > writes only blessed provider identity and strips all pre-planted routing/executable config [5.52ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > atomically replaces a private but invalid pre-planted config without parsing it [5.40ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > rejects symlink escapes in workDir, config/data ancestors, and final targets [6.76ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > validates the full tree before mutation so a bad auth side cannot partially rewrite config [0.98ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > rejects permissive modes and foreign owners without chmod-follow repair [2.35ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > prepares .anet/nodes/node before profile secrets and provides atomic private leaf I/O [33.75ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > accepts an ordinary 0775 project root for a non-root uid=gid private group [7.82ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > profile preflight rejects .anet/nodes/node and config/.env symlink chains before secret writes [8.34ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > profile preflight rejects writable ancestors and non-private node roots [2.47ms] + +src/opencode-smoke-env.test.ts: +(pass) buildOpencodeSmokeEnv > locks the exact hardened ancestor candidate set [1.16ms] +(pass) buildOpencodeSmokeEnv > rejects sticky world-writable /tmp instead of silently degrading [0.58ms] +(pass) buildOpencodeSmokeEnv > inherits only transport/locale trust settings and controls all OpenCode roots [0.82ms] +(pass) buildOpencodeSmokeEnv > every writable root can be precreated private, including XDG_RUNTIME_DIR [1.13ms] + +src/grok-attach-client.test.ts: +(pass) validateGrokAttachSocket rejects symlinks, non-sockets, and foreign owners [3.40ms] +(pass) connectGrokAttach bridges base64 terminal I/O, status, resize, and detach [9.72ms] +(pass) connectGrokAttach splits large input so every NDJSON frame stays bounded [1.17ms] +(pass) connectGrokAttach fails closed on an invalid handshake and oversized frame [1.39ms] +(pass) a single-client rejection before hello preserves the server error [0.62ms] +(pass) hello followed by a fatal frame in the same chunk cannot return a dead session [0.73ms] +(pass) detach force-closes a peer that never completes its half-close [13.12ms] +(pass) callback failure and invalid limits fail before returning an attached client [1.48ms] +(pass) remote detach is surfaced and closes without echoing a detach frame [1.01ms] + +src/grok-copresence-profile.test.ts: +(pass) Grok copresence profile defaults > builds the Grok agent-node parent environment from an exact empty allowlist [2.65ms] +(pass) Grok copresence profile defaults > does not mistake an old headless-only agent-node for co-presence support [0.16ms] +(pass) Grok copresence profile defaults > builds the npm resolver environment from an exact empty allowlist [0.67ms] +(pass) Grok copresence profile defaults > prepares two distinct empty owner-only npm config files without following symlinks [1.99ms] +(pass) Grok copresence profile defaults > enables copresence only for non-headless grok-build-cli [0.60ms] +(pass) Grok copresence profile defaults > uses the owner-bound state home even when XDG is owner-only [0.53ms] +(pass) Grok copresence profile defaults > falls back to a bounded owner tmp path when the state home is too long [0.34ms] + +src/opencode-copresence-cli.test.ts: +(pass) OpenCode co-presence CLI wiring > persists copresence mode before launching the bridge [0.06ms] +(pass) OpenCode co-presence CLI wiring > starts only exact alias and alias-bridge tmux sessions [0.07ms] +(pass) OpenCode co-presence CLI wiring > does not depend on a long-lived tmux server's stale launcher environment [0.05ms] +(pass) OpenCode co-presence CLI wiring > waits for the owner-only runtime launcher before starting the official TUI [0.06ms] +(pass) OpenCode co-presence CLI wiring > the generic --copresence dispatcher selects OpenCode by stored runtime [0.19ms] +(pass) OpenCode co-presence CLI wiring > operator help names the create, attach, and stop commands [0.99ms] +(pass) OpenCode co-presence CLI wiring > prints an exact tmux target so an exited TUI cannot prefix-match the bridge [0.07ms] + +src/locale-diagnostic.test.ts: +(pass) #68 locale diagnostic > LC_ALL overrides an otherwise UTF-8 LANG [2.06ms] +(pass) #68 locale diagnostic > LC_CTYPE overrides LANG when LC_ALL is empty [0.14ms] +(pass) #68 locale diagnostic > accepts common UTF-8 spellings [0.09ms] +(pass) #68 locale diagnostic > warns for POSIX, C, non-UTF-8, and unset locale [0.12ms] +(pass) #68 locale diagnostic > does not prescribe POSIX locale variables on Windows [0.05ms] +(pass) #68 locale diagnostic > renders locale values without terminal control or unbounded output [0.24ms] + +src/opencode-package-binary.test.ts: +(pass) validateOpencodePackageBinary > accepts only the canonical exact npm package entrypoint [3.11ms] +(pass) validateOpencodePackageBinary > rejects a same-version package impersonator inside the project [1.40ms] +(pass) validateOpencodePackageBinary > skips a same-version project shim and selects a later trusted package [2.06ms] +(pass) validateOpencodePackageBinary > rejects a monorepo-root package when invoked from a nested app [3.96ms] +(pass) validateOpencodePackageBinary > ordinary 0664 checkout package.json does not abort boundary discovery [2.55ms] +(pass) validateOpencodePackageBinary > accepts both exact registry spellings of bin.opencode [3.02ms] +(pass) validateOpencodePackageBinary > rejects forged name, version, and bin metadata [2.99ms] +(pass) validateOpencodePackageBinary > rejects world-writable files and package ancestors [2.37ms] +(pass) validateOpencodePackageBinary > rejects a symlinked package.json even when its contents are exact [0.96ms] +(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd 为文件系统根时,禁止根含 / —— 与任何包路径都重叠 [0.19ms] +(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd=/ 时,一个各方面都合法的包也会被拒 [2.44ms] +(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd 是全局安装前缀的祖先时,全局安装的包被判成项目本地 [1.27ms] +(pass) #739 cwd 参与信任判定 > 这条守卫要防的东西必须继续被防住(修 #739 时不许放宽它) [0.89ms] + +src/im/access-resolve.test.ts: +(pass) normalizeAllowFrom — input shapes > real string[] passes through deduped (filter empty strings) [1.97ms] +(pass) normalizeAllowFrom — input shapes > undefined → empty + not malformed [0.06ms] +(pass) normalizeAllowFrom — input shapes > null → empty + not malformed [0.03ms] +(pass) normalizeAllowFrom — input shapes > non-array object → empty + malformed (corrupted access.json shape) [0.05ms] +(pass) normalizeAllowFrom — input shapes > string instead of array → malformed [0.05ms] +(pass) normalizeAllowFrom — input shapes > array with non-string elements drops them [0.07ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > empty array → deny with empty-fail-closed kind [0.18ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > undefined → deny [0.06ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > malformed → deny + reason mentions malformed [0.11ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*'] alone allows any sender [0.05ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*', 'specific_id'] still wildcard-allows (wins precedence) [0.04ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderId in list → allow [0.11ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderUsername match (no id match) → allow [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > neither id nor username in list → deny [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > empty senderUsername doesn't accidentally match empty list entry [0.04ms] +(pass) resolveTelegramAccess — explicit id / username matching > blank-string id with username match still allows [0.07ms] +(pass) resolveTelegramAccess — explicit id / username matching > production-shape: bare username (no @) in allowFrom matches bare msg.from.username [0.09ms] +(pass) resolveTelegramAccess — explicit id / username matching > production-shape mismatch: @vansin in allowFrom does NOT match bare vansin payload [0.04ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > empty allowFrom → deny [0.22ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > wildcard allows [0.06ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > specific id allows [0.05ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > sender not in list → deny [0.05ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > empty allowChats → fail-closed [0.10ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=all → allow [0.32ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=observe → deny [0.07ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat NOT in allowChats → deny (even with policy=all) [0.14ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > wildcard chats opens any chat (with groupPolicy=all) [0.06ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > groupPolicy=mention allows (caller decides at message inspect time) [0.05ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for empty allowFrom [0.13ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for malformed allowFrom + mentions malformed [0.06ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null when allowFrom has at least one entry [0.05ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null for wildcard-allow (channel intentionally open) [0.03ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader stores raw allowFrom verbatim — no normalization at load time [0.13ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is missing [0.08ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is malformed (non-array) [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader is silent when allowFrom has at least one entry (even if numeric) [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123] alone (numeric sender id from a misformatted access.json) → loader+resolver fail-closed [0.08ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null] (corrupted access.json) → loader+resolver fail-closed [0.06ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [{}] (object instead of id string) → loader+resolver fail-closed [0.06ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123, '@vansin'] (mixed) → '@vansin' still allowed, numeric '123' rejected [0.08ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null, '*'] (mixed wildcard) → wildcard wins despite garbage entries [0.05ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > missing access.json entirely (loader gets null) → fail-closed [0.08ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > empty array NEVER allows [0.06ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > undefined NEVER allows [0.04ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > null NEVER allows [0.03ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > object-shape (corrupted) NEVER allows [0.03ms] + +src/im/feishu/adapter-lifecycle.test.ts: +(pass) FeishuAdapter WS lifecycle > SDK start resolution is not readiness; missing onReady times out fail-closed [22.28ms] +(pass) FeishuAdapter WS lifecycle > onReady is the only initial online authority [1.62ms] +(pass) FeishuAdapter WS lifecycle > initial onError rejects and scrubs credentials [1.63ms] +(pass) FeishuAdapter WS lifecycle > initial onError scrubs arbitrary Lark access-token shapes [1.64ms] +(pass) FeishuAdapter WS lifecycle > spurious reconnect before first ready cannot mark health connected [17.29ms] +[2026-08-13T00:27:07.115Z] [feishu:audit] error from=? conv=? — inbound [redacted] Bearer [redacted] +(pass) FeishuAdapter WS lifecycle > inbound handler errors use the same token scrub before health [4.32ms] +(pass) FeishuAdapter WS lifecycle > reconnecting lowers health and reconnected restores it [3.61ms] +(pass) FeishuAdapter WS lifecycle > terminal error after ready lowers health and notifies worker owner once [1.61ms] +(pass) FeishuAdapter WS lifecycle > stop closes the public SDK client and invalidates late callbacks [1.65ms] +(pass) worker terminal owner logs safely and exits non-zero [0.31ms] + + 438 pass + 0 fail + 1333 expect() calls +Ran 438 tests across 46 files. [4.13s] +executed_files=46 discovered_files=46 +[L0b] every agent-network/tests file, dispatched by kind +tests_dir_executed=19 tests_dir_discovered=19 tests_dir_failed=0 +[L1] witnessed-red: top-level config help must match the implemented parser +Expected to contain: "anet config [path|json]" +MUTATION_RED stale-config-help rc=1 +RESULT: PASS +``` diff --git a/tests/test725-agent-node-unit-ci/run.sh b/tests/test725-agent-node-unit-ci/run.sh index 40a64ad6e..5f26509d3 100644 --- a/tests/test725-agent-node-unit-ci/run.sh +++ b/tests/test725-agent-node-unit-ci/run.sh @@ -27,6 +27,43 @@ grep -Eq '^[[:space:]]*0 fail$' /tmp/test725-green.log || { exit 1 } + +# tests/ 下还有 6 个文件,直到现在没有任何 CI 会跑 —— 而这道门的抬头写着 +# "complete agent-node unit domain"。补上,让那句话变成真的。 +# +# 这个目录里混着两种测试,任何单一命令都跑不全: +# - 脚本式:自己打 "N/N passed",失败时 process.exit(1),必须 `bun `; +# 用 `bun test` 跑会因为 top-level 的 process.exit 把整个 run 打断在第一个文件。 +# - bun:test 式:describe/it,必须 `bun test `;用 `bun ` 跑会报 +# "Cannot use describe outside of the test runner"。 +# 所以按文件内容分派。 +echo "[L0b] every agent-node/tests file, dispatched by kind" +tdir_total=$(find "$ROOT/agent-node/tests" -maxdepth 1 -type f -name '*.test.ts' | wc -l | tr -d ' ') +tdir_ran=0; tdir_failed=0; tdir_names="" +while IFS= read -r f; do + rel=${f#"$ROOT"/agent-node/} + if grep -q 'bun:test' "$f"; then cmd="bun test $rel"; else cmd="bun $rel"; fi + if runuser -u node -- env HOME=/home/node \ + bash -lc "cd $ROOT/agent-node && $cmd" >"/tmp/test725-tests-$(basename "$f" .test.ts).log" 2>&1; then + tdir_ran=$((tdir_ran+1)) + else + tdir_ran=$((tdir_ran+1)); tdir_failed=$((tdir_failed+1)) + tdir_names="$tdir_names $(basename "$f" .test.ts)" + echo "--- FAILED: agent-node/$rel ---" + tail -20 "/tmp/test725-tests-$(basename "$f" .test.ts).log" + fi +done < <(find "$ROOT/agent-node/tests" -maxdepth 1 -type f -name '*.test.ts' | sort) + +echo "tests_dir_executed=$tdir_ran tests_dir_discovered=$tdir_total tests_dir_failed=$tdir_failed" +[[ "$tdir_ran" -eq "$tdir_total" && "$tdir_total" -gt 0 ]] || { + echo "FAIL: ran $tdir_ran of $tdir_total files under agent-node/tests" >&2 + exit 1 +} +[[ "$tdir_failed" -eq 0 ]] || { + echo "FAIL: $tdir_failed file(s) failed under agent-node/tests:$tdir_names" >&2 + exit 1 +} + echo "[L1] witnessed-red: disconnect readable attachment content from runtime" TARGET=$'deliverToRuntime: () => processTask(\n runtimeContent,' MUTATED=$'deliverToRuntime: () => processTask(\n content,' diff --git a/tests/test745-agent-network-unit-ci/Dockerfile b/tests/test745-agent-network-unit-ci/Dockerfile index ca29fd9c0..067a6f39c 100644 --- a/tests/test745-agent-network-unit-ci/Dockerfile +++ b/tests/test745-agent-network-unit-ci/Dockerfile @@ -22,13 +22,18 @@ COPY agent-network/package.json agent-network/package-lock.json ./agent-network/ RUN cd agent-network && npm ci COPY agent-node/package.json ./agent-node/package.json +# tests/feishu-envelope-compat.test.ts 跨包 import agent-node 的 runtime 源码。 +COPY agent-node/src ./agent-node/src COPY agent-network ./agent-network COPY tests/test745-agent-network-unit-ci/run.sh ./tests/test745-agent-network-unit-ci/run.sh ARG SOURCE_COMMIT ENV TEST745_SOURCE_COMMIT=$SOURCE_COMMIT -RUN chmod 0755 ./tests/test745-agent-network-unit-ci/run.sh \ +# tests/feishu-bridge-ipc.test.ts 把附件落在硬编码的 /work/feishu-attachments 下, +# 不是 workspace 相对路径。给 node 建出来,否则它红在 EACCES 上、看着像产品坏。 +RUN install -d -o node -g node -m 0755 /work \ + && chmod 0755 ./tests/test745-agent-network-unit-ci/run.sh \ && install -d -o node -g node -m 0700 "/run/user/$(id -u node)" \ && chown -R node:node /workspace diff --git a/tests/test745-agent-network-unit-ci/run.sh b/tests/test745-agent-network-unit-ci/run.sh index ae46f0fc5..be889fd32 100644 --- a/tests/test745-agent-network-unit-ci/run.sh +++ b/tests/test745-agent-network-unit-ci/run.sh @@ -48,6 +48,43 @@ echo "executed_files=${executed:-unknown} discovered_files=$test_files" exit 1 } + +# tests/ 下还有 19 个文件,直到现在没有任何 CI 会跑 —— 而这道门的抬头写着 +# "complete agent-network unit domain"。补上,让那句话变成真的。 +# +# 这个目录里混着两种测试,任何单一命令都跑不全: +# - 脚本式:自己打 "N/N passed",失败时 process.exit(1),必须 `bun `; +# 用 `bun test` 跑会因为 top-level 的 process.exit 把整个 run 打断在第一个文件。 +# - bun:test 式:describe/it,必须 `bun test `;用 `bun ` 跑会报 +# "Cannot use describe outside of the test runner"。 +# 所以按文件内容分派。 +echo "[L0b] every agent-network/tests file, dispatched by kind" +tdir_total=$(find "$ROOT/agent-network/tests" -maxdepth 1 -type f -name '*.test.ts' | wc -l | tr -d ' ') +tdir_ran=0; tdir_failed=0; tdir_names="" +while IFS= read -r f; do + rel=${f#"$ROOT"/agent-network/} + if grep -q 'bun:test' "$f"; then cmd="bun test $rel"; else cmd="bun $rel"; fi + if runuser -u node -- env HOME=/home/node \ + bash -lc "cd $ROOT/agent-network && $cmd" >"/tmp/test745-tests-$(basename "$f" .test.ts).log" 2>&1; then + tdir_ran=$((tdir_ran+1)) + else + tdir_ran=$((tdir_ran+1)); tdir_failed=$((tdir_failed+1)) + tdir_names="$tdir_names $(basename "$f" .test.ts)" + echo "--- FAILED: agent-network/$rel ---" + tail -20 "/tmp/test745-tests-$(basename "$f" .test.ts).log" + fi +done < <(find "$ROOT/agent-network/tests" -maxdepth 1 -type f -name '*.test.ts' | sort) + +echo "tests_dir_executed=$tdir_ran tests_dir_discovered=$tdir_total tests_dir_failed=$tdir_failed" +[[ "$tdir_ran" -eq "$tdir_total" && "$tdir_total" -gt 0 ]] || { + echo "FAIL: ran $tdir_ran of $tdir_total files under agent-network/tests" >&2 + exit 1 +} +[[ "$tdir_failed" -eq 0 ]] || { + echo "FAIL: $tdir_failed file(s) failed under agent-network/tests:$tdir_names" >&2 + exit 1 +} + echo "[L1] witnessed-red: top-level config help must match the implemented parser" TARGET=' anet config [path|json] Show config summary, path, or raw JSON' MUTATED=' anet config get|set Inspect or edit config' From 4031216eafa354346f046b25ae75905cd7d745a0 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 10:07:52 +0800 Subject: [PATCH 04/11] =?UTF-8?q?docs(tests):=20report-only=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E9=94=9A=E7=82=B9=2046e752c3(=E5=90=AB=20current?= =?UTF-8?q?=20main=20034f0064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按独审要求重做 provenance:append current main → 在精确源码提交上重跑 → report-only 子提交。 main 的新增提交 #802 只动 tests/qa-180-rename-ghost/,与本 PR 四个文件零相交, rebase 无冲突;qa.yml 两处 path(test746 / test798)都保留; server 步骤已是独立 job server-unit(name="server unit (Docker, non-root)", timeout 12), 不再嵌在 agent-network-unit 里。 实测:executed_files=69 discovered_files=69 failed_files=0, MUTATION_RED registration-password-floor-weakened rc=1,RESULT: PASS。 --- docs/tests/report-test798-server-unit-ci.txt | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/tests/report-test798-server-unit-ci.txt b/docs/tests/report-test798-server-unit-ci.txt index bce99d7c2..b8418393a 100644 --- a/docs/tests/report-test798-server-unit-ci.txt +++ b/docs/tests/report-test798-server-unit-ci.txt @@ -1,23 +1,21 @@ -# test798 — server 聚合单测门 -source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +# test798 —— server 聚合单测门 +source_commit=46e752c3d9815a31902c5cc0a57b34deb898ffa2 +base(current main)=034f00647d42d38d5086d7fc057eb7824a441791 -## 落地前的实测(为什么需要它,以及踩过的三层坑) -server/src 69 个测试,CI 此前只点名跑 6 个(L0_TESTS 5 + test686 引用 1)。 +锚点即被测代码那一版:本文件是它的 report-only 子提交。 +早先两份报告的锚点分别是 92d96129… 和 ecf4679c…(此处刻意不写成 key=value 形式), +都不是当时的分支 head —— 前者是我把 --build-arg 传成了 origin/main,后者是 rebase 后失效。 -第一版(共享一个 DB、cwd=server/):895 pass / 6 fail。逐条查下来全不是产品坏: - - 5 条 harness:hub↔daemon 漂移门要读 agent-node/src/shared,镜像里没有 - - 1 条 harness:task-lifecycle-watcher 用 process.cwd() 拼 ./server/src/db.js,要求 cwd=仓根 - - 3 条 harness:scheduled-tasks 要 tests/test601-.../race-worker.ts - - 剩下的是共享 DB 造成的跨文件污染(单跑全绿,聚合红) - -第二版(按既有契约逐文件独立 DB):红的换成另外 2 个,与第一版不相交 —— -它们跨包 import agent-node 的 reply-reliability / inbox-dispatch,镜像里没带。 +## 落地前那两轮红(没有一条是产品缺陷) +第一版(共享 DB、cwd=server/)895 pass / 6 fail:5 条 harness(缺 agent-node/src/shared)、 +1 条 cwd 契约(process.cwd() 拼 ./server/src/db.js)、3 条缺 test601 race-worker、余下共享 DB 污染。 +第二版(逐文件独立 DB)红的换成另外 2 个、与第一版不相交(跨包 import reply-reliability / inbox-dispatch)。 补齐后 69/69 全绿。 -## 最终输出 +## 本次输出 ``` # test798 — complete server unit domain -source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +source_commit=46e752c3d9815a31902c5cc0a57b34deb898ffa2 bun=1.3.14 node=v22.23.2 uid=1000 commhub_db=/tmp/test798-server-unit.db test_files=69 From bdfd4322598e73739d550476afa2480a9777b20c Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 10:14:26 +0800 Subject: [PATCH 05/11] =?UTF-8?q?docs(tests):=20report-only=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E9=94=9A=E7=82=B9=20a4fd375f(=E5=90=AB=20current?= =?UTF-8?q?=20main=20034f0064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按独审 SUPERSEDE 的要求重做 provenance。原 CLEAN 判定被撤回是对的: 上一份报告声称的锚点 92d96129 比本 PR base 还早两个提交,那上面的 run.sh 里 没有 tests_dir_executed 那段代码,报告内容 provably 不可能由它产出。 根因是我跑门时 --build-arg SOURCE_COMMIT 传的是当时的 origin/main。 实测:test725 tests_dir 6/6/0 + MUTATION_RED + PASS; test745 tests_dir 19/19/0 + MUTATION_RED + PASS。 --- docs/tests/report-pkg-tests-dir-gate.txt | 3090 +++++++++++----------- 1 file changed, 1542 insertions(+), 1548 deletions(-) diff --git a/docs/tests/report-pkg-tests-dir-gate.txt b/docs/tests/report-pkg-tests-dir-gate.txt index 5f21938e0..43fb9376f 100644 --- a/docs/tests/report-pkg-tests-dir-gate.txt +++ b/docs/tests/report-pkg-tests-dir-gate.txt @@ -1,1009 +1,1003 @@ # test725/test745 扩到 tests/ 目录 -source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +source_commit=a4fd375f2b2e4f35e1a1dcea0a5f093f1439796a +base(current main)=034f00647d42d38d5086d7fc057eb7824a441791 -## 为什么 -两个门的抬头都写着 complete X unit domain,却把 tests/ 下 25 个文件排除在外。 +锚点即被测代码那一版;本文件是它的 report-only 子提交。 +早先两份的锚点是 92d9612949… 和 2b2a7c2b…(此处刻意不写成 key=value 形式): +前者是我把 --build-arg 传成了当时的 origin/main —— 独审(通信IM马)据此撤回 CLEAN 判定,指控成立; +后者在 rebase 到 current main 后失效。 -## 这个目录的形状(混着两种测试,任何单一命令都跑不全) - agent-network/tests 19 个:bun:test 式 3 + 脚本式 16 - agent-node/tests 6 个:全是脚本式 - 脚本式用 bun test 跑 → top-level process.exit 把整个 run 打断在第一个文件 - bun:test 式用 bun 跑 → Cannot use describe outside of the test runner +## 这个目录为什么要按内容分派 +脚本式(22 个)失败时 process.exit(1),必须 bun ;用 bun test 跑会被 top-level 的 +process.exit 打断在第一个文件。bun:test 式(3 个)反之。退出码可用已先验:失败确实非零。 -## 落地前实测 - agent-node/tests:6/6 直接过 - agent-network/tests:单命令跑 14/19;按内容分派后 17/19;补两处环境契约后 19/19 - - feishu-envelope-compat 跨包 import agent-node/src/runtime/feishu-envelope(镜像没带) - - feishu-bridge-ipc 硬编码绝对路径 /work/feishu-attachments(容器里 node 建不了 → EACCES) - -## 最终输出 -### test725 +## test725(agent-node) ``` # test725 — complete agent-node unit domain -source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +source_commit=a4fd375f2b2e4f35e1a1dcea0a5f093f1439796a bun=1.3.14 node=v22.23.2 uid=1000 [L0] full agent-node/src unit suite as non-root bun test v1.3.14 (0d9b296a) src/inbox-message-policy.test.ts: -(pass) atomic peer reply inbox policy > ordinary work still expects a response [0.21ms] -(pass) atomic peer reply inbox policy > a peer reply is actionable but cannot start reply ping-pong [0.05ms] +(pass) atomic peer reply inbox policy > ordinary work still expects a response [0.71ms] +(pass) atomic peer reply inbox policy > a peer reply is actionable but cannot start reply ping-pong [0.06ms] (pass) atomic peer reply inbox policy > plain informational messages retain ack-only behavior [0.05ms] src/external-schedules.test.ts: -(pass) external schedule manifest > reports an exact bounded shape and strips host paths to basename [1.18ms] -(pass) external schedule manifest > missing manifest is an explicit empty observation; config-less legacy stays omitted [1.01ms] -(pass) external schedule manifest > unknown keys, duplicate ids, invalid timestamps, and oversized lists fail closed [0.72ms] -(pass) external schedule manifest > symlink manifest never follows the target [0.65ms] -(pass) external schedule manifest > editable/revision are derived only from a verified managed crontab under the process gate [2.66ms] +(pass) external schedule manifest > reports an exact bounded shape and strips host paths to basename [1.36ms] +(pass) external schedule manifest > missing manifest is an explicit empty observation; config-less legacy stays omitted [2.13ms] +(pass) external schedule manifest > unknown keys, duplicate ids, invalid timestamps, and oversized lists fail closed [0.96ms] +(pass) external schedule manifest > symlink manifest never follows the target [0.66ms] +(pass) external schedule manifest > editable/revision are derived only from a verified managed crontab under the process gate [3.37ms] src/inbox-skip-log.test.ts: -(pass) formatInboxSkipLog > self-message diagnostics identify the routing layer and full task [0.09ms] -(pass) formatInboxSkipLog > all inbound filter reasons produce an actionable INFO-safe line [0.13ms] -(pass) formatInboxSkipLog > the formatter has no message-content input [0.05ms] +(pass) formatInboxSkipLog > self-message diagnostics identify the routing layer and full task [0.23ms] +(pass) formatInboxSkipLog > all inbound filter reasons produce an actionable INFO-safe line [0.16ms] +(pass) formatInboxSkipLog > the formatter has no message-content input [0.06ms] src/owner-schedule-consumer.test.ts: -(pass) process-gated owner schedule consumer > disabled process registers no poll and makes zero network/host calls [0.83ms] -(pass) process-gated owner schedule consumer > exact node intent applies once, ACKs, and deletes journal only after ACK [8.13ms] -(pass) process-gated owner schedule consumer > foreign-node intent and invalid authority shape never reach crontab [0.73ms] -(pass) process-gated owner schedule consumer > lost ACK keeps journal; same delivered intent recovers without a second install [6.01ms] +(pass) process-gated owner schedule consumer > disabled process registers no poll and makes zero network/host calls [1.20ms] +(pass) process-gated owner schedule consumer > exact node intent applies once, ACKs, and deletes journal only after ACK [17.01ms] +(pass) process-gated owner schedule consumer > foreign-node intent and invalid authority shape never reach crontab [0.98ms] +(pass) process-gated owner schedule consumer > lost ACK keeps journal; same delivered intent recovers without a second install [8.29ms] src/codex-model-default.test.ts: -(pass) agent-node Codex model resolution > missing model uses the verified supported default [0.05ms] -(pass) agent-node Codex model resolution > explicit model remains authoritative [0.02ms] +(pass) agent-node Codex model resolution > missing model uses the verified supported default [0.06ms] +(pass) agent-node Codex model resolution > explicit model remains authoritative [0.03ms] src/claude-tool-aliases.test.ts: -(pass) Claude CommHub tool aliases > pins the exact registered in-process CommHub tool set [0.09ms] -(pass) Claude CommHub tool aliases > does not advertise aliases when the in-process server failed [0.05ms] +(pass) Claude CommHub tool aliases > pins the exact registered in-process CommHub tool set [0.08ms] +(pass) Claude CommHub tool aliases > does not advertise aliases when the in-process server failed [0.08ms] src/reply-reliability.test.ts: -(pass) classifyCommHubResponse > returns ok with parsed application payload (the happy path) [0.26ms] -(pass) classifyCommHubResponse > JSON-RPC error envelope → retryable CommHubError [0.16ms] -(pass) classifyCommHubResponse > MCP result.isError → retryable CommHubError [0.20ms] -(pass) classifyCommHubResponse > real legacy Hub unknown-tool result preserves the MCP code [0.10ms] -(pass) classifyCommHubResponse > application-level ok:false → appLevel CommHubError (NON-retryable) [0.14ms] -(pass) classifyCommHubResponse > non-JSON tool text is passed through verbatim [0.20ms] -(pass) classifyCommHubResponse > data with neither error nor result returns ok with the raw data [0.06ms] -(pass) CommHubError > instances are distinguishable from generic Error via instanceof [0.07ms] -(pass) CommHubError > appLevel flag survives the throw/catch round trip [0.10ms] -(pass) PendingReplyQueue > load() returns empty array when file does not exist [0.65ms] -(pass) PendingReplyQueue > persist + load round-trips an entry with attempts=0 [4.01ms] -(pass) PendingReplyQueue > final persistence boundary scrubs known, shaped, assignment and error credentials [3.34ms] -(pass) PendingReplyQueue > direct save cannot bypass scrub and leaves no sibling temp artifact [2.51ms] -(pass) PendingReplyQueue > load migrates an old broad-mode queue without leaving raw credential bytes [3.30ms] -(pass) PendingReplyQueue > load repairs a broad mode even when content needs no rewrite [0.70ms] -(pass) PendingReplyQueue > accepts the same process-wide redactor used by ordinary log call sites [2.29ms] +(pass) classifyCommHubResponse > returns ok with parsed application payload (the happy path) [0.28ms] +(pass) classifyCommHubResponse > JSON-RPC error envelope → retryable CommHubError [0.19ms] +(pass) classifyCommHubResponse > MCP result.isError → retryable CommHubError [0.22ms] +(pass) classifyCommHubResponse > real legacy Hub unknown-tool result preserves the MCP code [0.17ms] +(pass) classifyCommHubResponse > application-level ok:false → appLevel CommHubError (NON-retryable) [0.11ms] +(pass) classifyCommHubResponse > non-JSON tool text is passed through verbatim [0.14ms] +(pass) classifyCommHubResponse > data with neither error nor result returns ok with the raw data [0.07ms] +(pass) CommHubError > instances are distinguishable from generic Error via instanceof [0.06ms] +(pass) CommHubError > appLevel flag survives the throw/catch round trip [0.11ms] +(pass) PendingReplyQueue > load() returns empty array when file does not exist [0.74ms] +(pass) PendingReplyQueue > persist + load round-trips an entry with attempts=0 [7.21ms] +(pass) PendingReplyQueue > final persistence boundary scrubs known, shaped, assignment and error credentials [4.27ms] +(pass) PendingReplyQueue > direct save cannot bypass scrub and leaves no sibling temp artifact [2.72ms] +(pass) PendingReplyQueue > load migrates an old broad-mode queue without leaving raw credential bytes [2.36ms] +(pass) PendingReplyQueue > load repairs a broad mode even when content needs no rewrite [0.59ms] +(pass) PendingReplyQueue > accepts the same process-wide redactor used by ordinary log call sites [2.23ms] (pass) PendingReplyQueue > invalid legacy content is securely replaced with an empty 0600 queue [2.40ms] -(pass) PendingReplyQueue > persist is idempotent on (to, taskId) — attempts counter preserved [5.76ms] -(pass) PendingReplyQueue > clear removes only the matching (to, taskId) [8.57ms] -(pass) PendingReplyQueue.drain > delivers every entry on success and persists an empty queue [6.96ms] -(pass) PendingReplyQueue.drain > transient failure requeues with attempts++ and lastError [4.97ms] -(pass) PendingReplyQueue.drain > transient error text is scrubbed before it reaches disk [4.57ms] -(pass) PendingReplyQueue.drain > app-level CommHubError is dropped loud — not retried, not requeued [6.19ms] -(pass) PendingReplyQueue.drain > drain on empty queue is a no-op and does not write the file [0.53ms] -(pass) PendingReplyQueue.drain > file format is stable JSON — readable by an operator after a crash [2.15ms] -(pass) quickHash > is deterministic [0.27ms] -(pass) quickHash > differs across inputs [0.07ms] +(pass) PendingReplyQueue > persist is idempotent on (to, taskId) — attempts counter preserved [10.52ms] +(pass) PendingReplyQueue > clear removes only the matching (to, taskId) [17.05ms] +(pass) PendingReplyQueue.drain > delivers every entry on success and persists an empty queue [13.25ms] +(pass) PendingReplyQueue.drain > transient failure requeues with attempts++ and lastError [16.84ms] +(pass) PendingReplyQueue.drain > transient error text is scrubbed before it reaches disk [4.80ms] +(pass) PendingReplyQueue.drain > app-level CommHubError is dropped loud — not retried, not requeued [11.34ms] +(pass) PendingReplyQueue.drain > drain on empty queue is a no-op and does not write the file [1.02ms] +(pass) PendingReplyQueue.drain > file format is stable JSON — readable by an operator after a crash [3.61ms] +(pass) quickHash > is deterministic [0.28ms] +(pass) quickHash > differs across inputs [0.06ms] (pass) quickHash > returns 32-char hex [0.10ms] src/controlled-upload.test.ts: -(pass) normalizeUploadName > strips directories and control chars [0.78ms] -(pass) resolveControlledUploadPath — NUL live guard > rejects embedded NUL before any fs access [0.71ms] -(pass) resolveControlledUploadPath — NUL live guard > rejects NUL-only / leading NUL [0.47ms] -(pass) resolveControlledUploadPath > accepts regular file under root [0.83ms] -(pass) resolveControlledUploadPath > rejects path outside roots [0.55ms] -(pass) resolveControlledUploadPath > rejects absolute foreign path /etc/passwd [0.44ms] -(pass) resolveControlledUploadPath > rejects traversal that escapes root [0.49ms] -(pass) resolveControlledUploadPath > rejects missing path [0.50ms] -(pass) openFstatBoundedReadControlledFile — same fd + bound > reads small PNG via same-fd path [1.44ms] -(pass) openFstatBoundedReadControlledFile — same fd + bound > rejects oversize without allocating full max+1 into a single slurp beyond cap [15.00ms] -(pass) openFstatBoundedReadControlledFile — same fd + bound > rejects symlink leaf at open (O_NOFOLLOW) [1.14ms] -(pass) openFstatBoundedReadControlledFile — same fd + bound > fstat is on the same opened fd (structural pin) [0.55ms] -(pass) uploadControlledLocalFile > uploads PNG fixture via mock fetch and returns file_id [2.61ms] -(pass) uploadControlledLocalFile > refuses oversize before network [15.99ms] -(pass) uploadControlledLocalFile > never falls back to path when file_id missing [1.21ms] -(pass) uploadControlledLocalFile > rejects untrusted path without calling hub [0.65ms] -(pass) uploadControlledLocalFile > rejects NUL path without calling hub [0.43ms] -(pass) defaultControlledUploadRoots > includes grok sessions and attachment cache [0.64ms] -(pass) source contracts (adversarial pins) > same-fd pin: fstatSync(fd) + openSync; no path re-stat/readFileSync in reader [0.35ms] -(pass) source contracts (adversarial pins) > NUL guard pin: rawPath.includes NUL marker present [0.28ms] -(pass) source contracts (adversarial pins) > bounded-read pin: extra-byte probe after maxBytes [0.35ms] +(pass) normalizeUploadName > strips directories and control chars [0.90ms] +(pass) resolveControlledUploadPath — NUL live guard > rejects embedded NUL before any fs access [0.77ms] +(pass) resolveControlledUploadPath — NUL live guard > rejects NUL-only / leading NUL [0.55ms] +(pass) resolveControlledUploadPath > accepts regular file under root [0.85ms] +(pass) resolveControlledUploadPath > rejects path outside roots [0.59ms] +(pass) resolveControlledUploadPath > rejects absolute foreign path /etc/passwd [0.45ms] +(pass) resolveControlledUploadPath > rejects traversal that escapes root [0.58ms] +(pass) resolveControlledUploadPath > rejects missing path [0.57ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > reads small PNG via same-fd path [1.77ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > rejects oversize without allocating full max+1 into a single slurp beyond cap [20.31ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > rejects symlink leaf at open (O_NOFOLLOW) [1.18ms] +(pass) openFstatBoundedReadControlledFile — same fd + bound > fstat is on the same opened fd (structural pin) [0.70ms] +(pass) uploadControlledLocalFile > uploads PNG fixture via mock fetch and returns file_id [2.35ms] +(pass) uploadControlledLocalFile > refuses oversize before network [18.91ms] +(pass) uploadControlledLocalFile > never falls back to path when file_id missing [1.50ms] +(pass) uploadControlledLocalFile > rejects untrusted path without calling hub [0.80ms] +(pass) uploadControlledLocalFile > rejects NUL path without calling hub [0.59ms] +(pass) defaultControlledUploadRoots > includes grok sessions and attachment cache [0.84ms] +(pass) source contracts (adversarial pins) > same-fd pin: fstatSync(fd) + openSync; no path re-stat/readFileSync in reader [0.71ms] +(pass) source contracts (adversarial pins) > NUL guard pin: rawPath.includes NUL marker present [0.84ms] +(pass) source contracts (adversarial pins) > bounded-read pin: extra-byte probe after maxBytes [0.44ms] src/commhub-mcp.test.ts: -(pass) injectAgentFromSession > adds current alias to outbound task calls [0.17ms] -(pass) injectAgentFromSession > adds current alias to outbound message calls [0.08ms] -(pass) injectAgentFromSession > overrides stale or model-supplied from_session on ntok outbound calls [0.05ms] -(pass) injectAgentFromSession > does not add from_session to read-only calls [0.04ms] +(pass) injectAgentFromSession > adds current alias to outbound task calls [0.19ms] +(pass) injectAgentFromSession > adds current alias to outbound message calls [0.06ms] +(pass) injectAgentFromSession > overrides stale or model-supplied from_session on ntok outbound calls [0.19ms] +(pass) injectAgentFromSession > does not add from_session to read-only calls [0.08ms] src/inbox-dispatch.test.ts: -(pass) isInteractiveDashboardTask > accepts a Hub-authenticated dashboard chat task [0.35ms] -(pass) isInteractiveDashboardTask > pre-stamp admin rows stay FIFO because aliases are not auth facts [0.13ms] -(pass) isInteractiveDashboardTask > rejects node-authenticated spoofing, malformed ids, and plain messages [0.08ms] -(pass) dispatchInboxBatch > awaited batches preserve legacy runtime serialization [1.85ms] -(pass) dispatchInboxBatch > a later SSE snapshot enters while the first detached turn is still running [2.14ms] -(pass) dispatchInboxBatch > the real serialized drain lane can fetch a later SSE snapshot before the active turn ends [1.37ms] +(pass) isInteractiveDashboardTask > accepts a Hub-authenticated dashboard chat task [0.30ms] +(pass) isInteractiveDashboardTask > pre-stamp admin rows stay FIFO because aliases are not auth facts [0.11ms] +(pass) isInteractiveDashboardTask > rejects node-authenticated spoofing, malformed ids, and plain messages [0.09ms] +(pass) dispatchInboxBatch > awaited batches preserve legacy runtime serialization [1.77ms] +(pass) dispatchInboxBatch > a later SSE snapshot enters while the first detached turn is still running [1.85ms] +(pass) dispatchInboxBatch > the real serialized drain lane can fetch a later SSE snapshot before the active turn ends [1.04ms] (pass) dispatchInboxBatch > detached completion failures remain observable [1.41ms] -(pass) dispatchInboxBatch > settling detached work emits a wake for the next Hub inbox window [1.31ms] -(pass) dispatchInboxBatch > a throwing settle callback cannot strand queued N+1 work [1.50ms] -(pass) dispatchInboxBatch > same-tick duplicate kicks claim one row exactly once [0.54ms] -(pass) dispatchInboxBatch > bounded admission waits N+1 and starts it after a slot settles [1.82ms] -(pass) dispatchInboxBatch > durable reply drain waits until detached Codex rows finish [0.06ms] -(pass) dispatchInboxBatch > active Codex direct delivery and durable drain send one reply, not two [6.23ms] +(pass) dispatchInboxBatch > settling detached work emits a wake for the next Hub inbox window [1.81ms] +(pass) dispatchInboxBatch > a throwing settle callback cannot strand queued N+1 work [1.52ms] +(pass) dispatchInboxBatch > same-tick duplicate kicks claim one row exactly once [0.45ms] +(pass) dispatchInboxBatch > bounded admission waits N+1 and starts it after a slot settles [1.88ms] +(pass) dispatchInboxBatch > durable reply drain waits until detached Codex rows finish [0.08ms] +(pass) dispatchInboxBatch > active Codex direct delivery and durable drain send one reply, not two [7.45ms] src/reply-routing-source.test.ts: -(pass) #698 peer reply runtime wiring > peer replies negotiate the atomic tool and retain only a terminal legacy fallback [1.02ms] -(pass) #698 peer reply runtime wiring > every actionable inbox turn crosses the behavior-tested reply-policy seam [0.63ms] +(pass) #698 peer reply runtime wiring > peer replies negotiate the atomic tool and retain only a terminal legacy fallback [1.54ms] +(pass) #698 peer reply runtime wiring > every actionable inbox turn crosses the behavior-tested reply-policy seam [0.72ms] (pass) #698 peer reply runtime wiring > new_reply SSE events wake the actionable work inbox [0.32ms] src/task-runtime-evidence.test.ts: -(pass) logicalTaskIdFromInbox > retry/reassign task rows use stable task_id, not fresh inbox.id [0.09ms] -(pass) logicalTaskIdFromInbox > legacy task rows and non-task rows retain transport identity [0.05ms] -(pass) createTaskRuntimeEvidenceReporter > construction and process admission report no evidence [0.21ms] -(pass) createTaskRuntimeEvidenceReporter > submission and many runtime events produce one exact report per level [0.33ms] -(pass) createTaskRuntimeEvidenceReporter > a consumed-only runtime remains honest and lets the Hub imply submission [0.19ms] -(pass) createTaskRuntimeEvidenceReporter > missing logical task identity is a fail-closed no-op [0.13ms] -(pass) createTaskRuntimeEvidenceReporter > an old-Hub failure is visible but never breaks the model turn [0.37ms] -(pass) agent-node inbox wiring > keeps transport ACK separate from stable task evidence and replies [2.89ms] -(pass) agent-node inbox wiring > all runtime dispatch families receive the same task-lifetime reporter [0.96ms] -(pass) agent-node inbox wiring > SDK and direct-stdio boundaries preserve their distinct evidence semantics [1.26ms] +(pass) logicalTaskIdFromInbox > retry/reassign task rows use stable task_id, not fresh inbox.id [0.15ms] +(pass) logicalTaskIdFromInbox > legacy task rows and non-task rows retain transport identity [0.07ms] +(pass) createTaskRuntimeEvidenceReporter > construction and process admission report no evidence [0.55ms] +(pass) createTaskRuntimeEvidenceReporter > submission and many runtime events produce one exact report per level [0.38ms] +(pass) createTaskRuntimeEvidenceReporter > a consumed-only runtime remains honest and lets the Hub imply submission [0.21ms] +(pass) createTaskRuntimeEvidenceReporter > missing logical task identity is a fail-closed no-op [0.15ms] +(pass) createTaskRuntimeEvidenceReporter > an old-Hub failure is visible but never breaks the model turn [0.39ms] +(pass) agent-node inbox wiring > keeps transport ACK separate from stable task evidence and replies [1.58ms] +(pass) agent-node inbox wiring > all runtime dispatch families receive the same task-lifetime reporter [1.05ms] +(pass) agent-node inbox wiring > SDK and direct-stdio boundaries preserve their distinct evidence semantics [1.28ms] src/grok-isolated-cwd.test.ts: -(pass) prepareGrokIsolatedCwd (#204 preview.7) > creates per-node grok-cwd directory under home/.anet/nodes//grok-cwd [1.75ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to alias when nodeId is absent [1.03ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > sanitises nodeKey to avoid path traversal / weird chars [1.55ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > skips .mcp.json (does NOT symlink it into isolated cwd) [1.04ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > symlinks top-level files (README.md) and directories (docs/, src/) [1.27ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > is idempotent — second run sees existing symlinks and counts 0 new [1.11ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > picks up new entries on re-run (snapshot freshness) [2.01ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to userCwd (isolated=false) when mkdir fails [1.26ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to userCwd when userCwd does not exist (readdir fails) [1.64ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > does NOT throw on per-entry symlink failure — warns and continues [2.11ms] -(pass) prepareGrokIsolatedCwd (#204 preview.7) > two different nodes get fully isolated dirs (concurrency safe by construction) [2.12ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > creates per-node grok-cwd directory under home/.anet/nodes//grok-cwd [2.47ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to alias when nodeId is absent [1.34ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > sanitises nodeKey to avoid path traversal / weird chars [1.83ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > skips .mcp.json (does NOT symlink it into isolated cwd) [1.52ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > symlinks top-level files (README.md) and directories (docs/, src/) [1.53ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > is idempotent — second run sees existing symlinks and counts 0 new [1.50ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > picks up new entries on re-run (snapshot freshness) [1.37ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to userCwd (isolated=false) when mkdir fails [1.20ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > falls back to userCwd when userCwd does not exist (readdir fails) [1.68ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > does NOT throw on per-entry symlink failure — warns and continues [1.47ms] +(pass) prepareGrokIsolatedCwd (#204 preview.7) > two different nodes get fully isolated dirs (concurrency safe by construction) [2.08ms] src/inbox-dispatch-wiring.test.ts: -(pass) Codex app-server live inbox kick wiring > a Codex snapshot releases the serialized fetch lane after submission [0.22ms] -(pass) Codex app-server live inbox kick wiring > Codex detached admission is explicitly bounded and completion wakes the Hub window [0.38ms] -(pass) Codex app-server live inbox kick wiring > pending reply drain is fenced while detached Codex rows are active [0.15ms] +(pass) Codex app-server live inbox kick wiring > a Codex snapshot releases the serialized fetch lane after submission [0.25ms] +(pass) Codex app-server live inbox kick wiring > Codex detached admission is explicitly bounded and completion wakes the Hub window [0.73ms] +(pass) Codex app-server live inbox kick wiring > pending reply drain is fenced while detached Codex rows are active [0.18ms] src/owner-schedule-control.test.ts: -(pass) owner schedule managed-cron control > parses only exact managed markers and publishes bounded inventory [0.98ms] -(pass) owner schedule managed-cron control > changes timing/enabled while preserving command and unmanaged bytes [4.50ms] -(pass) owner schedule managed-cron control > command replacement, wrong node, wrong revision, and unknown patch fail before install [1.84ms] -(pass) owner schedule managed-cron control > install/readback failure restores and verifies the exact old crontab [2.46ms] -(pass) owner schedule managed-cron control > unsafe node directory and symlink journal fail closed with zero host write [0.96ms] -(pass) owner schedule managed-cron control > local audit is minimal, private and idempotent [2.24ms] +(pass) owner schedule managed-cron control > parses only exact managed markers and publishes bounded inventory [2.29ms] +(pass) owner schedule managed-cron control > changes timing/enabled while preserving command and unmanaged bytes [8.72ms] +(pass) owner schedule managed-cron control > command replacement, wrong node, wrong revision, and unknown patch fail before install [3.50ms] +(pass) owner schedule managed-cron control > install/readback failure restores and verifies the exact old crontab [13.11ms] +(pass) owner schedule managed-cron control > unsafe node directory and symlink journal fail closed with zero host write [1.62ms] +(pass) owner schedule managed-cron control > local audit is minimal, private and idempotent [5.71ms] src/owner-schedule-wiring.test.ts: -(pass) owner schedule process wiring > capability is pinned from config once and never exposed as a model tool [1.15ms] -(pass) owner schedule process wiring > SSE is only a doorbell and snapshots are editable only under the same gate [0.80ms] -(pass) owner schedule process wiring > new token mint paths bind the immutable node id and opt-in is explicit [2.70ms] +(pass) owner schedule process wiring > capability is pinned from config once and never exposed as a model tool [1.48ms] +(pass) owner schedule process wiring > SSE is only a doorbell and snapshots are editable only under the same gate [1.38ms] +(pass) owner schedule process wiring > new token mint paths bind the immutable node id and opt-in is explicit [3.22ms] src/runtime-effective-label.test.ts: -(pass) #491 startup banner reports the EFFECTIVE runtime > alias input 'codex-tui' → banner names the effective runtime (codex-app-server), not just the raw input [7178.00ms] -(pass) #491 startup banner reports the EFFECTIVE runtime > canonical input stays readable (no regression for the common case) [7150.08ms] -(pass) #491 regression lock — unknown runtime fails closed > unknown runtime → non-zero exit, error names the value AND the supported list [128.93ms] -(pass) #553 Grok startup banner reports model ownership truthfully > unset model on Grok ACP names the Grok CLI as owner, not the runtime alias as a model id [7149.05ms] -(pass) #553 Grok startup banner reports model ownership truthfully > unset model on Grok CLI uses the same non-versioned ownership statement [7142.68ms] -(pass) #553 Grok startup banner reports model ownership truthfully > an explicit Grok model is still reported exactly [7134.50ms] +(pass) #491 startup banner reports the EFFECTIVE runtime > alias input 'codex-tui' → banner names the effective runtime (codex-app-server), not just the raw input [7361.88ms] +(pass) #491 startup banner reports the EFFECTIVE runtime > canonical input stays readable (no regression for the common case) [7176.36ms] +(pass) #491 regression lock — unknown runtime fails closed > unknown runtime → non-zero exit, error names the value AND the supported list [131.18ms] +(pass) #553 Grok startup banner reports model ownership truthfully > unset model on Grok ACP names the Grok CLI as owner, not the runtime alias as a model id [7149.39ms] +(pass) #553 Grok startup banner reports model ownership truthfully > unset model on Grok CLI uses the same non-versioned ownership statement [7198.05ms] +(pass) #553 Grok startup banner reports model ownership truthfully > an explicit Grok model is still reported exactly [7157.65ms] src/peer-reply-send.test.ts: -(pass) peer reply capability fallback > capable Hub uses only the atomic terminal route [0.46ms] -(pass) peer reply capability fallback > old Hub wire error terminalizes through send_reply, never send_task [0.53ms] -(pass) peer reply capability fallback > every explicit capability downgrade preserves terminal reply semantics [0.49ms] -(pass) peer reply capability fallback > transport ambiguity and unrelated hard errors never choose a second route [0.37ms] -(pass) peer reply capability fallback > negative capability is rechecked instead of cached [0.35ms] -(pass) peer reply capability fallback > legacy terminalization failure stays visible to the pending queue [0.22ms] -(pass) peer reply capability fallback > classifier accepts only explicit capability signals [0.09ms] +(pass) peer reply capability fallback > capable Hub uses only the atomic terminal route [0.58ms] +(pass) peer reply capability fallback > old Hub wire error terminalizes through send_reply, never send_task [0.61ms] +(pass) peer reply capability fallback > every explicit capability downgrade preserves terminal reply semantics [0.61ms] +(pass) peer reply capability fallback > transport ambiguity and unrelated hard errors never choose a second route [0.44ms] +(pass) peer reply capability fallback > negative capability is rechecked instead of cached [0.36ms] +(pass) peer reply capability fallback > legacy terminalization failure stays visible to the pending queue [0.26ms] +(pass) peer reply capability fallback > classifier accepts only explicit capability signals [0.10ms] src/private-log.test.ts: -(pass) Grok preview private ordinary logs > scrubs and repairs legacy logs before appending through a 0600 file [4.30ms] -(pass) Grok preview private ordinary logs > rejects a symlinked directory or final log file [1.26ms] -(pass) Grok preview private ordinary logs > rejects a multiply-linked log instead of rewriting another pathname [0.59ms] -(pass) Grok preview private ordinary logs > does not follow a log-directory symlink introduced after preparation [0.57ms] +(pass) Grok preview private ordinary logs > scrubs and repairs legacy logs before appending through a 0600 file [4.55ms] +(pass) Grok preview private ordinary logs > rejects a symlinked directory or final log file [1.33ms] +(pass) Grok preview private ordinary logs > rejects a multiply-linked log instead of rewriting another pathname [0.61ms] +(pass) Grok preview private ordinary logs > does not follow a log-directory symlink introduced after preparation [0.61ms] src/owner-schedule-system-crontab.test.ts: -(pass) owner schedule real crontab adapter > round-trips an exact managed marker through the container crontab [26.36ms] +(pass) owner schedule real crontab adapter > round-trips an exact managed marker through the container crontab [26.60ms] src/credential-redaction.test.ts: -(pass) credential persistence redactor > removes exact caller-known values regardless of punctuation or context [0.26ms] -(pass) credential persistence redactor > redacts network, GitHub, AWS and provider token shapes in free text [0.20ms] -(pass) credential persistence redactor > redacts credential assignments while preserving keys and valid JSON [0.30ms] -(pass) credential persistence redactor > redacts shell/error assignment forms including quoted values [0.16ms] -(pass) credential persistence redactor > redacts an unlabelled connection URI with embedded userinfo [0.07ms] -(pass) credential persistence redactor > does not over-delete normal prose and non-credential settings [0.08ms] -(pass) credential persistence redactor > deep-redacts JSON-like values without mutating the input [0.31ms] -(pass) credential value collection > collects exact sensitive values and shaped values under unknown keys [1.06ms] -(pass) credential value collection > key classifier is exact enough not to treat ordinary AWS settings as credentials [0.10ms] +(pass) credential persistence redactor > removes exact caller-known values regardless of punctuation or context [0.31ms] +(pass) credential persistence redactor > redacts network, GitHub, AWS and provider token shapes in free text [0.25ms] +(pass) credential persistence redactor > redacts credential assignments while preserving keys and valid JSON [0.38ms] +(pass) credential persistence redactor > redacts shell/error assignment forms including quoted values [0.15ms] +(pass) credential persistence redactor > redacts an unlabelled connection URI with embedded userinfo [0.08ms] +(pass) credential persistence redactor > does not over-delete normal prose and non-credential settings [0.10ms] +(pass) credential persistence redactor > deep-redacts JSON-like values without mutating the input [0.38ms] +(pass) credential value collection > collects exact sensitive values and shaped values under unknown keys [1.43ms] +(pass) credential value collection > key classifier is exact enough not to treat ordinary AWS settings as credentials [0.12ms] src/inbox-skip-log-wiring.test.ts: -(pass) processInbox logs skipped messages at INFO before acknowledging [1.60ms] +(pass) processInbox logs skipped messages at INFO before acknowledging [1.71ms] src/peer-reply-inbox.test.ts: -(pass) inbox turn reply-policy enforcement > delivers once, ACKs once, and exposes no outbound reply dependency [0.41ms] +(pass) inbox turn reply-policy enforcement > delivers once, ACKs once, and exposes no outbound reply dependency [0.48ms] (pass) inbox turn reply-policy enforcement > ordinary request returns its outcome without ACKing in this seam [0.26ms] -(pass) inbox turn reply-policy enforcement > runtime failure does not ACK a result that was never consumed [0.29ms] +(pass) inbox turn reply-policy enforcement > runtime failure does not ACK a result that was never consumed [0.24ms] (pass) peer reply SSE routing > new_reply schedules exactly one drain [0.11ms] -(pass) peer reply SSE routing > unrelated events do not schedule a drain [0.05ms] +(pass) peer reply SSE routing > unrelated events do not schedule a drain [0.04ms] src/grok-artifact-extractor.test.ts: -(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > returns empty when grokSessionDir is undefined [0.47ms] -(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > returns empty when videos/ subdir is missing [0.27ms] -(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > enumerates .mp4 files in videos/ as absolute paths [0.73ms] -(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > matches mp4 case-insensitively [0.57ms] -(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > does not throw on permission errors — returns [] [0.41ms] -(pass) formatVideoTrailer (#205 Step 2 simplified) > returns empty string for empty list [0.13ms] -(pass) formatVideoTrailer (#205 Step 2 simplified) > formats one path [0.09ms] -(pass) formatVideoTrailer (#205 Step 2 simplified) > formats multiple paths [0.10ms] -(pass) formatVideoTrailer (#205 Step 2 simplified) > skips paths already mentioned in existingReply (no duplication) [0.03ms] -(pass) formatVideoTrailer (#205 Step 2 simplified) > only appends paths NOT already mentioned, even when some are [0.07ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > returns empty when grokSessionDir is undefined [0.64ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > returns empty when videos/ subdir is missing [0.26ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > enumerates .mp4 files in videos/ as absolute paths [0.80ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > matches mp4 case-insensitively [0.79ms] +(pass) listGrokVideoArtifacts (#205 Step 2 simplified) > does not throw on permission errors — returns [] [0.48ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > returns empty string for empty list [0.17ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > formats one path [0.10ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > formats multiple paths [0.06ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > skips paths already mentioned in existingReply (no duplication) [0.04ms] +(pass) formatVideoTrailer (#205 Step 2 simplified) > only appends paths NOT already mentioned, even when some are [0.08ms] src/explicit-task-lifecycle.test.ts: -(pass) explicit delegation lifecycle trace > keeps the production delegation loop wired through the tested state machine [1.16ms] -(pass) explicit delegation lifecycle trace > emits ack, start, and reply from the production polling state machine [0.88ms] -(pass) explicit delegation lifecycle trace > emits both bounded stale warnings and expiry when delivery never advances [0.31ms] -(pass) explicit delegation lifecycle trace > pins the production poll, stale-warning, and timeout defaults [0.62ms] -(pass) explicit delegation lifecycle trace > maps failed and cancelled terminal states to a failed trace without retrying [0.37ms] +(pass) explicit delegation lifecycle trace > keeps the production delegation loop wired through the tested state machine [0.86ms] +(pass) explicit delegation lifecycle trace > emits ack, start, and reply from the production polling state machine [1.04ms] +(pass) explicit delegation lifecycle trace > emits both bounded stale warnings and expiry when delivery never advances [0.33ms] +(pass) explicit delegation lifecycle trace > pins the production poll, stale-warning, and timeout defaults [0.61ms] +(pass) explicit delegation lifecycle trace > maps failed and cancelled terminal states to a failed trace without retrying [0.38ms] src/task-trace.test.ts: -(pass) task trace contract > renders missing parent and lifecycle scope honestly [0.27ms] -(pass) task trace contract > redacts credentials from errors [0.14ms] -(pass) task trace contract > emits parseable JSON and neutralizes human log injection [0.13ms] -(pass) task trace contract > recognizes the real MCP content envelope before cli parsing [0.58ms] -(pass) task trace contract > uses stable event names for send and observed lifecycle phases [0.10ms] +(pass) task trace contract > renders missing parent and lifecycle scope honestly [0.44ms] +(pass) task trace contract > redacts credentials from errors [0.18ms] +(pass) task trace contract > emits parseable JSON and neutralizes human log injection [0.14ms] +(pass) task trace contract > recognizes the real MCP content envelope before cli parsing [0.71ms] +(pass) task trace contract > uses stable event names for send and observed lifecycle phases [0.12ms] src/sse-recovery-guidance.test.ts: -(pass) sseAbandonGuidance > states that abandon leaves the current process alive [0.09ms] -(pass) sseAbandonGuidance > requires stop-and-replace instead of starting a duplicate [0.05ms] -(pass) sseAbandonGuidance > preserves the co-presence launch shape in recovery guidance [0.05ms] -(pass) sseAbandonGuidance > the production SSE abandon hook uses the honest guidance [1.06ms] +(pass) sseAbandonGuidance > states that abandon leaves the current process alive [0.14ms] +(pass) sseAbandonGuidance > requires stop-and-replace instead of starting a duplicate [0.09ms] +(pass) sseAbandonGuidance > preserves the co-presence launch shape in recovery guidance [0.06ms] +(pass) sseAbandonGuidance > the production SSE abandon hook uses the honest guidance [1.05ms] src/cli-explicit-delegation.test.ts: -(pass) extractExplicitDelegation > matches send_task alias/task call [0.68ms] -(pass) extractExplicitDelegation > matches mcp send_task positional call [0.12ms] -(pass) extractExplicitDelegation > matches 给 X 发任务 [0.13ms] -(pass) extractExplicitDelegation > matches 和 X 沟通一下 [0.19ms] -(pass) extractExplicitDelegation > matches bare 和 X 沟通一下 [0.12ms] -(pass) extractExplicitDelegation > matches 和 X send_task 一下 [0.06ms] -(pass) extractExplicitDelegation > matches 和 X send_task 一下 with no punctuation before body [0.09ms] +(pass) extractExplicitDelegation > matches send_task alias/task call [0.71ms] +(pass) extractExplicitDelegation > matches mcp send_task positional call [0.15ms] +(pass) extractExplicitDelegation > matches 给 X 发任务 [0.15ms] +(pass) extractExplicitDelegation > matches 和 X 沟通一下 [0.29ms] +(pass) extractExplicitDelegation > matches bare 和 X 沟通一下 [0.13ms] +(pass) extractExplicitDelegation > matches 和 X send_task 一下 [0.07ms] +(pass) extractExplicitDelegation > matches 和 X send_task 一下 with no punctuation before body [0.08ms] (pass) extractExplicitDelegation > matches bare 和 X send_task 一下 [0.06ms] -(pass) extractExplicitDelegation > matches 让 X 做 [0.10ms] -(pass) extractExplicitDelegation > matches 交给 X [0.05ms] -(pass) extractExplicitDelegation > does not match no alias [0.03ms] -(pass) extractExplicitDelegation > does not match normal Q&A [0.04ms] +(pass) extractExplicitDelegation > matches 让 X 做 [0.11ms] +(pass) extractExplicitDelegation > matches 交给 X [0.07ms] +(pass) extractExplicitDelegation > does not match no alias [0.04ms] +(pass) extractExplicitDelegation > does not match normal Q&A [0.02ms] (pass) extractExplicitDelegation > matches bare send_task (MCP-like) [0.04ms] -(pass) extractExplicitDelegation > matches bare send_task with multi-word task body [0.04ms] -(pass) extractExplicitDelegation > matches 你去给 X 打个招呼 [0.06ms] +(pass) extractExplicitDelegation > matches bare send_task with multi-word task body [0.05ms] +(pass) extractExplicitDelegation > matches 你去给 X 打个招呼 [0.08ms] (pass) extractExplicitDelegation > matches 你去给 X with longer body [0.05ms] -(pass) extractExplicitDelegation > matches 给 X 发个消息 BODY (verb-suffix stripped) [0.04ms] -(pass) extractExplicitDelegation > matches 给 X 发 BODY (bare verb) [0.06ms] -(pass) extractExplicitDelegation > matches 给 X 沟通一下 BODY [0.07ms] +(pass) extractExplicitDelegation > matches 给 X 发个消息 BODY (verb-suffix stripped) [0.06ms] +(pass) extractExplicitDelegation > matches 给 X 发 BODY (bare verb) [0.04ms] +(pass) extractExplicitDelegation > matches 给 X 沟通一下 BODY [0.05ms] (pass) extractExplicitDelegation > matches 给 X 说 BODY [0.05ms] -(pass) extractExplicitDelegation > matches 给 X 发任务 (regression — specific pattern still wins) [0.07ms] +(pass) extractExplicitDelegation > matches 给 X 发任务 (regression — specific pattern still wins) [0.04ms] src/util/timeout.test.ts: (pass) withTimeout — happy path (factory wins) > resolves with factory value when fn settles before deadline [0.51ms] -(pass) withTimeout — happy path (factory wins) > passes a non-aborted signal when fn finishes promptly [0.17ms] -(pass) withTimeout — happy path (factory wins) > returns objects, not just strings [0.12ms] -(pass) withTimeout — happy path (factory wins) > propagates fn's rejection unchanged (not wrapped) [0.24ms] -(pass) withTimeout — timeout path (timer wins) > rejects with TimeoutError when fn outlasts deadline [31.93ms] -(pass) withTimeout — timeout path (timer wins) > TimeoutError message includes label + ms [0.06ms] -(pass) withTimeout — timeout path (timer wins) > TimeoutError without label still works [0.05ms] -(pass) withTimeout — timeout path (timer wins) > fires AbortSignal on timeout so factory can cancel in-flight work [43.82ms] -(pass) withTimeout — zero / negative deadline sentinel > timeoutMs=0 disables the timer (CLAUDE_TIMEOUT_MS=0 sentinel) [51.64ms] -(pass) withTimeout — zero / negative deadline sentinel > timeoutMs<0 also disables (defensive) [0.48ms] -(pass) withTimeout — zero / negative deadline sentinel > untimed call still receives a non-aborted signal [0.19ms] -(pass) withTimeout — externalSignal propagation > forwards external abort into factory signal [212.14ms] -(pass) withTimeout — externalSignal propagation > already-aborted external signal aborts immediately [0.59ms] -(pass) withTimeout — cleanup > clears timer on successful return (no dangling handles) [21.89ms] -(pass) resolveTimeoutMs — precedence > env wins over flag and default [0.34ms] -(pass) resolveTimeoutMs — precedence > flag wins when env is missing [0.04ms] -(pass) resolveTimeoutMs — precedence > default wins when env and flag both missing [0.11ms] -(pass) resolveTimeoutMs — precedence > flag wins when env is empty string (treated as unset) [0.05ms] -(pass) resolveTimeoutMs — precedence > flag wins when env is non-numeric garbage [0.05ms] +(pass) withTimeout — happy path (factory wins) > passes a non-aborted signal when fn finishes promptly [0.14ms] +(pass) withTimeout — happy path (factory wins) > returns objects, not just strings [0.26ms] +(pass) withTimeout — happy path (factory wins) > propagates fn's rejection unchanged (not wrapped) [0.32ms] +(pass) withTimeout — timeout path (timer wins) > rejects with TimeoutError when fn outlasts deadline [32.41ms] +(pass) withTimeout — timeout path (timer wins) > TimeoutError message includes label + ms [0.10ms] +(pass) withTimeout — timeout path (timer wins) > TimeoutError without label still works [0.06ms] +(pass) withTimeout — timeout path (timer wins) > fires AbortSignal on timeout so factory can cancel in-flight work [45.53ms] +(pass) withTimeout — zero / negative deadline sentinel > timeoutMs=0 disables the timer (CLAUDE_TIMEOUT_MS=0 sentinel) [51.75ms] +(pass) withTimeout — zero / negative deadline sentinel > timeoutMs<0 also disables (defensive) [0.54ms] +(pass) withTimeout — zero / negative deadline sentinel > untimed call still receives a non-aborted signal [0.21ms] +(pass) withTimeout — externalSignal propagation > forwards external abort into factory signal [212.24ms] +(pass) withTimeout — externalSignal propagation > already-aborted external signal aborts immediately [0.62ms] +(pass) withTimeout — cleanup > clears timer on successful return (no dangling handles) [21.76ms] +(pass) resolveTimeoutMs — precedence > env wins over flag and default [0.44ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is missing [0.08ms] +(pass) resolveTimeoutMs — precedence > default wins when env and flag both missing [0.07ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is empty string (treated as unset) [0.30ms] +(pass) resolveTimeoutMs — precedence > flag wins when env is non-numeric garbage [0.10ms] (pass) resolveTimeoutMs — precedence > flag wins when env is negative [0.06ms] -(pass) resolveTimeoutMs — precedence > default wins when flag is NaN [0.07ms] -(pass) resolveTimeoutMs — precedence > zero is honoured (not treated as unset) — env=0 disables timeout [0.10ms] +(pass) resolveTimeoutMs — precedence > default wins when flag is NaN [0.05ms] +(pass) resolveTimeoutMs — precedence > zero is honoured (not treated as unset) — env=0 disables timeout [0.09ms] (pass) resolveTimeoutMs — precedence > zero is honoured at flag level too [0.06ms] -(pass) resolveTimeoutMs — clamping > clamps below minMs and reports clamped=true [0.07ms] +(pass) resolveTimeoutMs — clamping > clamps below minMs and reports clamped=true [0.06ms] (pass) resolveTimeoutMs — clamping > clamps above maxMs and reports clamped=true [0.05ms] -(pass) resolveTimeoutMs — clamping > in-bounds value is not clamped [0.05ms] -(pass) resolveTimeoutMs — clamping > default value also gets clamped (configuration sanity) [0.05ms] -(pass) resolveTimeoutMs — defensive null handling > null envValue is treated as unset [0.04ms] +(pass) resolveTimeoutMs — clamping > in-bounds value is not clamped [0.07ms] +(pass) resolveTimeoutMs — clamping > default value also gets clamped (configuration sanity) [0.06ms] +(pass) resolveTimeoutMs — defensive null handling > null envValue is treated as unset [0.05ms] (pass) resolveTimeoutMs — defensive null handling > null flagValue is treated as unset [0.04ms] src/util/single-flight.test.ts: -(pass) single-flight resource initialization > concurrent callers share exactly one initializer [0.75ms] -(pass) single-flight resource initialization > a rejected initializer is cleared and can be retried [0.39ms] +(pass) single-flight resource initialization > concurrent callers share exactly one initializer [0.80ms] +(pass) single-flight resource initialization > a rejected initializer is cleared and can be retried [0.32ms] src/util/supervise-child.test.ts: -(pass) superviseChild — shutdown gate stops the loop > shutdownGate=true from the start → runOnce never called [0.70ms] -(pass) superviseChild — shutdown gate stops the loop > shutdownGate flips true after first iteration → exactly one runOnce [0.31ms] -(pass) superviseChild — backoff growth + cap > waits double the delay each iteration, capping at maxDelayMs [2.93ms] +(pass) superviseChild — shutdown gate stops the loop > shutdownGate=true from the start → runOnce never called [0.69ms] +(pass) superviseChild — shutdown gate stops the loop > shutdownGate flips true after first iteration → exactly one runOnce [0.27ms] +(pass) superviseChild — backoff growth + cap > waits double the delay each iteration, capping at maxDelayMs [2.83ms] (pass) superviseChild — runOnce that returns WITHOUT markStable is treated as failed (regression pin) > runOnce that returns cleanly without markStable → backoff doubles [0.63ms] -(pass) superviseChild — markStable resets backoff > after iteration that calls markStable, next wait is baseDelayMs again [0.54ms] -(pass) superviseChild — markStable resets backoff > markStable called multiple times in one iteration is idempotent [0.56ms] -(pass) superviseChild — abandonAfterMs > calls onAbandon and returns after cumulative downtime exceeds threshold [0.51ms] -(pass) superviseChild — abandonAfterMs > markStable in any iteration resets downtime — abandon never fires [0.49ms] -(pass) superviseChild — runOnce error handling > runOnce throws → onError fires, loop continues [0.68ms] -(pass) superviseChild — runOnce error handling > runOnce throws AND shutdownGate goes true → loop exits, no further iteration [0.27ms] -(pass) superviseChild — jitter range > jitterRatio=0.25 + random=0 → -25% of delay (lower bound) [0.48ms] -(pass) superviseChild — jitter range > jitterRatio=0.25 + random=1 → +25% of delay (upper bound) [0.42ms] -(pass) superviseChild — jitter range > jitterRatio=0 → deterministic waits at exact delay [0.36ms] -(pass) superviseChild — jitter range > waitMs floor 100 enforces minimum wait even with tiny base + negative jitter [0.45ms] -(pass) superviseChild — defensive contract > returns (does not throw) when runOnce never resolves and shutdown flips [1.39ms] +(pass) superviseChild — markStable resets backoff > after iteration that calls markStable, next wait is baseDelayMs again [0.60ms] +(pass) superviseChild — markStable resets backoff > markStable called multiple times in one iteration is idempotent [0.53ms] +(pass) superviseChild — abandonAfterMs > calls onAbandon and returns after cumulative downtime exceeds threshold [0.47ms] +(pass) superviseChild — abandonAfterMs > markStable in any iteration resets downtime — abandon never fires [0.51ms] +(pass) superviseChild — runOnce error handling > runOnce throws → onError fires, loop continues [0.73ms] +(pass) superviseChild — runOnce error handling > runOnce throws AND shutdownGate goes true → loop exits, no further iteration [0.33ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=0 → -25% of delay (lower bound) [0.84ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=1 → +25% of delay (upper bound) [0.52ms] +(pass) superviseChild — jitter range > jitterRatio=0 → deterministic waits at exact delay [0.52ms] +(pass) superviseChild — jitter range > waitMs floor 100 enforces minimum wait even with tiny base + negative jitter [0.55ms] +(pass) superviseChild — defensive contract > returns (does not throw) when runOnce never resolves and shutdown flips [1.51ms] src/util/access-resolve.test.ts: -(pass) normalizeAllowFrom — input shapes > real string[] passes through deduped (filter empty strings) [0.22ms] -(pass) normalizeAllowFrom — input shapes > undefined → empty + not malformed [0.04ms] -(pass) normalizeAllowFrom — input shapes > null → empty + not malformed [0.03ms] -(pass) normalizeAllowFrom — input shapes > non-array object → empty + malformed (corrupted access.json shape) [0.05ms] +(pass) normalizeAllowFrom — input shapes > real string[] passes through deduped (filter empty strings) [0.29ms] +(pass) normalizeAllowFrom — input shapes > undefined → empty + not malformed [0.05ms] +(pass) normalizeAllowFrom — input shapes > null → empty + not malformed [0.04ms] +(pass) normalizeAllowFrom — input shapes > non-array object → empty + malformed (corrupted access.json shape) [0.07ms] (pass) normalizeAllowFrom — input shapes > string instead of array → malformed [0.03ms] -(pass) normalizeAllowFrom — input shapes > array with non-string elements drops them [0.08ms] -(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > empty array → deny with empty-fail-closed kind [0.20ms] -(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > undefined → deny [0.05ms] -(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > malformed → deny + reason mentions malformed [0.12ms] -(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*'] alone allows any sender [0.06ms] +(pass) normalizeAllowFrom — input shapes > array with non-string elements drops them [0.07ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > empty array → deny with empty-fail-closed kind [0.23ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > undefined → deny [0.06ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > malformed → deny + reason mentions malformed [0.13ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*'] alone allows any sender [0.07ms] (pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*', 'specific_id'] still wildcard-allows (wins precedence) [0.05ms] -(pass) resolveTelegramAccess — explicit id / username matching > senderId in list → allow [0.07ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderId in list → allow [0.08ms] (pass) resolveTelegramAccess — explicit id / username matching > senderUsername match (no id match) → allow [0.05ms] -(pass) resolveTelegramAccess — explicit id / username matching > neither id nor username in list → deny [0.06ms] -(pass) resolveTelegramAccess — explicit id / username matching > empty senderUsername doesn't accidentally match empty list entry [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > neither id nor username in list → deny [0.07ms] +(pass) resolveTelegramAccess — explicit id / username matching > empty senderUsername doesn't accidentally match empty list entry [0.04ms] (pass) resolveTelegramAccess — explicit id / username matching > blank-string id with username match still allows [0.05ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > empty allowFrom → deny [0.27ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > wildcard allows [0.08ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > specific id allows [0.08ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > sender not in list → deny [0.05ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > empty allowChats → fail-closed [0.08ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=all → allow [0.06ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=observe → deny [0.05ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat NOT in allowChats → deny (even with policy=all) [0.08ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > wildcard chats opens any chat (with groupPolicy=all) [0.04ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > groupPolicy=mention allows (caller decides at message inspect time) [0.04ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > empty allowFrom → deny [0.21ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > wildcard allows [0.06ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > specific id allows [0.27ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > sender not in list → deny [0.06ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > empty allowChats → fail-closed [0.12ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=all → allow [0.09ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=observe → deny [0.07ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat NOT in allowChats → deny (even with policy=all) [0.12ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > wildcard chats opens any chat (with groupPolicy=all) [0.05ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > groupPolicy=mention allows (caller decides at message inspect time) [0.05ms] (pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for empty allowFrom [0.16ms] -(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for malformed allowFrom + mentions malformed [0.04ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for malformed allowFrom + mentions malformed [0.05ms] (pass) buildEmptyAllowlistWarn — boot-time visibility > returns null when allowFrom has at least one entry [0.04ms] -(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null for wildcard-allow (channel intentionally open) [0.03ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader stores raw allowFrom verbatim — no normalization at load time [0.09ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is missing [0.05ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is malformed (non-array) [0.07ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader is silent when allowFrom has at least one entry (even if numeric) [0.09ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null for wildcard-allow (channel intentionally open) [0.14ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader stores raw allowFrom verbatim — no normalization at load time [0.14ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is missing [0.04ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is malformed (non-array) [0.05ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader is silent when allowFrom has at least one entry (even if numeric) [0.10ms] (pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123] alone (numeric sender id from a misformatted access.json) → loader+resolver fail-closed [0.07ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null] (corrupted access.json) → loader+resolver fail-closed [0.07ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [{}] (object instead of id string) → loader+resolver fail-closed [0.08ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null] (corrupted access.json) → loader+resolver fail-closed [0.10ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [{}] (object instead of id string) → loader+resolver fail-closed [0.07ms] (pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123, '@vansin'] (mixed) → '@vansin' still allowed, numeric '123' rejected [0.09ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null, '*'] (mixed wildcard) → wildcard wins despite garbage entries [0.07ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null, '*'] (mixed wildcard) → wildcard wins despite garbage entries [0.06ms] (pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > missing access.json entirely (loader gets null) → fail-closed [0.06ms] (pass) regression — pre-v0.11 fail-open MUST NOT come back > empty array NEVER allows [0.04ms] (pass) regression — pre-v0.11 fail-open MUST NOT come back > undefined NEVER allows [0.03ms] -(pass) regression — pre-v0.11 fail-open MUST NOT come back > null NEVER allows [0.04ms] -(pass) regression — pre-v0.11 fail-open MUST NOT come back > object-shape (corrupted) NEVER allows [0.03ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > null NEVER allows [0.03ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > object-shape (corrupted) NEVER allows [0.02ms] src/runtime/fetch-attachment.test.ts: -(pass) FILE_ID_REGEX matches server contract > accepts the same shapes the hub accepts [0.21ms] -(pass) FILE_ID_REGEX matches server contract > rejects path-traversal + length-out-of-range [0.09ms] -(pass) resolveAttachmentToLocalPath — file_id path > hub 200 OK → bytes written to cache + chmod 600 + Bearer auth attached [9.30ms] -(pass) resolveAttachmentToLocalPath — file_id path > file_id_invalid before any HTTP call (path traversal attempt) [0.47ms] -(pass) resolveAttachmentToLocalPath — file_id path > hub 404 → not_found code [0.53ms] -(pass) resolveAttachmentToLocalPath — file_id path > hub 401 → auth_failed code [0.47ms] -(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > Content-Length > cap → size_exceeded with declared-and-cap surfaced + no cache file written [0.62ms] -(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > Content-Length lies (says small, sends big) → size_exceeded MID-STREAM with cleanup [1.47ms] -(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > DEFAULT_MAX_BYTES is 50 MiB unless COMMHUB_ATTACHMENT_MAX_BYTES is set (current process is unset → 50 MiB) [0.05ms] -(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id + path inside cache root → returns canonical path, no HTTP call [0.80ms] -(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > configured Feishu root remains a compatible trusted drop-zone [0.62ms] -(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > existing file outside trusted roots is rejected [0.58ms] -(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > symlink inside a trusted root cannot escape to another host file [0.56ms] -(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id + path does NOT exist → not_found error [0.51ms] -(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id AND no path → no_file_id_no_path error [0.35ms] -(pass) resolveAttachmentToLocalPath — cache hit > same file_id + same size → no HTTP call, returns cached:true [0.54ms] -(pass) resolveAttachmentToLocalPath — cache hit > same file_id + different size → cache miss, re-fetches [4.42ms] -(pass) sweepAttachmentCacheOnce > purges files older than TTL, keeps fresh [0.89ms] -(pass) sweepAttachmentCacheOnce > no-op when cache dir doesn't exist [0.26ms] +(pass) FILE_ID_REGEX matches server contract > accepts the same shapes the hub accepts [0.19ms] +(pass) FILE_ID_REGEX matches server contract > rejects path-traversal + length-out-of-range [0.07ms] +(pass) resolveAttachmentToLocalPath — file_id path > hub 200 OK → bytes written to cache + chmod 600 + Bearer auth attached [11.66ms] +(pass) resolveAttachmentToLocalPath — file_id path > file_id_invalid before any HTTP call (path traversal attempt) [0.61ms] +(pass) resolveAttachmentToLocalPath — file_id path > hub 404 → not_found code [0.48ms] +(pass) resolveAttachmentToLocalPath — file_id path > hub 401 → auth_failed code [0.75ms] +(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > Content-Length > cap → size_exceeded with declared-and-cap surfaced + no cache file written [0.90ms] +(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > Content-Length lies (says small, sends big) → size_exceeded MID-STREAM with cleanup [1.96ms] +(pass) resolveAttachmentToLocalPath — size cap (🔴 通信龙 nit: BYTE unit + mid-stream abort) > DEFAULT_MAX_BYTES is 50 MiB unless COMMHUB_ATTACHMENT_MAX_BYTES is set (current process is unset → 50 MiB) [0.11ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id + path inside cache root → returns canonical path, no HTTP call [1.37ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > configured Feishu root remains a compatible trusted drop-zone [0.85ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > existing file outside trusted roots is rejected [0.82ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > symlink inside a trusted root cannot escape to another host file [1.46ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id + path does NOT exist → not_found error [0.70ms] +(pass) resolveAttachmentToLocalPath — trusted local path fallback (single-host / feishu compat) > no file_id AND no path → no_file_id_no_path error [0.45ms] +(pass) resolveAttachmentToLocalPath — cache hit > same file_id + same size → no HTTP call, returns cached:true [0.71ms] +(pass) resolveAttachmentToLocalPath — cache hit > same file_id + different size → cache miss, re-fetches [5.74ms] +(pass) sweepAttachmentCacheOnce > purges files older than TTL, keeps fresh [1.19ms] +(pass) sweepAttachmentCacheOnce > no-op when cache dir doesn't exist [0.30ms] src/runtime/readable-attachment-prompt.test.ts: -(pass) readable attachment prompt > pins the exact runtime set without changing structured-image SDK lanes [0.17ms] -(pass) readable attachment prompt > pins the readable extension allowlist as an exact value set [0.35ms] -(pass) readable attachment prompt > injects absolute deduplicated paths and escapes control characters [0.24ms] +(pass) readable attachment prompt > pins the exact runtime set without changing structured-image SDK lanes [0.11ms] +(pass) readable attachment prompt > pins the readable extension allowlist as an exact value set [0.32ms] +(pass) readable attachment prompt > injects absolute deduplicated paths and escapes control characters [0.31ms] (pass) readable attachment prompt > leaves text byte-identical when no attachment resolved [0.05ms] -(pass) readable attachment prompt > path-prompt runtimes reject sender-local paths while structured lanes retain legacy behavior [0.18ms] -(pass) readable attachment prompt > the inbox choke point feeds the augmented text into processTask [2.38ms] +(pass) readable attachment prompt > path-prompt runtimes reject sender-local paths while structured lanes retain legacy behavior [0.22ms] +(pass) readable attachment prompt > the inbox choke point feeds the augmented text into processTask [1.83ms] src/runtime/create-node-daemon.test.ts: -(pass) #633 daemon private state > global config repair and replacement converge to private state [3.74ms] -(pass) #633 daemon private state > global config read refuses a symlink without touching its target [0.90ms] -(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > permissionMode enum [0.40ms] -(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > dangerouslySkipPermissions boolean (string 'true' must be rejected) [0.15ms] +(pass) #633 daemon private state > global config repair and replacement converge to private state [4.46ms] +(pass) #633 daemon private state > global config read refuses a symlink without touching its target [1.05ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > permissionMode enum [0.42ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > dangerouslySkipPermissions boolean (string 'true' must be rejected) [0.20ms] (pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > maxTurns integer range — 'DROP TABLE' / float / out-of-range rejected [0.25ms] -(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > budget number with decimals allowed; out-of-range rejected [0.23ms] -(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > timeout integer range [0.15ms] -(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > unknown key rejected [0.07ms] -(pass) buildAnetArgsDaemon now reaches flag value validation > happy path with mixed flags [0.48ms] -(pass) buildAnetArgsDaemon now reaches flag value validation > smuggled string maxTurns rejected by daemon even if hub missed [0.41ms] -(pass) buildAnetArgsDaemon now reaches flag value validation > smuggled string dangerouslySkipPermissions rejected [0.10ms] -(pass) buildAnetArgsDaemon now reaches flag value validation > name shell-metachar still rejected (existing validateName, F2) [0.11ms] -(pass) buildAnetArgsDaemon now reaches flag value validation > runtime enum still enforced [0.09ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > budget number with decimals allowed; out-of-range rejected [0.25ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > timeout integer range [0.16ms] +(pass) §4.2.2 daemon-side flag VALUE validator (BLOCKER #2 — defense in depth) > unknown key rejected [0.17ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > happy path with mixed flags [0.56ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > smuggled string maxTurns rejected by daemon even if hub missed [0.22ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > smuggled string dangerouslySkipPermissions rejected [0.13ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > name shell-metachar still rejected (existing validateName, F2) [0.15ms] +(pass) buildAnetArgsDaemon now reaches flag value validation > runtime enum still enforced [0.10ms] (pass) buildAnetArgsDaemon now reaches flag value validation > channels non-empty rejected (P1 fail-closed) [0.10ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > happy path with hash witness [1.04ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > happy path with hash witness [0.99ms] (pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: no ANET_BIN_ABS at all [0.16ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: relative path [0.16ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: symlink (contains symlink component) [0.57ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: world-writable (mode 0o777) [0.48ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: group-writable (mode 0o775) [0.42ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: not executable (mode 0o644) [0.45ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: owner not root (no opt-out) [0.48ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > ACCEPT: owner not root WHEN ANET_DAEMON_ALLOW_NON_ROOT_BIN=1 (explicit opt-out) [0.37ms] -(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: sha256 mismatch with install witness [0.55ms] -(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > happy path: no extra → PATH includes daemon's own node bin dir + SAFE_PATH (issue #301 nvm fix) [0.40ms] -(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > legitimate extra key passes + fixed PATH keeps execPath prepend (issue #301) [0.22ms] -(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > THROWS on reserved key in extra (LD_PRELOAD smuggled by attacker) [0.22ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: relative path [0.13ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: symlink (contains symlink component) [0.53ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: world-writable (mode 0o777) [0.45ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: group-writable (mode 0o775) [0.38ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: not executable (mode 0o644) [0.36ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: owner not root (no opt-out) [0.68ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > ACCEPT: owner not root WHEN ANET_DAEMON_ALLOW_NON_ROOT_BIN=1 (explicit opt-out) [0.39ms] +(pass) §4.2.6 B2 loadAndVerifyAnetBin — install-time pin 5-check (BLOCKER #3 hardened) > REJECT: sha256 mismatch with install witness [0.49ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > happy path: no extra → PATH includes daemon's own node bin dir + SAFE_PATH (issue #301 nvm fix) [0.38ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > legitimate extra key passes + fixed PATH keeps execPath prepend (issue #301) [0.21ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > THROWS on reserved key in extra (LD_PRELOAD smuggled by attacker) [0.21ms] (pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > THROWS on fixed key in extra (PATH smuggled — caller cannot override the trust-root execPath prepend) [0.11ms] -(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > C1 invariant — issue #301 fix does NOT widen attacker surface: PATH source is process.execPath (daemon's already-resolved node), NOT env.PATH (attacker C1 surface) [0.30ms] -(pass) FAIL_FAST_MS primitive — real subprocess kill-0 lifecycle > child that exits within window → process.kill(pid, 0) raises ESRCH after wait [502.11ms] -(pass) FAIL_FAST_MS primitive — real subprocess kill-0 lifecycle > child that survives window → process.kill(pid, 0) succeeds [203.27ms] +(pass) minimalEnv defensive compose (BLOCKER #1+#2 lineage — kept stable) > C1 invariant — issue #301 fix does NOT widen attacker surface: PATH source is process.execPath (daemon's already-resolved node), NOT env.PATH (attacker C1 surface) [0.32ms] +(pass) FAIL_FAST_MS primitive — real subprocess kill-0 lifecycle > child that exits within window → process.kill(pid, 0) raises ESRCH after wait [503.51ms] +(pass) FAIL_FAST_MS primitive — real subprocess kill-0 lifecycle > child that survives window → process.kill(pid, 0) succeeds [203.84ms] (pass) RFC-027 BLOCKER-1 — childrenMap key shape matches hub canonical node_id > derive key from request_id, not alias [0.22ms] -(pass) RFC-027 BLOCKER-1 — childrenMap key shape matches hub canonical node_id > recordSpawnedChild end-to-end with the canonical key — stop-daemon can find it [8.45ms] +(pass) RFC-027 BLOCKER-1 — childrenMap key shape matches hub canonical node_id > recordSpawnedChild end-to-end with the canonical key — stop-daemon can find it [10.02ms] src/runtime/claude-native-binary.test.ts: -(pass) Claude native binary version pin > uses a directly exported package manifest when available [0.47ms] +(pass) Claude native binary version pin > uses a directly exported package manifest when available [0.42ms] (pass) Claude native binary version pin > walks from the resolved entrypoint when package exports hide package.json [0.35ms] -(pass) Claude native binary version pin > fails closed instead of installing latest when the SDK cannot be attested [0.18ms] -(pass) Claude native binary version pin > missing-binary fallback invokes npm with the installed SDK exact version [0.26ms] +(pass) Claude native binary version pin > fails closed instead of installing latest when the SDK cannot be attested [0.35ms] +(pass) Claude native binary version pin > missing-binary fallback invokes npm with the installed SDK exact version [0.32ms] src/runtime/stop-daemon.test.ts: -(pass) recordSpawnedChild + map shape > records + snapshot returns entry [0.51ms] -(pass) recordSpawnedChild + map shape > re-record overwrites pid [0.24ms] -(pass) handleStopDoorbell — noop_not_my_child > unknown child_node_id → degraded ack (not error) [1.42ms] +(pass) recordSpawnedChild + map shape > records + snapshot returns entry [0.60ms] +(pass) recordSpawnedChild + map shape > re-record overwrites pid [0.29ms] +(pass) handleStopDoorbell — noop_not_my_child > unknown child_node_id → degraded ack (not error) [1.57ms] /bin/sh: 1: pgrep: not found -(pass) handleStopDoorbell — happy stop (SIGTERM-reaped quickly) > child reaped after SIGTERM → ack stopped + SIGTERM signal recorded [8.99ms] +(pass) handleStopDoorbell — happy stop (SIGTERM-reaped quickly) > child reaped after SIGTERM → ack stopped + SIGTERM signal recorded [5.20ms] /bin/sh: 1: pgrep: not found -(pass) handleStopDoorbell — SIGKILL escalation > child ignores SIGTERM → grace exceeded → SIGKILL → ack stopped w/ SIGKILL [33.29ms] +(pass) handleStopDoorbell — SIGKILL escalation > child ignores SIGTERM → grace exceeded → SIGKILL → ack stopped w/ SIGKILL [34.18ms] /bin/sh: 1: pgrep: not found -(pass) handleStopDoorbell — delete action with delete_config > mv child workdir to ~/.anet/deleted/-/ + chmod 700 + ack backup_path [2.58ms] +(pass) handleStopDoorbell — delete action with delete_config > mv child workdir to ~/.anet/deleted/-/ + chmod 700 + ack backup_path [3.60ms] /bin/sh: 1: pgrep: not found -(pass) handleStopDoorbell — delete action with delete_config > delete_config=false → no backup dir, no source move [2.02ms] -(pass) handleStopDoorbell — real subprocess primitive (no mocks) > real subprocess: SIGTERM kills + kill-0 ESRCH after [304.47ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > happy: hub returns 2 children + each has unique matching pid → both recovered [2.52ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > alias substring collision: pgrep finds 'bot2' for alias 'bot' but cmdline argv exact-match rejects [0.77ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > zombie pid skipped (state=Z) [0.55ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > ambiguous: multiple verified pids → skipped (operator intervention) [0.59ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > hub-active but pgrep finds nothing → missing (warn, don't auto-nudge) [0.64ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > daemon's own pid is excluded from candidates [0.53ms] -(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > list_my_children failure → safe empty result (no throw, no map mutation) [0.51ms] -(pass) rebuildChildrenMapOnBoot — real subprocess primitive (no pgrep mocks, no proc mocks) > matcher accepts a real subprocess whose argv contains --alias [203.59ms] +(pass) handleStopDoorbell — delete action with delete_config > delete_config=false → no backup dir, no source move [2.50ms] +(pass) handleStopDoorbell — real subprocess primitive (no mocks) > real subprocess: SIGTERM kills + kill-0 ESRCH after [303.77ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > happy: hub returns 2 children + each has unique matching pid → both recovered [2.00ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > alias substring collision: pgrep finds 'bot2' for alias 'bot' but cmdline argv exact-match rejects [0.73ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > zombie pid skipped (state=Z) [0.57ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > ambiguous: multiple verified pids → skipped (operator intervention) [0.52ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > hub-active but pgrep finds nothing → missing (warn, don't auto-nudge) [0.52ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > daemon's own pid is excluded from candidates [0.41ms] +(pass) rebuildChildrenMapOnBoot (RFC-027 PR1.1) > list_my_children failure → safe empty result (no throw, no map mutation) [0.45ms] +(pass) rebuildChildrenMapOnBoot — real subprocess primitive (no pgrep mocks, no proc mocks) > matcher accepts a real subprocess whose argv contains --alias [203.36ms] src/runtime/claude-error-classify.test.ts: -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > HTTP 429 standalone [0.28ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > HTTP 529 overloaded (Anthropic spec) [0.04ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate_limit_exceeded (Anthropic / OpenAI shape) [0.02ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > HTTP 429 standalone [0.27ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > HTTP 529 overloaded (Anthropic spec) [0.06ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate_limit_exceeded (Anthropic / OpenAI shape) [0.04ms] (pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate-limit hyphen variant [0.03ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate limit space variant [0.04ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > rate limit space variant [0.03ms] (pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > quota exceeded phrase [0.03ms] (pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > quota exhausted phrase [0.03ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > Anthropic spec overloaded_error [0.04ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > plain overloaded mention [0.05ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > Anthropic spec overloaded_error [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > plain overloaded mention [0.06ms] (pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > too_many_requests OpenAI-compat [0.03ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > too many requests space form [0.04ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > too many requests space form [0.03ms] (pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > insufficient_quota OpenAI shape [0.03ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > usage_limit hit [0.02ms] -(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > MiniMax Chinese Token Plan 上限 [0.17ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > usage_limit hit [0.03ms] +(pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > MiniMax Chinese Token Plan 上限 [0.13ms] (pass) isRateLimitOrQuotaError — POSITIVE (must classify as quota/rate-limit) > capacity exceeded vendor message [0.04ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 401 unauthorized (auth, not quota) [0.03ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 403 forbidden (auth, not quota) [0.03ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > plain timeout (not quota) [0.04ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 400 bad request (not quota) [0.03ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 499 client closed (not quota) [0.02ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > ETIMEDOUT network error (not quota) [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 401 unauthorized (auth, not quota) [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 403 forbidden (auth, not quota) [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > plain timeout (not quota) [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 400 bad request (not quota) [0.04ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > 499 client closed (not quota) [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > ETIMEDOUT network error (not quota) [0.02ms] (pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > HTTP 4290 not a real status (avoid false positive on substring) [0.03ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > empty string [0.02ms] -(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > null / undefined [0.03ms] -(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result null + output_tokens 0 [0.11ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > empty string [0.03ms] +(pass) isRateLimitOrQuotaError — NEGATIVE (regression gate, must NOT match) > null / undefined [0.04ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result null + output_tokens 0 [0.10ms] (pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result undefined (M3 incident shape) [0.04ms] -(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result empty string but usage non-zero [0.03ms] -(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result has text but output_tokens 0 (suspicious) [0.04ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result empty string but usage non-zero [0.04ms] +(pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > result has text but output_tokens 0 (suspicious) [0.05ms] (pass) isEmptyResultSoftFailure — POSITIVE (must flag as empty-vendor-reply) > usage missing entirely (defaulting to 1 = non-zero) but result empty [0.04ms] (pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > normal success — result + non-zero tokens [0.04ms] -(pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > short single-char reply still counts as success [0.03ms] +(pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > short single-char reply still counts as success [0.02ms] (pass) isEmptyResultSoftFailure — NEGATIVE (regression gate, normal success) > usage entirely missing but result non-empty [0.03ms] -(pass) quotaRemediationHint — vendor URL routing > intern-ai routing [0.18ms] -(pass) quotaRemediationHint — vendor URL routing > minimax routing [0.06ms] -(pass) quotaRemediationHint — vendor URL routing > deepseek routing [0.05ms] +(pass) quotaRemediationHint — vendor URL routing > intern-ai routing [0.14ms] +(pass) quotaRemediationHint — vendor URL routing > minimax routing [0.07ms] +(pass) quotaRemediationHint — vendor URL routing > deepseek routing [0.06ms] (pass) quotaRemediationHint — vendor URL routing > anthropic-native routing [0.05ms] (pass) quotaRemediationHint — vendor URL routing > unknown vendor falls back to generic hint [0.06ms] -(pass) quotaRemediationHint — vendor URL routing > empty / undefined → generic [0.07ms] +(pass) quotaRemediationHint — vendor URL routing > empty / undefined → generic [0.04ms] src/runtime/grok-build-cli.test.ts: -(pass) buildGrokCliArgs > rejects an older Grok CLI before it can ignore required safety flags [0.50ms] -(pass) buildGrokCliArgs > uses streaming headless mode and resumes an existing session [0.32ms] +(pass) buildGrokCliArgs > rejects an older Grok CLI before it can ignore required safety flags [0.58ms] +(pass) buildGrokCliArgs > uses streaming headless mode and resumes an existing session [0.36ms] (pass) buildGrokCliArgs > fails closed instead of auto-approving when permission bypass is disabled [0.11ms] -(pass) buildGrokCliArgs > maps an explicit node tool allowlist and keeps MCP unavailable [0.22ms] -(pass) buildGrokCliArgs > intersects explicit tools with the read-only set when auto-approval is off [0.09ms] -(pass) buildGrokCliArgs > rejects unknown node tool names instead of silently widening access [0.08ms] -(pass) buildGrokCliArgs > rejects an explicit empty tool allowlist instead of widening to all tools [0.07ms] -(pass) buildGrokCliArgs > denies model reads of runtime credential and node-state paths [0.10ms] -(pass) runGrokCliTurn > reports spawn submission before first exact JSONL event consumption [70.47ms] -(pass) runGrokCliTurn > reduces streaming JSON text and persists the end-event session [41.33ms] -(pass) runGrokCliTurn > spawns with exactly the projected environment and no ambient credentials [45.58ms] -(pass) runGrokCliTurn > keeps the production-shaped setpriv/sh launcher on the exact PWD-bound env [48.45ms] -(pass) runGrokCliTurn > refuses a shell launcher when PWD is missing from the reviewed env [0.89ms] -(pass) runGrokCliTurn > removes the prompt when spawn rejects a malformed allowed env value [1.31ms] -(pass) runGrokCliTurn > surfaces non-zero exits and stderr [43.58ms] -(pass) runGrokCliTurn > fails fast when headless Grok asks for an interactive login [41.17ms] -(pass) runGrokCliTurn > rejects cancelled turns [44.82ms] -(pass) runGrokCliTurn > rejects a formal error event even if the process exits zero [46.95ms] -(pass) runGrokCliTurn > rejects max-turn truncation instead of reporting a partial reply as success [47.68ms] -(pass) runGrokCliTurn > terminates the process group when the caller aborts [35.18ms] -(pass) runGrokCliTurn > kills a silent child after the idle timeout [36.63ms] -(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > passes when the probe succeeds [0.46ms] -(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > throws with the real stderr and an actionable next step when uid_map is refused [0.20ms] -(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > still throws when the probe fails with no stderr at all [0.21ms] -(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > honours a custom unshare binary path [0.09ms] +(pass) buildGrokCliArgs > maps an explicit node tool allowlist and keeps MCP unavailable [0.21ms] +(pass) buildGrokCliArgs > intersects explicit tools with the read-only set when auto-approval is off [0.13ms] +(pass) buildGrokCliArgs > rejects unknown node tool names instead of silently widening access [0.12ms] +(pass) buildGrokCliArgs > rejects an explicit empty tool allowlist instead of widening to all tools [0.08ms] +(pass) buildGrokCliArgs > denies model reads of runtime credential and node-state paths [0.07ms] +(pass) runGrokCliTurn > reports spawn submission before first exact JSONL event consumption [81.05ms] +(pass) runGrokCliTurn > reduces streaming JSON text and persists the end-event session [52.92ms] +(pass) runGrokCliTurn > spawns with exactly the projected environment and no ambient credentials [53.13ms] +(pass) runGrokCliTurn > keeps the production-shaped setpriv/sh launcher on the exact PWD-bound env [69.35ms] +(pass) runGrokCliTurn > refuses a shell launcher when PWD is missing from the reviewed env [1.23ms] +(pass) runGrokCliTurn > removes the prompt when spawn rejects a malformed allowed env value [1.44ms] +(pass) runGrokCliTurn > surfaces non-zero exits and stderr [49.86ms] +(pass) runGrokCliTurn > fails fast when headless Grok asks for an interactive login [51.16ms] +(pass) runGrokCliTurn > rejects cancelled turns [53.11ms] +(pass) runGrokCliTurn > rejects a formal error event even if the process exits zero [50.56ms] +(pass) runGrokCliTurn > rejects max-turn truncation instead of reporting a partial reply as success [59.56ms] +(pass) runGrokCliTurn > terminates the process group when the caller aborts [37.58ms] +(pass) runGrokCliTurn > kills a silent child after the idle timeout [40.50ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > passes when the probe succeeds [0.44ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > throws with the real stderr and an actionable next step when uid_map is refused [0.23ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > still throws when the probe fails with no stderr at all [0.27ms] +(pass) assertUnprivilegedUserNsUsable (#grok userns preflight) > honours a custom unshare binary path [0.11ms] src/runtime/grok-child-env.test.ts: -(pass) Grok child environment boundary > builds the exact reviewed key set and drops every unreviewed credential [0.37ms] -(pass) Grok child environment boundary > re-projects a beforeSpawn result instead of trusting arbitrary keys [0.11ms] -(pass) Grok child environment boundary > rejects a beforeSpawn callback that changes a controlled value [1.01ms] -(pass) Grok child environment boundary > keeps the inherited list exact and reviewable [0.06ms] -(pass) Grok child environment boundary > keeps PTY PWD equal and adds only reviewed terminal/sandbox controls [0.57ms] +(pass) Grok child environment boundary > builds the exact reviewed key set and drops every unreviewed credential [0.36ms] +(pass) Grok child environment boundary > re-projects a beforeSpawn result instead of trusting arbitrary keys [0.15ms] +(pass) Grok child environment boundary > rejects a beforeSpawn callback that changes a controlled value [1.41ms] +(pass) Grok child environment boundary > keeps the inherited list exact and reviewable [0.07ms] +(pass) Grok child environment boundary > keeps PTY PWD equal and adds only reviewed terminal/sandbox controls [0.52ms] (pass) Grok child environment boundary > builds the narrower helper environment from an empty object [0.23ms] src/runtime/node-id-source.test.ts: -(pass) resolveNodeIdSource > configured identity wins over a polluted supervisor env [0.24ms] -(pass) resolveNodeIdSource > matching launcher env is accepted without a warning [0.07ms] +(pass) resolveNodeIdSource > configured identity wins over a polluted supervisor env [2.32ms] +(pass) resolveNodeIdSource > matching launcher env is accepted without a warning [0.17ms] (pass) resolveNodeIdSource > legacy config without node_id keeps the env fallback [0.04ms] -(pass) resolveNodeIdSource > missing identity remains empty [0.02ms] -(pass) resolveNodeIdSource > warning escapes control characters from inherited env [0.14ms] +(pass) resolveNodeIdSource > missing identity remains empty [0.04ms] +(pass) resolveNodeIdSource > warning escapes control characters from inherited env [0.25ms] src/runtime/inbox-drain-lane.test.ts: -(pass) inbox drain lanes > an informational lane drains while the work lane is busy [0.50ms] -(pass) inbox drain lanes > each lane remains serial [0.32ms] -(pass) inbox drain lanes > repeated wakeups for the same drain coalesce into one dirty rerun [0.32ms] -(pass) inbox drain lanes > a failed drain is reported and does not poison later retries [0.33ms] -(pass) inbox drain lanes > retry mode backs off and eventually completes the same drain [3.33ms] -(pass) inbox drain lanes > one failed inbox item does not starve later items in the same snapshot [0.49ms] -(pass) inbox drain lanes > ack-only retry does not duplicate the first notification or delay the second [1.41ms] +(pass) inbox drain lanes > an informational lane drains while the work lane is busy [0.71ms] +(pass) inbox drain lanes > each lane remains serial [0.38ms] +(pass) inbox drain lanes > repeated wakeups for the same drain coalesce into one dirty rerun [0.74ms] +(pass) inbox drain lanes > a failed drain is reported and does not poison later retries [0.51ms] +(pass) inbox drain lanes > retry mode backs off and eventually completes the same drain [3.47ms] +(pass) inbox drain lanes > one failed inbox item does not starve later items in the same snapshot [0.64ms] +(pass) inbox drain lanes > ack-only retry does not duplicate the first notification or delay the second [1.61ms] src/runtime/codex-app-server-client.test.ts: -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > reverse request (method + id) routes to `reverse_request`, NOT orphan_response [15.08ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > reverse request also fires `reverse:` targeted event [11.01ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > notification (method + no id) routes to method-keyed event [8.33ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > response (id + result) resolves the matching pending request [10.02ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > response (id + error) rejects with codex-formatted Error [8.26ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > orphan response (id present, no matching pending) fires `orphan_response` [9.48ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > malformed messages fire `malformed` [7.96ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > parse errors on non-JSON payload fire `parse_error` [10.30ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > request timeout rejects the pending promise and cleans up the entry [45.40ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > close rejects any in-flight request cleanly (no unhandled rejection) [4.30ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > respondToReverseRequest emits a well-formed response envelope [13.01ms] -(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > errorReverseRequest emits a JSON-RPC error envelope [10.09ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > reverse request (method + id) routes to `reverse_request`, NOT orphan_response [16.15ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > reverse request also fires `reverse:` targeted event [9.97ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > notification (method + no id) routes to method-keyed event [9.98ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > response (id + result) resolves the matching pending request [9.18ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > response (id + error) rejects with codex-formatted Error [14.46ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > orphan response (id present, no matching pending) fires `orphan_response` [10.59ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > malformed messages fire `malformed` [8.54ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > parse errors on non-JSON payload fire `parse_error` [10.32ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > request timeout rejects the pending promise and cleans up the entry [45.60ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > close rejects any in-flight request cleanly (no unhandled rejection) [4.05ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > respondToReverseRequest emits a well-formed response envelope [13.52ms] +(pass) CodexAppServerClient — dispatch correctness (RFC-030 §7 + bug fix) > errorReverseRequest emits a JSON-RPC error envelope [10.65ms] (pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > wraps an empty TypeError with endpoint and remediation [0.71ms] -(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > scrubs nested causes and bearer credentials independently of runtime shape [0.27ms] -(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > synchronous WebSocket constructor failure uses the same safe boundary [0.50ms] -(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > real dead loopback with query credential rejects/emits without leaking it [1.09ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > scrubs nested causes and bearer credentials independently of runtime shape [0.25ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > synchronous WebSocket constructor failure uses the same safe boundary [0.54ms] +(pass) CodexAppServerClient — dead shared endpoint diagnostics (#455) > real dead loopback with query credential rejects/emits without leaking it [1.12ms] src/runtime/delegation-precheck.test.ts: -(pass) delegationTargetExists > imperative happy path — real other session is found [0.17ms] -(pass) delegationTargetExists > #230 — descriptive-text false positive no longer self-reflects [0.07ms] -(pass) delegationTargetExists > self-only match — only the calling node has this alias [0.05ms] -(pass) delegationTargetExists > typo alias — caller meant a real agent but mistyped [0.05ms] -(pass) delegationTargetExists > empty sessions array → empty_sessions [0.10ms] +(pass) delegationTargetExists > imperative happy path — real other session is found [0.20ms] +(pass) delegationTargetExists > #230 — descriptive-text false positive no longer self-reflects [0.08ms] +(pass) delegationTargetExists > self-only match — only the calling node has this alias [0.07ms] +(pass) delegationTargetExists > typo alias — caller meant a real agent but mistyped [0.06ms] +(pass) delegationTargetExists > empty sessions array → empty_sessions [0.05ms] (pass) delegationTargetExists > missing sessions field (caller did not destructure correctly) → no_sessions_field [0.06ms] -(pass) delegationTargetExists > empty target alias is defensively reported as not_in_sessions [0.03ms] -(pass) delegationTargetExists > whitespace padding is trimmed before comparison [0.04ms] -(pass) delegationTargetExists > sessions with missing / non-string alias fields are skipped without throwing [0.05ms] +(pass) delegationTargetExists > empty target alias is defensively reported as not_in_sessions [0.04ms] +(pass) delegationTargetExists > whitespace padding is trimmed before comparison [0.05ms] +(pass) delegationTargetExists > sessions with missing / non-string alias fields are skipped without throwing [0.06ms] src/runtime/classify-result.test.ts: (pass) classifyRuntimeResult — error precedence > quota error msg → soft-fail-quota (highest precedence) [0.16ms] -(pass) classifyRuntimeResult — error precedence > non-quota error → hard error [0.04ms] -(pass) classifyRuntimeResult — error precedence > auth error msg (401) → hard error (NOT quota — auth has its own path) [0.02ms] -(pass) classifyRuntimeResult — error precedence > error msg outranks empty result (don't double-classify) [0.04ms] +(pass) classifyRuntimeResult — error precedence > non-quota error → hard error [0.05ms] +(pass) classifyRuntimeResult — error precedence > auth error msg (401) → hard error (NOT quota — auth has its own path) [0.03ms] +(pass) classifyRuntimeResult — error precedence > error msg outranks empty result (don't double-classify) [0.03ms] (pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > all three zero → soft-fail-empty (even when result text present) [0.05ms] -(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > in=0 & out=0 but cost field MISSING + non-empty result → success (codex usage unreliable) [0.03ms] -(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > in=0 & cost=0 but out>0 → NOT silent reject (vendor returned something) [0.03ms] -(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > normal turn (all signals positive) → success [0.05ms] -(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > non-empty result + output_tokens=0 + cost missing → success (codex false-positive guard) [0.05ms] -(pass) classifyRuntimeResult — empty-result rule (strict) > empty string result + non-zero tokens → soft-fail-empty [0.04ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > in=0 & out=0 but cost field MISSING + non-empty result → success (codex usage unreliable) [0.05ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > in=0 & cost=0 but out>0 → NOT silent reject (vendor returned something) [0.05ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > normal turn (all signals positive) → success [0.04ms] +(pass) classifyRuntimeResult — in=0 & out=0 & cost=0 silent reject > non-empty result + output_tokens=0 + cost missing → success (codex false-positive guard) [0.09ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > empty string result + non-zero tokens → soft-fail-empty [0.05ms] (pass) classifyRuntimeResult — empty-result rule (strict) > null result + non-zero tokens → soft-fail-empty [0.04ms] (pass) classifyRuntimeResult — empty-result rule (strict) > undefined result, missing usage → soft-fail-empty (empty result alone is enough) [0.04ms] -(pass) classifyRuntimeResult — empty-result rule (strict) > single-char '0' result + tokens → success (not empty) [0.04ms] +(pass) classifyRuntimeResult — empty-result rule (strict) > single-char '0' result + tokens → success (not empty) [0.05ms] (pass) classifyRuntimeResult — empty-result rule (strict) > result text present + missing usage → success (don't penalise unreported usage) [0.04ms] (pass) classifyRuntimeResult — empty-result rule (strict) > empty string result + cost present + tokens → soft-fail-empty (text emptiness is the signal) [0.04ms] -(pass) classifyRuntimeResult — vendor hint routing via baseUrl > quota error with deepseek baseUrl → deepseek dashboard hint [0.07ms] -(pass) classifyRuntimeResult — vendor hint routing via baseUrl > quota error with intern baseUrl → intern hint [0.05ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > quota error with deepseek baseUrl → deepseek dashboard hint [0.06ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > quota error with intern baseUrl → intern hint [0.06ms] (pass) classifyRuntimeResult — vendor hint routing via baseUrl > empty result with anthropic baseUrl → anthropic hint [0.05ms] -(pass) classifyRuntimeResult — vendor hint routing via baseUrl > missing baseUrl → generic hint [0.05ms] +(pass) classifyRuntimeResult — vendor hint routing via baseUrl > missing baseUrl → generic hint [0.03ms] (pass) formatClassificationError — message shape (parsed by IM bridge) > soft-fail-quota → 执行出错: [额度用尽][] : — [0.45ms] -(pass) formatClassificationError — message shape (parsed by IM bridge) > soft-fail-empty → 执行出错: 返回空响应 with in/out [0.07ms] -(pass) formatClassificationError — message shape (parsed by IM bridge) > error kind → 执行出错: [0.05ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > soft-fail-empty → 执行出错: 返回空响应 with in/out [0.08ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > error kind → 执行出错: [0.06ms] (pass) formatClassificationError — message shape (parsed by IM bridge) > success kind → empty string (caller should not call this; defensive) [0.04ms] -(pass) formatClassificationError — message shape (parsed by IM bridge) > missing usage in context → in=0 out=0 fallback [0.05ms] -(pass) formatClassificationError — message shape (parsed by IM bridge) > missing hint on quota → no trailing dash artifact [0.10ms] -(pass) formatClassificationError — message shape (parsed by IM bridge) > reason longer than 80 chars is truncated on quota path [0.08ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > missing usage in context → in=0 out=0 fallback [0.04ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > missing hint on quota → no trailing dash artifact [0.09ms] +(pass) formatClassificationError — message shape (parsed by IM bridge) > reason longer than 80 chars is truncated on quota path [0.07ms] src/runtime/codex-app-server-bridge.test.ts: -(pass) CodexAppServerBridge — bootstrap + task mapping > bootstrap sends initialize + initialized + thread/resume in order [5.95ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > empty threadId → bootstrap creates a thread (thread/start) and adopts its id [8.22ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > stale threadId with no rollout → resume fails, bootstrap falls back to thread/start [6.33ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > startTaskTurn returns the server-assigned turnId and marks bridge working [4.55ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed for OUR turn fires task_reply mapped back to the task_id [14.64ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > only exact owned-turn item events emit task_activity [16.26ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > authenticated Dashboard native /goal text reaches the shared thread unchanged and replies [17.28ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > clientUserMessageId rebinds a task when a goal successor replaces the turn/start response id [54.40ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > client-id ownership observed before the RPC response wins without reversing task event order [25.98ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > real bridge + runtime bounds a deferred terminal when exact client identity never arrives [38.44ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > real bridge + runtime bounds an unresolved turn/start through the left-FIFO fallback [61.72ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > agentMessage/delta accumulates when server omits finalText [17.12ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed for a HUMAN-TUI-initiated turn is dropped (§7.5) [15.98ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > events for a DIFFERENT thread are dropped (defense in depth) [16.60ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > startTaskTurn refuses a second task while one is active [7.15ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed with an error field fires task_error, NOT task_reply [17.11ms] -(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed with interrupted status cannot become a successful reply [16.64ms] -(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > reverse-request approval records waiting_human and sends NO response [18.33ms] -(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > serverRequest/resolved clears waiting_human and status recovers [28.77ms] -(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > multiple concurrent approvals: bridge stays waiting_human until all resolve [37.97ms] -(pass) CodexAppServerBridge — two-client race for idle > only one bridge wins turn/start; the other observes and does not reply [22.05ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect recovers an active human turn and keeps it steerable [6.99ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect provenance keeps an orphaned network turn FIFO-only [27.23ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect provenance ignores leading whitespace before the network prefix [5.78ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect stays FIFO-only when real-wire active history omits userMessage [4.57ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > uses exact turn/steer contract and maps the human turn final answer [25.41ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > multiple Dashboard rows steer one human turn while ordinary agent work stays queued [40.11ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > steer mismatch fails closed and preserves the task in the normal FIFO [40.20ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > turn completion cannot attribute a task before turn/steer acceptance [43.03ms] -(pass) CodexAppServerBridge — authenticated Dashboard steering > reconciliation recovers a missed human turn completion and exact steered reply [15.62ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > concurrent startTaskTurn: exactly ONE turn/start reaches the server even with a slow response [55.86ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > submitTask queues the second task and drains it after turn/completed (order preserved) [118.01ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > cancelQueuedTask removes only the named FIFO row before it can execute [56.89ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read recovers a completed owned turn while a successor keeps the thread active [107.35ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read uses clientUserMessageId to recover a replacement turn when all live item events were lost [4.96ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > slow full-history fallback recovers when both terminal and successor notifications are lost [3.87ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > full history never attributes a different completed turn to the owned task [3.91ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read never recovers an interrupted turn as success [2.80ms] -(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > drain losing the idle race requeues at the FRONT and retries on next idle [166.89ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > bootstrap sends initialize + initialized + thread/resume in order [5.24ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > empty threadId → bootstrap creates a thread (thread/start) and adopts its id [9.39ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > stale threadId with no rollout → resume fails, bootstrap falls back to thread/start [7.65ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > startTaskTurn returns the server-assigned turnId and marks bridge working [5.06ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed for OUR turn fires task_reply mapped back to the task_id [15.15ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > only exact owned-turn item events emit task_activity [17.28ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > authenticated Dashboard native /goal text reaches the shared thread unchanged and replies [16.98ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > clientUserMessageId rebinds a task when a goal successor replaces the turn/start response id [56.38ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > client-id ownership observed before the RPC response wins without reversing task event order [24.51ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > real bridge + runtime bounds a deferred terminal when exact client identity never arrives [39.03ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > real bridge + runtime bounds an unresolved turn/start through the left-FIFO fallback [63.61ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > agentMessage/delta accumulates when server omits finalText [17.81ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed for a HUMAN-TUI-initiated turn is dropped (§7.5) [18.60ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > events for a DIFFERENT thread are dropped (defense in depth) [18.72ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > startTaskTurn refuses a second task while one is active [5.49ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed with an error field fires task_error, NOT task_reply [17.04ms] +(pass) CodexAppServerBridge — bootstrap + task mapping > turn/completed with interrupted status cannot become a successful reply [16.87ms] +(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > reverse-request approval records waiting_human and sends NO response [21.05ms] +(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > serverRequest/resolved clears waiting_human and status recovers [30.33ms] +(pass) CodexAppServerBridge — approvals (waiting_human) §7.6 > multiple concurrent approvals: bridge stays waiting_human until all resolve [43.56ms] +(pass) CodexAppServerBridge — two-client race for idle > only one bridge wins turn/start; the other observes and does not reply [23.01ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect recovers an active human turn and keeps it steerable [7.35ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect provenance keeps an orphaned network turn FIFO-only [28.62ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect provenance ignores leading whitespace before the network prefix [6.68ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconnect stays FIFO-only when real-wire active history omits userMessage [4.27ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > uses exact turn/steer contract and maps the human turn final answer [25.54ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > multiple Dashboard rows steer one human turn while ordinary agent work stays queued [40.90ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > steer mismatch fails closed and preserves the task in the normal FIFO [39.82ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > turn completion cannot attribute a task before turn/steer acceptance [41.08ms] +(pass) CodexAppServerBridge — authenticated Dashboard steering > reconciliation recovers a missed human turn completion and exact steered reply [16.87ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > concurrent startTaskTurn: exactly ONE turn/start reaches the server even with a slow response [56.43ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > submitTask queues the second task and drains it after turn/completed (order preserved) [117.45ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > cancelQueuedTask removes only the named FIFO row before it can execute [58.50ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read recovers a completed owned turn while a successor keeps the thread active [107.60ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read uses clientUserMessageId to recover a replacement turn when all live item events were lost [5.34ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > slow full-history fallback recovers when both terminal and successor notifications are lost [3.88ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > full history never attributes a different completed turn to the owned task [4.90ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > thread/read never recovers an interrupted turn as success [4.86ms] +(pass) CodexAppServerBridge — sync claim + FIFO queue (通信龙) > drain losing the idle race requeues at the FRONT and retries on next idle [169.17ms] src/runtime/probe-daemon.test.ts: -(pass) createPinnedLookup — Node/Bun lookup callback contract > single-address callback honors requested family [0.58ms] -(pass) createPinnedLookup — Node/Bun lookup callback contract > all-address callback returns only pinned copies [0.20ms] -(pass) createPinnedLookup — Node/Bun lookup callback contract > wrong hostname and unavailable family fail closed without fallback [0.35ms] -(pass) assertSecureTlsEnv (boot guard) > clean env passes [0.10ms] -(pass) assertSecureTlsEnv (boot guard) > NODE_TLS_REJECT_UNAUTHORIZED=0 throws [0.14ms] +(pass) createPinnedLookup — Node/Bun lookup callback contract > single-address callback honors requested family [0.72ms] +(pass) createPinnedLookup — Node/Bun lookup callback contract > all-address callback returns only pinned copies [0.29ms] +(pass) createPinnedLookup — Node/Bun lookup callback contract > wrong hostname and unavailable family fail closed without fallback [0.43ms] +(pass) assertSecureTlsEnv (boot guard) > clean env passes [0.11ms] +(pass) assertSecureTlsEnv (boot guard) > NODE_TLS_REJECT_UNAUTHORIZED=0 throws [0.12ms] (pass) classifyProbeResponse — status enum mapping > 200 → ok [0.16ms] -(pass) classifyProbeResponse — status enum mapping > 401 → auth_fail [0.03ms] +(pass) classifyProbeResponse — status enum mapping > 401 → auth_fail [0.04ms] (pass) classifyProbeResponse — status enum mapping > 403 → auth_fail [0.03ms] -(pass) classifyProbeResponse — status enum mapping > 429 → quota [0.03ms] -(pass) classifyProbeResponse — status enum mapping > 500 → vendor_5xx [0.04ms] +(pass) classifyProbeResponse — status enum mapping > 429 → quota [0.04ms] +(pass) classifyProbeResponse — status enum mapping > 500 → vendor_5xx [0.03ms] (pass) classifyProbeResponse — status enum mapping > 404 → other_4xx [0.03ms] (pass) classifyProbeResponse — status enum mapping > errorKind=redirect_forbidden surfaces directly [0.04ms] -(pass) classifyProbeResponse — status enum mapping > errorKind=timeout surfaces [0.05ms] -(pass) classifyProbeResponse — status enum mapping > errorKind=probe_resolve_unsafe_ip → returned status string passes through [0.04ms] -(pass) classifyProbeResponse — status enum mapping > ack has NO error_message / response_body / url fields (zod whitelist on hub side will reject; we just don't include) [0.08ms] -(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with private IP literal (169.254.169.254) → probe_resolve_unsafe_ip [1.45ms] -(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with private IP literal (10.0.0.1) → probe_resolve_unsafe_ip [0.13ms] -(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with localhost without ALLOW_LOOPBACK env → probe_resolve_unsafe_ip [0.13ms] -(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with localhost WITH ALLOW_LOOPBACK env → permitted to proceed (will fail on real network but not on IP guard) [5.25ms] -(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > NODE_TLS_REJECT_UNAUTHORIZED=0 → tls_error before any fetch [0.18ms] -(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > non-allowlist host for anthropic → daemon-level reject + ack probe_target_forbidden, no fetch [1.32ms] -(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > unknown vendor → daemon rejects, ack probe_target_forbidden [0.27ms] -(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > bad URL (not parseable) → daemon rejects, ack probe_target_forbidden [0.27ms] -(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > plain HTTP scheme on non-loopback host → daemon rejects, ack probe_target_forbidden [0.20ms] -(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > get_probe_request returns ok:false → no ack pushed (hub sweeper handles) [0.20ms] +(pass) classifyProbeResponse — status enum mapping > errorKind=timeout surfaces [0.03ms] +(pass) classifyProbeResponse — status enum mapping > errorKind=probe_resolve_unsafe_ip → returned status string passes through [0.05ms] +(pass) classifyProbeResponse — status enum mapping > ack has NO error_message / response_body / url fields (zod whitelist on hub side will reject; we just don't include) [0.06ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with private IP literal (169.254.169.254) → probe_resolve_unsafe_ip [1.72ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with private IP literal (10.0.0.1) → probe_resolve_unsafe_ip [0.19ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with localhost without ALLOW_LOOPBACK env → probe_resolve_unsafe_ip [0.12ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > base_url with localhost WITH ALLOW_LOOPBACK env → permitted to proceed (will fail on real network but not on IP guard) [5.78ms] +(pass) safelyFetchProbe — SSRF guards (per 通信龙 spot-check c) > NODE_TLS_REJECT_UNAUTHORIZED=0 → tls_error before any fetch [0.13ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > non-allowlist host for anthropic → daemon-level reject + ack probe_target_forbidden, no fetch [1.44ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > unknown vendor → daemon rejects, ack probe_target_forbidden [0.32ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > bad URL (not parseable) → daemon rejects, ack probe_target_forbidden [0.29ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > plain HTTP scheme on non-loopback host → daemon rejects, ack probe_target_forbidden [0.21ms] +(pass) handleProbeDoorbell — daemon validateBaseUrl re-check (compromised-hub defense) > get_probe_request returns ok:false → no ack pushed (hub sweeper handles) [0.31ms] src/runtime/current-alias.test.ts: -(pass) CurrentAliasResolver — startup snapshot > current() returns the initial alias before any refresh() [0.20ms] -(pass) CurrentAliasResolver — startup snapshot > ageMs() reports Infinity before first fetch (cache is cold) [0.14ms] -(pass) CurrentAliasResolver — refresh() cache behaviour > warm cache short-circuits — no fetch fired within TTL [0.63ms] -(pass) CurrentAliasResolver — refresh() cache behaviour > expired cache hits the server and updates the alias + fires onDrift [0.38ms] -(pass) CurrentAliasResolver — refresh() cache behaviour > concurrent refresh() calls dedupe onto one fetch [11.96ms] -(pass) CurrentAliasResolver — graceful fetch failure > fetch throwing keeps the cached value and emits a warn [0.55ms] +(pass) CurrentAliasResolver — startup snapshot > current() returns the initial alias before any refresh() [0.25ms] +(pass) CurrentAliasResolver — startup snapshot > ageMs() reports Infinity before first fetch (cache is cold) [0.11ms] +(pass) CurrentAliasResolver — refresh() cache behaviour > warm cache short-circuits — no fetch fired within TTL [0.61ms] +(pass) CurrentAliasResolver — refresh() cache behaviour > expired cache hits the server and updates the alias + fires onDrift [0.39ms] +(pass) CurrentAliasResolver — refresh() cache behaviour > concurrent refresh() calls dedupe onto one fetch [10.59ms] +(pass) CurrentAliasResolver — graceful fetch failure > fetch throwing keeps the cached value and emits a warn [0.59ms] (pass) CurrentAliasResolver — graceful fetch failure > fetch returning null is treated as 'server does not know yet' [0.20ms] -(pass) CurrentAliasResolver — graceful fetch failure > fetch returning empty string is also treated as 'server does not know' [0.15ms] -(pass) CurrentAliasResolver — graceful fetch failure > after a failed fetch the cache timestamp still bumps — no hammering [0.29ms] -(pass) CurrentAliasResolver — set() force install > set() updates the alias and fires onDrift with source 'snapshot' [0.22ms] -(pass) CurrentAliasResolver — set() force install > set() with the same value is a no-op (no drift event, but cache timestamp bumps) [0.09ms] -(pass) CurrentAliasResolver — set() force install > set('') is ignored (defends against caller forgetting to validate) [0.06ms] -(pass) CurrentAliasResolver — edge cases > nodeId = null short-circuits refresh() and never calls the fetch hook [0.17ms] -(pass) CurrentAliasResolver — edge cases > cacheTtlMs = 0 disables caching — every refresh() fetches [0.22ms] -(pass) CurrentAliasResolver — edge cases > ageMs() reflects elapsed time after a refresh [0.16ms] +(pass) CurrentAliasResolver — graceful fetch failure > fetch returning empty string is also treated as 'server does not know' [0.19ms] +(pass) CurrentAliasResolver — graceful fetch failure > after a failed fetch the cache timestamp still bumps — no hammering [0.37ms] +(pass) CurrentAliasResolver — set() force install > set() updates the alias and fires onDrift with source 'snapshot' [0.25ms] +(pass) CurrentAliasResolver — set() force install > set() with the same value is a no-op (no drift event, but cache timestamp bumps) [0.11ms] +(pass) CurrentAliasResolver — set() force install > set('') is ignored (defends against caller forgetting to validate) [0.08ms] +(pass) CurrentAliasResolver — edge cases > nodeId = null short-circuits refresh() and never calls the fetch hook [0.21ms] +(pass) CurrentAliasResolver — edge cases > cacheTtlMs = 0 disables caching — every refresh() fetches [0.24ms] +(pass) CurrentAliasResolver — edge cases > ageMs() reflects elapsed time after a refresh [0.22ms] src/runtime/feishu-outbound-dir.test.ts: -(pass) Feishu legacy outbound directory > prefers the canonical worker value verbatim [0.12ms] -(pass) Feishu legacy outbound directory > reconstructs a legacy envelope from the explicit channel binding [0.07ms] -(pass) Feishu legacy outbound directory > does not consult a stale ambient node alias [0.10ms] -(pass) Feishu legacy outbound directory > passes the same explicit binding name to the worker [0.10ms] +(pass) Feishu legacy outbound directory > prefers the canonical worker value verbatim [0.13ms] +(pass) Feishu legacy outbound directory > reconstructs a legacy envelope from the explicit channel binding [0.09ms] +(pass) Feishu legacy outbound directory > does not consult a stale ambient node alias [0.12ms] +(pass) Feishu legacy outbound directory > passes the same explicit binding name to the worker [0.13ms] src/runtime/deleted-sweeper.test.ts: -(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > backup older than RETENTION_MS → physically removed [1.37ms] -(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > backup younger than 30d → KEPT [0.51ms] -(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > mixed: 2 old + 1 recent → only the 2 olds purged [0.85ms] -(pass) sweeper safety invariants (D7 nit) > skips dir names that don't match - pattern (no accidental purge) [0.48ms] -(pass) sweeper safety invariants (D7 nit) > log function receives ONLY the dir name — never any inner file path [0.50ms] -(pass) sweeper safety invariants (D7 nit) > dir-listing error (deletedRoot missing) → returns clean empty result, no throw [0.39ms] -[deleted-sweeper] failed to purge 1783988499555-bad: simulated EACCES -(pass) sweeper safety invariants (D7 nit) > rmDir throw → counted as error, sweep continues for siblings [0.91ms] +(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > backup older than RETENTION_MS → physically removed [1.72ms] +(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > backup younger than 30d → KEPT [0.59ms] +(pass) RFC-027 §5.2 K — sweeper purges 30d+ backups (physical delete, no soft state) > mixed: 2 old + 1 recent → only the 2 olds purged [0.94ms] +(pass) sweeper safety invariants (D7 nit) > skips dir names that don't match - pattern (no accidental purge) [0.54ms] +(pass) sweeper safety invariants (D7 nit) > log function receives ONLY the dir name — never any inner file path [0.59ms] +(pass) sweeper safety invariants (D7 nit) > dir-listing error (deletedRoot missing) → returns clean empty result, no throw [0.47ms] +[deleted-sweeper] failed to purge 1783994859460-bad: simulated EACCES +(pass) sweeper safety invariants (D7 nit) > rmDir throw → counted as error, sweep continues for siblings [1.04ms] src/runtime/config-apply.test.ts: -(pass) RESTART_SENTINEL — exact value pin > equals 75 (BSD EX_TEMPFAIL semantics, parent supervisor checks this exact code) [0.24ms] -(pass) #633 private text writer > replaces a leaf symlink without following it [2.30ms] -(pass) validateLocalPatch — defense-in-depth > undefined model + empty flags passes (no-op patch) [0.41ms] -(pass) validateLocalPatch — defense-in-depth > valid full patch passes [0.20ms] -(pass) validateLocalPatch — defense-in-depth > unknown flag rejected (even if hub validator drifts loose) [0.18ms] -(pass) validateLocalPatch — defense-in-depth > permissionMode invalid enum rejected [0.17ms] -(pass) validateLocalPatch — defense-in-depth > dangerouslySkipPermissions non-boolean rejected [0.19ms] -(pass) validateLocalPatch — defense-in-depth > maxTurns out of range rejected [0.26ms] -(pass) validateLocalPatch — defense-in-depth > timeout invalid rejected [0.22ms] -(pass) validateLocalPatch — defense-in-depth > empty-string model rejected [0.15ms] -(pass) computeApplyMode — tier classifier > empty patch → restart_only (restart_node) [0.21ms] -(pass) computeApplyMode — tier classifier > model only → restart [0.14ms] +(pass) RESTART_SENTINEL — exact value pin > equals 75 (BSD EX_TEMPFAIL semantics, parent supervisor checks this exact code) [0.27ms] +(pass) #633 private text writer > replaces a leaf symlink without following it [2.34ms] +(pass) validateLocalPatch — defense-in-depth > undefined model + empty flags passes (no-op patch) [0.37ms] +(pass) validateLocalPatch — defense-in-depth > valid full patch passes [0.18ms] +(pass) validateLocalPatch — defense-in-depth > unknown flag rejected (even if hub validator drifts loose) [0.14ms] +(pass) validateLocalPatch — defense-in-depth > permissionMode invalid enum rejected [0.13ms] +(pass) validateLocalPatch — defense-in-depth > dangerouslySkipPermissions non-boolean rejected [0.22ms] +(pass) validateLocalPatch — defense-in-depth > maxTurns out of range rejected [0.19ms] +(pass) validateLocalPatch — defense-in-depth > timeout invalid rejected [0.21ms] +(pass) validateLocalPatch — defense-in-depth > empty-string model rejected [0.17ms] +(pass) computeApplyMode — tier classifier > empty patch → restart_only (restart_node) [0.27ms] +(pass) computeApplyMode — tier classifier > model only → restart [0.18ms] (pass) computeApplyMode — tier classifier > permissionMode → restart [0.15ms] -(pass) computeApplyMode — tier classifier > dangerouslySkipPermissions → restart [0.15ms] -(pass) computeApplyMode — tier classifier > teammateMode no longer in allowlist → ignored by classifier (returns hot since no restart-required flag matches) [0.22ms] -(pass) computeApplyMode — tier classifier > timeout → restart [0.16ms] -(pass) computeApplyMode — tier classifier > maxTurns only → hot [0.18ms] -(pass) computeApplyMode — tier classifier > budget only → hot [0.19ms] -(pass) computeApplyMode — tier classifier > mixed (model + maxTurns) → restart (strictest wins) [0.17ms] -(pass) atomicWriteJson — temp + rename > creates file with JSON content + trailing newline [2.52ms] -(pass) atomicWriteJson — temp + rename > overwrites existing file atomically (no .tmp left behind) [2.34ms] -(pass) #472 private config permissions > atomic write is 0600 under umask 0 [2.32ms] -(pass) #472 private config permissions > atomic write is 0600 under umask 2 [2.65ms] -(pass) #472 private config permissions > atomic write is 0600 under umask 22 [2.07ms] -(pass) #472 private config permissions > atomic write is 0600 under umask 77 [2.04ms] -(pass) #472 private config permissions > repairs existing primary, backup, and parent before token read [0.66ms] -(pass) #472 private config permissions > custom --config parent is never chmodded [0.41ms] -(pass) #472 private config permissions > atomic custom --config write preserves parent mode [2.19ms] -(pass) #472 private config permissions > backup atomically replaces a legacy broad .prev [2.26ms] -(pass) backupConfigPrev — pre-write snapshot > copies existing config to .prev [2.17ms] -(pass) backupConfigPrev — pre-write snapshot > returns backedUp=false when no config exists yet (first-write case) [0.24ms] -(pass) backupConfigPrev — pre-write snapshot > overwrites previous .prev (single-generation rotation) [3.97ms] -(pass) loadConfigWithSelfHeal — boot recovery > primary parses → returns primary [0.41ms] +(pass) computeApplyMode — tier classifier > dangerouslySkipPermissions → restart [0.23ms] +(pass) computeApplyMode — tier classifier > teammateMode no longer in allowlist → ignored by classifier (returns hot since no restart-required flag matches) [0.20ms] +(pass) computeApplyMode — tier classifier > timeout → restart [0.20ms] +(pass) computeApplyMode — tier classifier > maxTurns only → hot [0.20ms] +(pass) computeApplyMode — tier classifier > budget only → hot [0.16ms] +(pass) computeApplyMode — tier classifier > mixed (model + maxTurns) → restart (strictest wins) [0.18ms] +(pass) atomicWriteJson — temp + rename > creates file with JSON content + trailing newline [2.18ms] +(pass) atomicWriteJson — temp + rename > overwrites existing file atomically (no .tmp left behind) [3.14ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 0 [2.91ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 2 [2.83ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 22 [2.45ms] +(pass) #472 private config permissions > atomic write is 0600 under umask 77 [2.58ms] +(pass) #472 private config permissions > repairs existing primary, backup, and parent before token read [0.75ms] +(pass) #472 private config permissions > custom --config parent is never chmodded [0.42ms] +(pass) #472 private config permissions > atomic custom --config write preserves parent mode [1.98ms] +(pass) #472 private config permissions > backup atomically replaces a legacy broad .prev [2.25ms] +(pass) backupConfigPrev — pre-write snapshot > copies existing config to .prev [2.13ms] +(pass) backupConfigPrev — pre-write snapshot > returns backedUp=false when no config exists yet (first-write case) [0.29ms] +(pass) backupConfigPrev — pre-write snapshot > overwrites previous .prev (single-generation rotation) [4.13ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary parses → returns primary [0.51ms] (pass) loadConfigWithSelfHeal — boot recovery > primary corrupted + .prev valid → restores .prev + reports source=prev [2.41ms] -(pass) loadConfigWithSelfHeal — boot recovery > primary corrupted + no .prev → throws (truly bricked, caller surfaces) [0.50ms] -(pass) loadConfigWithSelfHeal — boot recovery > primary AND .prev corrupted → throws with both errors [0.53ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary corrupted + no .prev → throws (truly bricked, caller surfaces) [0.42ms] +(pass) loadConfigWithSelfHeal — boot recovery > primary AND .prev corrupted → throws with both errors [0.42ms] (pass) loadConfigWithSelfHeal — boot recovery > primary missing entirely → throws (caller will skip / first-boot path) [0.26ms] -(pass) mergePatch — patch + existing → new config (no mutation) > model replace [0.34ms] -(pass) mergePatch — patch + existing → new config (no mutation) > flags merge (does not replace whole flags obj) [0.25ms] -(pass) mergePatch — patch + existing → new config (no mutation) > empty existing + patch → patch only [0.16ms] +(pass) mergePatch — patch + existing → new config (no mutation) > model replace [0.37ms] +(pass) mergePatch — patch + existing → new config (no mutation) > flags merge (does not replace whole flags obj) [0.23ms] +(pass) mergePatch — patch + existing → new config (no mutation) > empty existing + patch → patch only [0.19ms] (pass) mergePatch — patch + existing → new config (no mutation) > empty patch → existing unchanged (deep clone) [0.21ms] -(pass) buildConfigSnapshot — pure helper contract (#290 final, drain-omit guard) > buildConfigSnapshot returns a valid snapshot regardless of caller drain state (pure) [0.57ms] -(pass) validateLocalPatch — teammateMode dropped (#290 review) > teammateMode rejected (was: allowed boolean; now: not-in-allowlist) [0.25ms] -(pass) computeApplyMode — teammateMode is no longer restart-required (#290 review) > teammateMode-only patch → hot (no longer in RESTART_REQUIRED_FLAGS) [0.16ms] -(pass) buildConfigSnapshot — masked report (no secrets) > includes model + ALLOWED_FLAGS only [0.23ms] -(pass) buildConfigSnapshot — masked report (no secrets) > missing model → null (not undefined, dashboard renders explicitly) [0.15ms] -(pass) buildConfigSnapshot — masked report (no secrets) > config_update_capable=false signals bare node (no supervisor wrapper) [0.15ms] -(pass) buildConfigSnapshot — role (PR1 #338) > role: host_supervisor passes through (string) [0.15ms] -(pass) buildConfigSnapshot — role (PR1 #338) > role: member passes through [0.15ms] -(pass) buildConfigSnapshot — role (PR1 #338) > role: missing → null (not undefined; dashboard distinguishes) [0.14ms] -(pass) buildConfigSnapshot — role (PR1 #338) > role: non-string narrowed to null (typeof guard) [0.21ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > nests runtimes_supported + allowed_secret_keys + max_concurrent_children [0.25ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > matches hub canonical path snap.daemon_capabilities.* — NOT at top level [0.21ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > partial declare: only runtimes_supported emits, others omitted [0.17ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > missing → daemon_capabilities undefined (regular non-daemon node) [0.14ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: non-array runtimes_supported dropped silently [0.17ms] +(pass) buildConfigSnapshot — pure helper contract (#290 final, drain-omit guard) > buildConfigSnapshot returns a valid snapshot regardless of caller drain state (pure) [0.50ms] +(pass) validateLocalPatch — teammateMode dropped (#290 review) > teammateMode rejected (was: allowed boolean; now: not-in-allowlist) [0.23ms] +(pass) computeApplyMode — teammateMode is no longer restart-required (#290 review) > teammateMode-only patch → hot (no longer in RESTART_REQUIRED_FLAGS) [0.20ms] +(pass) buildConfigSnapshot — masked report (no secrets) > includes model + ALLOWED_FLAGS only [0.29ms] +(pass) buildConfigSnapshot — masked report (no secrets) > missing model → null (not undefined, dashboard renders explicitly) [0.23ms] +(pass) buildConfigSnapshot — masked report (no secrets) > config_update_capable=false signals bare node (no supervisor wrapper) [0.20ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: host_supervisor passes through (string) [0.18ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: member passes through [0.18ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: missing → null (not undefined; dashboard distinguishes) [0.15ms] +(pass) buildConfigSnapshot — role (PR1 #338) > role: non-string narrowed to null (typeof guard) [0.24ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > nests runtimes_supported + allowed_secret_keys + max_concurrent_children [0.24ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > matches hub canonical path snap.daemon_capabilities.* — NOT at top level [0.26ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > partial declare: only runtimes_supported emits, others omitted [0.19ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > missing → daemon_capabilities undefined (regular non-daemon node) [0.20ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: non-array runtimes_supported dropped silently [0.18ms] (pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: array with non-string element dropped silently [0.18ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: max_concurrent_children non-finite or non-positive dropped [0.22ms] -(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > partial valid + partial invalid: only valid fields included [0.20ms] -(pass) channels — validateLocalPatch > valid keys pass [0.28ms] -(pass) channels — validateLocalPatch > commhub rejected — not a fork target (cli.ts:673 UNSUPPORTED_CHANNEL guard) [0.23ms] -(pass) channels — validateLocalPatch > unknown channel key rejected (defense-in-depth vs hub drift) [0.23ms] -(pass) channels — validateLocalPatch > non-array rejected [0.26ms] -(pass) channels — validateLocalPatch > non-string element rejected [0.19ms] -(pass) channels — validateLocalPatch > more than 16 entries rejected [0.23ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > typeof narrow: max_concurrent_children non-finite or non-positive dropped [0.26ms] +(pass) buildConfigSnapshot — daemon_capabilities (PR3 #338 nit ①) > partial valid + partial invalid: only valid fields included [0.24ms] +(pass) channels — validateLocalPatch > valid keys pass [0.21ms] +(pass) channels — validateLocalPatch > commhub rejected — not a fork target (cli.ts:673 UNSUPPORTED_CHANNEL guard) [0.16ms] +(pass) channels — validateLocalPatch > unknown channel key rejected (defense-in-depth vs hub drift) [0.21ms] +(pass) channels — validateLocalPatch > non-array rejected [0.17ms] +(pass) channels — validateLocalPatch > non-string element rejected [0.14ms] +(pass) channels — validateLocalPatch > more than 16 entries rejected [0.24ms] (pass) channels — computeApplyMode > channels-present patch is restart-tier [0.18ms] -(pass) channels — computeApplyMode > channels: [] still a state change → restart [0.18ms] -(pass) channels — computeApplyMode > channels + hot flag upgrades to restart [0.17ms] +(pass) channels — computeApplyMode > channels: [] still a state change → restart [0.13ms] +(pass) channels — computeApplyMode > channels + hot flag upgrades to restart [0.15ms] (pass) channels — computeApplyMode > model + channels → restart [0.15ms] -(pass) channels — computeApplyMode > empty patch → restart_only [0.16ms] -(pass) channels — mergePatch replaces, does not merge > channels absent in patch: existing.channels preserved [0.22ms] -(pass) channels — mergePatch replaces, does not merge > channels present: existing.channels REPLACED wholesale [0.35ms] -(pass) channels — mergePatch replaces, does not merge > channels: [] disables all editable channels [0.24ms] -(pass) channels — mergePatch replaces, does not merge > first-write case (existing has no channels key) [0.16ms] -(pass) channels — mergePatch replaces, does not merge > defensive clone — patch mutation does not leak into merged [0.26ms] -(pass) mergePatch — path-qualified specs preserved > bare-type patch preserves existing telegram:/abs/path [0.21ms] -(pass) mergePatch — path-qualified specs preserved > bare-type patch keeps both when both were path-qualified [0.21ms] +(pass) channels — computeApplyMode > empty patch → restart_only [0.13ms] +(pass) channels — mergePatch replaces, does not merge > channels absent in patch: existing.channels preserved [0.20ms] +(pass) channels — mergePatch replaces, does not merge > channels present: existing.channels REPLACED wholesale [0.29ms] +(pass) channels — mergePatch replaces, does not merge > channels: [] disables all editable channels [0.22ms] +(pass) channels — mergePatch replaces, does not merge > first-write case (existing has no channels key) [0.19ms] +(pass) channels — mergePatch replaces, does not merge > defensive clone — patch mutation does not leak into merged [0.22ms] +(pass) mergePatch — path-qualified specs preserved > bare-type patch preserves existing telegram:/abs/path [0.20ms] +(pass) mergePatch — path-qualified specs preserved > bare-type patch keeps both when both were path-qualified [0.20ms] (pass) mergePatch — path-qualified specs preserved > bare-type patch adds new bare key when existing had no matching spec [0.18ms] -(pass) mergePatch — path-qualified specs preserved > disable-all still works — empty patch wipes even path-qualified specs [0.19ms] -(pass) mergePatch — path-qualified specs preserved > first-write no existing channels: bare types stay bare [0.16ms] -(pass) buildConfigSnapshot — always emits channels for content-match finalize > empty config emits channels=[] [0.21ms] -(pass) buildConfigSnapshot — always emits channels for content-match finalize > bare-type list emitted verbatim + sorted [0.21ms] +(pass) mergePatch — path-qualified specs preserved > disable-all still works — empty patch wipes even path-qualified specs [0.18ms] +(pass) mergePatch — path-qualified specs preserved > first-write no existing channels: bare types stay bare [0.19ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > empty config emits channels=[] [0.19ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > bare-type list emitted verbatim + sorted [0.25ms] (pass) buildConfigSnapshot — always emits channels for content-match finalize > path-qualified specs collapse to bare type [0.17ms] -(pass) buildConfigSnapshot — always emits channels for content-match finalize > dupes deduped, unparseable dropped [0.18ms] -(pass) buildConfigSnapshot — always emits channels for content-match finalize > non-array channels field yields [] [0.15ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > dupes deduped, unparseable dropped [0.19ms] +(pass) buildConfigSnapshot — always emits channels for content-match finalize > non-array channels field yields [] [0.20ms] src/runtime/codex-dep-loader.test.ts: -(pass) loadCodexSdk > returns the imported module without installing when already present [0.67ms] -(pass) loadCodexSdk > auto-installs and retries when the first import fails [0.61ms] -(pass) loadCodexSdk > throws a friendly multi-line error when install fails — includes pasteable npm command + module path + both root causes [0.77ms] -(pass) loadCodexSdk > install succeeds but post-install import still fails → terminal error names the install-then-resolve mismatch [0.45ms] -(pass) loadCodexSdk > module dir with shell metacharacters is single-quoted in the recovery hint [0.42ms] +(pass) loadCodexSdk > returns the imported module without installing when already present [0.46ms] +(pass) loadCodexSdk > auto-installs and retries when the first import fails [0.39ms] +(pass) loadCodexSdk > throws a friendly multi-line error when install fails — includes pasteable npm command + module path + both root causes [0.53ms] +(pass) loadCodexSdk > install succeeds but post-install import still fails → terminal error names the install-then-resolve mismatch [0.38ms] +(pass) loadCodexSdk > module dir with shell metacharacters is single-quoted in the recovery hint [0.31ms] src/runtime/create-node-daemon-private-wiring.test.ts: -(pass) #633 daemon secret writers all use the private atomic choke point [0.36ms] +(pass) #633 daemon secret writers all use the private atomic choke point [0.27ms] src/runtime/reply-routing.test.ts: -(pass) codex-app-server reply routing > dashboard/user sender that is not a session falls back to send_reply [0.61ms] -(pass) codex-app-server reply routing > agent sender with a real session keeps send_task wake path [0.19ms] -(pass) codex-app-server reply routing > missing task id does not create an unparented reply task [0.11ms] -(pass) codex-app-server reply routing > roster load failure fails closed to send_reply [0.28ms] +(pass) codex-app-server reply routing > dashboard/user sender that is not a session falls back to send_reply [0.82ms] +(pass) codex-app-server reply routing > agent sender with a real session keeps send_task wake path [0.22ms] +(pass) codex-app-server reply routing > missing task id does not create an unparented reply task [0.16ms] +(pass) codex-app-server reply routing > roster load failure fails closed to send_reply [0.24ms] (pass) codex-app-server reply routing > short ttl cache avoids repeated roster fetches and refreshes after expiry [0.42ms] -(pass) codex-app-server reply routing > failed send_task replies keep the peer-visible failure marker and high priority [0.07ms] +(pass) codex-app-server reply routing > failed send_task replies keep the peer-visible failure marker and high priority [0.12ms] src/runtime/grok-build-cli-home.test.ts: -(pass) prepareGrokCliHome > derives an opaque path segment and rejects dot identities [0.38ms] -(pass) prepareGrokCliHome > accepts only the pinned Grok regular-file copy of source agent_id [5.88ms] -(pass) prepareGrokCliHome > isolates config/trust, preserves a shared auth path, and creates stable sandbox profiles [2.92ms] -(pass) prepareGrokCliHome > refuses broad-mode or symlinked source auth without repairing it [1.44ms] -(pass) prepareGrokCliHome > repairs an existing Grok session store to owner-only modes [2.48ms] -(pass) prepareGrokCliHome > does not follow a symlink while repairing an existing session store [1.20ms] -(pass) prepareGrokCliHome > keeps the post-stop cleanup policy exact and reviewable [0.11ms] -(pass) prepareGrokCliHome > removes exact empty read-only project placeholders before resume without admitting executable sources [4.33ms] -(pass) prepareGrokCliHome > validates every exact project placeholder before unlinking any sibling [1.34ms] -(pass) prepareGrokCliHome > does not let a fatal project counterexample starve independent state containment [2.01ms] -(pass) prepareGrokCliHome > preserves nonempty, linked, wrong-mode, and wrong-type project counterexamples [4.47ms] -(pass) prepareGrokCliHome > preserves real project extension directories and still rejects executable contents on resume [1.83ms] -(pass) prepareGrokCliHome > removes only exact transient state and hardens retained post-stop state [5.10ms] -(pass) prepareGrokCliHome > hardens only the native lock derived from the exact leader socket [1.05ms] -(pass) prepareGrokCliHome > retains a non-empty leader log and rejects post-stop link attacks [2.16ms] -(pass) prepareGrokCliHome > refuses a non-empty exact sandbox placeholder [1.19ms] -(pass) prepareGrokCliHome > reclaims an empty mode-000 sandbox marker under a foreign pid without aborting [1.57ms] -(pass) prepareGrokCliHome > keeps a non-empty foreign sandbox marker unreadable so it fails closed [1.86ms] -(pass) prepareGrokCliHome > validates exact TUI process ids before mutation and refuses a placeholder symlink [1.47ms] -(pass) prepareGrokCliHome > enables the single TUI leader only for explicit copresence mode [12.29ms] -(pass) prepareGrokCliHome > admits only canonical owner-held commhub MCP artifacts [3.21ms] -(pass) prepareGrokCliHome > rejects a shared auth path covered by a required sandbox deny before state mutation [0.70ms] -(pass) prepareGrokCliHome > refuses to claim sandbox isolation when no deny target exists [0.67ms] -(pass) prepareGrokCliHome > rejects a source GROK_HOME reached through an ancestor symlink before state mutation [0.66ms] -(pass) prepareGrokCliHome > removes runtime-owned native hooks before every turn [1.13ms] -(pass) prepareGrokCliHome > unlinks a runtime-owned hook symlink without touching its external target [1.17ms] -(pass) prepareGrokCliHome > fails closed when a project native hook path exists [0.69ms] -(pass) prepareGrokCliHome > trusts only the exact canonical nested cwd and atomically replaces stale grants [3.36ms] -(pass) prepareGrokCliHome > rejects broad or symlinked folder-trust targets before writing trust state [1.23ms] -(pass) prepareGrokCliHome > refuses a planted trust-store symlink and leaves its target untouched [1.66ms] -(pass) prepareGrokCliHome > rejects every project executable source before granting folder trust [9.76ms] -(pass) prepareGrokCliHome > does not impose the shared-folder strict policy on legacy headless mode [2.28ms] -(pass) prepareGrokCliHome > rejects repo-root hooks from a nested cwd and dangling hook links [1.07ms] -(pass) prepareGrokCliHome > rejects a symlinked project .grok directory [0.75ms] -(pass) prepareGrokCliHome > rejects symlinked isolated homes and generated state without changing targets [1.49ms] -(pass) prepareGrokCliHome > rejects a state-home path escape before chmod, removal, or writes [0.84ms] -(pass) prepareGrokCliHome > requires a valid zero-hook inspect response [0.51ms] -(pass) prepareGrokCliHome > flocks the canonical project inode across symlink aliases and releases cleanly [118.03ms] -(pass) prepareGrokCliHome > gives the real flock holder only the exact helper environment [55.43ms] +(pass) prepareGrokCliHome > derives an opaque path segment and rejects dot identities [0.36ms] +(pass) prepareGrokCliHome > accepts only the pinned Grok regular-file copy of source agent_id [6.09ms] +(pass) prepareGrokCliHome > isolates config/trust, preserves a shared auth path, and creates stable sandbox profiles [2.69ms] +(pass) prepareGrokCliHome > refuses broad-mode or symlinked source auth without repairing it [1.17ms] +(pass) prepareGrokCliHome > repairs an existing Grok session store to owner-only modes [1.79ms] +(pass) prepareGrokCliHome > does not follow a symlink while repairing an existing session store [0.99ms] +(pass) prepareGrokCliHome > keeps the post-stop cleanup policy exact and reviewable [0.13ms] +(pass) prepareGrokCliHome > removes exact empty read-only project placeholders before resume without admitting executable sources [4.41ms] +(pass) prepareGrokCliHome > validates every exact project placeholder before unlinking any sibling [1.40ms] +(pass) prepareGrokCliHome > does not let a fatal project counterexample starve independent state containment [2.00ms] +(pass) prepareGrokCliHome > preserves nonempty, linked, wrong-mode, and wrong-type project counterexamples [4.52ms] +(pass) prepareGrokCliHome > preserves real project extension directories and still rejects executable contents on resume [2.05ms] +(pass) prepareGrokCliHome > removes only exact transient state and hardens retained post-stop state [5.52ms] +(pass) prepareGrokCliHome > hardens only the native lock derived from the exact leader socket [1.16ms] +(pass) prepareGrokCliHome > retains a non-empty leader log and rejects post-stop link attacks [2.35ms] +(pass) prepareGrokCliHome > refuses a non-empty exact sandbox placeholder [1.03ms] +(pass) prepareGrokCliHome > reclaims an empty mode-000 sandbox marker under a foreign pid without aborting [1.53ms] +(pass) prepareGrokCliHome > keeps a non-empty foreign sandbox marker unreadable so it fails closed [1.67ms] +(pass) prepareGrokCliHome > validates exact TUI process ids before mutation and refuses a placeholder symlink [1.45ms] +(pass) prepareGrokCliHome > enables the single TUI leader only for explicit copresence mode [13.63ms] +(pass) prepareGrokCliHome > admits only canonical owner-held commhub MCP artifacts [3.40ms] +(pass) prepareGrokCliHome > rejects a shared auth path covered by a required sandbox deny before state mutation [0.64ms] +(pass) prepareGrokCliHome > refuses to claim sandbox isolation when no deny target exists [1.07ms] +(pass) prepareGrokCliHome > rejects a source GROK_HOME reached through an ancestor symlink before state mutation [1.53ms] +(pass) prepareGrokCliHome > removes runtime-owned native hooks before every turn [1.44ms] +(pass) prepareGrokCliHome > unlinks a runtime-owned hook symlink without touching its external target [1.18ms] +(pass) prepareGrokCliHome > fails closed when a project native hook path exists [0.73ms] +(pass) prepareGrokCliHome > trusts only the exact canonical nested cwd and atomically replaces stale grants [3.57ms] +(pass) prepareGrokCliHome > rejects broad or symlinked folder-trust targets before writing trust state [1.21ms] +(pass) prepareGrokCliHome > refuses a planted trust-store symlink and leaves its target untouched [1.77ms] +(pass) prepareGrokCliHome > rejects every project executable source before granting folder trust [10.70ms] +(pass) prepareGrokCliHome > does not impose the shared-folder strict policy on legacy headless mode [2.04ms] +(pass) prepareGrokCliHome > rejects repo-root hooks from a nested cwd and dangling hook links [1.00ms] +(pass) prepareGrokCliHome > rejects a symlinked project .grok directory [0.72ms] +(pass) prepareGrokCliHome > rejects symlinked isolated homes and generated state without changing targets [1.68ms] +(pass) prepareGrokCliHome > rejects a state-home path escape before chmod, removal, or writes [0.71ms] +(pass) prepareGrokCliHome > requires a valid zero-hook inspect response [0.47ms] +(pass) prepareGrokCliHome > flocks the canonical project inode across symlink aliases and releases cleanly [126.15ms] +(pass) prepareGrokCliHome > gives the real flock holder only the exact helper environment [64.34ms] src/goals/format.test.ts: -(pass) formatSelfLoopsBlock — empty / omit semantics > no goals + omitWhenEmpty=true (default) → empty string [0.31ms] -(pass) formatSelfLoopsBlock — empty / omit semantics > no goals + omitWhenEmpty=false → explicit '无活跃循环' block [0.12ms] +(pass) formatSelfLoopsBlock — empty / omit semantics > no goals + omitWhenEmpty=true (default) → empty string [0.22ms] +(pass) formatSelfLoopsBlock — empty / omit semantics > no goals + omitWhenEmpty=false → explicit '无活跃循环' block [0.08ms] (pass) formatSelfLoopsBlock — empty / omit semantics > only terminal goals (cancelled/complete/failed) → empty (same as no goals) [0.24ms] -(pass) formatSelfLoopsBlock — content shape > single active goal: header + id8 + cadence + text [0.36ms] -(pass) formatSelfLoopsBlock — content shape > paused goals shown with status='paused' [0.13ms] -(pass) formatSelfLoopsBlock — content shape > mix active + paused + terminal → only active+paused appear [0.12ms] +(pass) formatSelfLoopsBlock — content shape > single active goal: header + id8 + cadence + text [0.35ms] +(pass) formatSelfLoopsBlock — content shape > paused goals shown with status='paused' [0.09ms] +(pass) formatSelfLoopsBlock — content shape > mix active + paused + terminal → only active+paused appear [0.14ms] (pass) formatSelfLoopsBlock — cron-lite cadence rendering > time_of_day cadence: '每天 09:00' [0.10ms] -(pass) formatSelfLoopsBlock — cron-lite cadence rendering > weekday cadence: 'mon/wed/fri 18:30' [0.11ms] -(pass) formatSelfLoopsBlock — cron-lite cadence rendering > new-format interval cadence renders same as legacy interval_ms [0.07ms] -(pass) formatSelfLoopsBlock — cap + truncation > more than maxGoals → truncates with '...' summary [0.29ms] +(pass) formatSelfLoopsBlock — cron-lite cadence rendering > weekday cadence: 'mon/wed/fri 18:30' [0.10ms] +(pass) formatSelfLoopsBlock — cron-lite cadence rendering > new-format interval cadence renders same as legacy interval_ms [0.25ms] +(pass) formatSelfLoopsBlock — cap + truncation > more than maxGoals → truncates with '...' summary [0.38ms] (pass) formatSelfLoopsBlock — cap + truncation > text is one-line truncated at 100 chars [0.12ms] -(pass) formatSelfLoopsBlock — cap + truncation > multi-line text is rendered as single line [0.13ms] -(pass) formatSelfLoopsBlock — relative time rendering > next_wake_at far in the future → ISO-shortened [0.10ms] -(pass) formatSelfLoopsBlock — relative time rendering > next_wake_at in past → '已到期' [0.08ms] -(pass) formatSelfLoopsBlock — relative time rendering > malformed ISO doesn't crash, falls back to raw [0.09ms] +(pass) formatSelfLoopsBlock — cap + truncation > multi-line text is rendered as single line [0.12ms] +(pass) formatSelfLoopsBlock — relative time rendering > next_wake_at far in the future → ISO-shortened [0.11ms] +(pass) formatSelfLoopsBlock — relative time rendering > next_wake_at in past → '已到期' [0.10ms] +(pass) formatSelfLoopsBlock — relative time rendering > malformed ISO doesn't crash, falls back to raw [0.07ms] src/goals/routing.test.ts: -(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > authenticated Dashboard /goal and /loop pass through for every agent-node runtime [0.21ms] -(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > authenticated Dashboard /agoal and /aloop always select the ANet scheduler [0.09ms] -(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > non-Dashboard traffic retains /goal and /loop during the compatibility window [0.15ms] -(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > near matches and slash text away from the start never select the scheduler [0.17ms] -(pass) appendLegacyScheduledGoalNotice > non-Dashboard /goal and /loop replies carry a deterministic migration notice [0.10ms] -(pass) appendLegacyScheduledGoalNotice > new namespaced commands, Dashboard pass-through, and near matches are not warned [0.07ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > authenticated Dashboard /goal and /loop pass through for every agent-node runtime [0.23ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > authenticated Dashboard /agoal and /aloop always select the ANet scheduler [0.10ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > non-Dashboard traffic retains /goal and /loop during the compatibility window [0.24ms] +(pass) shouldCreateScheduledGoal — Dashboard native slash pass-through > near matches and slash text away from the start never select the scheduler [0.19ms] +(pass) appendLegacyScheduledGoalNotice > non-Dashboard /goal and /loop replies carry a deterministic migration notice [0.11ms] +(pass) appendLegacyScheduledGoalNotice > new namespaced commands, Dashboard pass-through, and near matches are not warned [0.06ms] (pass) appendLegacyScheduledGoalNotice > the migration notice is first so the outer reply cap cannot truncate it [0.10ms] -(pass) Dashboard native slash migration notice > interval-shaped /goal and /loop replies explain that ANet scheduling moved to /aloop [0.63ms] -(pass) Dashboard native slash migration notice > ordinary native commands, namespaced commands, and non-Dashboard paths are untouched [0.07ms] -(pass) Dashboard native slash migration notice > the notice survives low-value filtering and the outer reply cap [0.22ms] -(pass) Dashboard native slash migration notice > failed native replies still surface the migration notice and the failure [0.10ms] -(pass) reply filtering uses authenticated message provenance > a short presence reply to an authenticated Dashboard human task is delivered [0.11ms] -(pass) reply filtering uses authenticated message provenance > the same low-value class remains filtered for agent-to-agent tasks [0.06ms] -(pass) reply filtering uses authenticated message provenance > a provenance flag cannot bypass filtering for a non-task message type [0.07ms] +(pass) Dashboard native slash migration notice > interval-shaped /goal and /loop replies explain that ANet scheduling moved to /aloop [0.92ms] +(pass) Dashboard native slash migration notice > ordinary native commands, namespaced commands, and non-Dashboard paths are untouched [0.09ms] +(pass) Dashboard native slash migration notice > the notice survives low-value filtering and the outer reply cap [0.25ms] +(pass) Dashboard native slash migration notice > failed native replies still surface the migration notice and the failure [0.11ms] +(pass) reply filtering uses authenticated message provenance > a short presence reply to an authenticated Dashboard human task is delivered [0.13ms] +(pass) reply filtering uses authenticated message provenance > the same low-value class remains filtered for agent-to-agent tasks [0.07ms] +(pass) reply filtering uses authenticated message provenance > a provenance flag cannot bypass filtering for a non-task message type [0.08ms] src/goals/loops-http-server.test.ts: -(pass) localhost binding (通信龙 hard constraint #1+#2) > server bound to 127.0.0.1, not 0.0.0.0 [7.93ms] -(pass) localhost binding (通信龙 hard constraint #1+#2) > port is reachable [8.58ms] -(pass) localhost binding (通信龙 hard constraint #1+#2) > random port (different runs get different ports) [6.48ms] -(pass) bearer auth no-bypass (通信龙 hard constraint #4) > missing Authorization header → 401 [5.97ms] -(pass) bearer auth no-bypass (通信龙 hard constraint #4) > wrong token → 401 [5.76ms] -(pass) bearer auth no-bypass (通信龙 hard constraint #4) > non-Bearer scheme → 401 [8.20ms] -(pass) bearer auth no-bypass (通信龙 hard constraint #4) > correct Bearer → 200 [6.02ms] -(pass) bearer auth no-bypass (通信龙 hard constraint #4) > path other than /mcp → 404 [5.02ms] -(pass) MCP protocol — initialize / tools/list / tools/call > initialize returns serverInfo + tools capability [7.65ms] -(pass) MCP protocol — initialize / tools/list / tools/call > tools/list returns all 6 self-loop tools [6.76ms] -(pass) MCP protocol — initialize / tools/list / tools/call > tools/list each tool has description + inputSchema [5.29ms] -(pass) MCP protocol — initialize / tools/list / tools/call > unknown method → JSON-RPC -32601 [9.78ms] -(pass) MCP protocol — initialize / tools/list / tools/call > malformed JSON → -32700 [9.03ms] -(pass) tools/call — handler dispatch into parent ctx > list_my_loops on empty store [6.01ms] -(pass) tools/call — handler dispatch into parent ctx > create_my_loop with interval string writes to parent goalStore [7.60ms] -(pass) tools/call — handler dispatch into parent ctx > unknown tool name → JSON-RPC -32601 [5.36ms] -(pass) safety防线 cross-HTTP boundary (M2 verification line) > batch-cancel via HTTP triggers confirm-back on 4th call [13.55ms] -(pass) safety防线 cross-HTTP boundary (M2 verification line) > cooldown via HTTP — edit within 30s of upsert rejected [11.07ms] -(pass) safety防线 cross-HTTP boundary (M2 verification line) > max-active-goals cap honored across HTTP [11.27ms] -(pass) safety防线 cross-HTTP boundary (M2 verification line) > preflight invalid timezone rejected via HTTP (M1 #302 round-2 still works) [13.26ms] -(pass) custom token override (for tests) > explicit token honored [10.51ms] -(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp (exact) accepted → 200 [7.11ms] -(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp?foo=bar (with query string) accepted → 200 [4.84ms] -(pass) path routing — exact pathname (通信牛 hardening nit) > /mcpXYZ (suffix) rejected → 404 (not auth-checked) [7.26ms] -(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp/ (trailing slash) rejected → 404 [6.28ms] -(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp-leak (dash suffix) rejected → 404 [4.51ms] -(pass) path routing — exact pathname (通信牛 hardening nit) > / (root) rejected → 404 [8.69ms] +(pass) localhost binding (通信龙 hard constraint #1+#2) > server bound to 127.0.0.1, not 0.0.0.0 [9.16ms] +(pass) localhost binding (通信龙 hard constraint #1+#2) > port is reachable [10.48ms] +(pass) localhost binding (通信龙 hard constraint #1+#2) > random port (different runs get different ports) [5.51ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > missing Authorization header → 401 [7.48ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > wrong token → 401 [7.24ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > non-Bearer scheme → 401 [8.80ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > correct Bearer → 200 [6.26ms] +(pass) bearer auth no-bypass (通信龙 hard constraint #4) > path other than /mcp → 404 [6.34ms] +(pass) MCP protocol — initialize / tools/list / tools/call > initialize returns serverInfo + tools capability [9.29ms] +(pass) MCP protocol — initialize / tools/list / tools/call > tools/list returns all 6 self-loop tools [6.78ms] +(pass) MCP protocol — initialize / tools/list / tools/call > tools/list each tool has description + inputSchema [7.52ms] +(pass) MCP protocol — initialize / tools/list / tools/call > unknown method → JSON-RPC -32601 [6.37ms] +(pass) MCP protocol — initialize / tools/list / tools/call > malformed JSON → -32700 [5.78ms] +(pass) tools/call — handler dispatch into parent ctx > list_my_loops on empty store [8.81ms] +(pass) tools/call — handler dispatch into parent ctx > create_my_loop with interval string writes to parent goalStore [8.67ms] +(pass) tools/call — handler dispatch into parent ctx > unknown tool name → JSON-RPC -32601 [6.92ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > batch-cancel via HTTP triggers confirm-back on 4th call [21.30ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > cooldown via HTTP — edit within 30s of upsert rejected [8.65ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > max-active-goals cap honored across HTTP [13.69ms] +(pass) safety防线 cross-HTTP boundary (M2 verification line) > preflight invalid timezone rejected via HTTP (M1 #302 round-2 still works) [9.67ms] +(pass) custom token override (for tests) > explicit token honored [14.06ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp (exact) accepted → 200 [9.34ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp?foo=bar (with query string) accepted → 200 [7.98ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcpXYZ (suffix) rejected → 404 (not auth-checked) [7.28ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp/ (trailing slash) rejected → 404 [7.01ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > /mcp-leak (dash suffix) rejected → 404 [6.02ms] +(pass) path routing — exact pathname (通信牛 hardening nit) > / (root) rejected → 404 [8.28ms] src/goals/failure-counter.test.ts: -(pass) resolveMaxConsecutiveFailures > default 5 when env unset [0.08ms] -(pass) resolveMaxConsecutiveFailures > env override honored [0.03ms] -(pass) resolveMaxConsecutiveFailures > invalid env falls back to default [0.04ms] -(pass) getFailureCount > legacy undefined → 0 [0.10ms] -(pass) getFailureCount > explicit 0 → 0 [0.04ms] -(pass) getFailureCount > explicit N → N [0.05ms] -(pass) bumpFailure > first failure: undefined → 1, shouldPause=false at default threshold [0.10ms] -(pass) bumpFailure > 4 → 5 at default threshold: shouldPause=true [0.08ms] -(pass) bumpFailure > 3 → 4 at threshold 5: shouldPause=false (below threshold) [0.06ms] +(pass) resolveMaxConsecutiveFailures > default 5 when env unset [0.17ms] +(pass) resolveMaxConsecutiveFailures > env override honored [0.07ms] +(pass) resolveMaxConsecutiveFailures > invalid env falls back to default [0.06ms] +(pass) getFailureCount > legacy undefined → 0 [0.14ms] +(pass) getFailureCount > explicit 0 → 0 [0.07ms] +(pass) getFailureCount > explicit N → N [0.44ms] +(pass) bumpFailure > first failure: undefined → 1, shouldPause=false at default threshold [0.21ms] +(pass) bumpFailure > 4 → 5 at default threshold: shouldPause=true [0.06ms] +(pass) bumpFailure > 3 → 4 at threshold 5: shouldPause=false (below threshold) [0.05ms] (pass) bumpFailure > custom threshold — 2 → 3 at threshold 3: shouldPause=true [0.04ms] -(pass) bumpFailure > beyond threshold: count continues to increment but shouldPause stays true [0.05ms] -(pass) resetFailure > legacy undefined stays undefined (no unnecessary write) [0.09ms] -(pass) resetFailure > 0 stays 0 (no unnecessary write) [0.05ms] -(pass) resetFailure > N > 0 → 0 [0.05ms] +(pass) bumpFailure > beyond threshold: count continues to increment but shouldPause stays true [0.04ms] +(pass) resetFailure > legacy undefined stays undefined (no unnecessary write) [0.08ms] +(pass) resetFailure > 0 stays 0 (no unnecessary write) [0.03ms] +(pass) resetFailure > N > 0 → 0 [0.04ms] (pass) resetFailure > threshold value → 0 [0.04ms] -(pass) applyAutoPause > status flipped to paused + counter preserved for observability [0.14ms] -(pass) applyAutoPause > progress_log entry recorded with count + reason [0.07ms] -(pass) applyAutoPause > long reason truncated to 300 chars in summary [0.11ms] -(pass) integration: full cycle > 5 bumps → pause → unpause reset → 5 more bumps → pause again [0.14ms] +(pass) applyAutoPause > status flipped to paused + counter preserved for observability [0.12ms] +(pass) applyAutoPause > progress_log entry recorded with count + reason [0.08ms] +(pass) applyAutoPause > long reason truncated to 300 chars in summary [0.10ms] +(pass) integration: full cycle > 5 bumps → pause → unpause reset → 5 more bumps → pause again [0.15ms] src/goals/parser.test.ts: -(pass) parseGoalCommand — English intervals > `5 min` form [0.12ms] -(pass) parseGoalCommand — English intervals > `5min` joined form [0.04ms] -(pass) parseGoalCommand — English intervals > `5 minutes` long form (plural wins over `min`) [0.03ms] -(pass) parseGoalCommand — English intervals > `1 hour` [0.05ms] -(pass) parseGoalCommand — English intervals > `hourly` keyword [0.08ms] -(pass) parseGoalCommand — English intervals > `daily` [0.05ms] -(pass) parseGoalCommand — English intervals > `1 day` [0.15ms] -(pass) parseGoalCommand — English intervals > `/goal` prefix is optional [0.04ms] -(pass) parseGoalCommand — English intervals > `/loop` alias [0.07ms] -(pass) parseGoalCommand — English intervals > `/aloop` strips the namespaced canonical prefix [0.09ms] -(pass) parseGoalCommand — English intervals > `/agoal` strips the namespaced compatibility prefix [0.12ms] -(pass) parseGoalCommand — Chinese intervals > `每5分钟` [0.24ms] -(pass) parseGoalCommand — Chinese intervals > `每 5 分钟` with spaces [0.06ms] -(pass) parseGoalCommand — Chinese intervals > `5分钟` bare (no 每) [0.13ms] +(pass) parseGoalCommand — English intervals > `5 min` form [0.14ms] +(pass) parseGoalCommand — English intervals > `5min` joined form [0.07ms] +(pass) parseGoalCommand — English intervals > `5 minutes` long form (plural wins over `min`) [0.06ms] +(pass) parseGoalCommand — English intervals > `1 hour` [0.07ms] +(pass) parseGoalCommand — English intervals > `hourly` keyword [0.07ms] +(pass) parseGoalCommand — English intervals > `daily` [0.06ms] +(pass) parseGoalCommand — English intervals > `1 day` [0.20ms] +(pass) parseGoalCommand — English intervals > `/goal` prefix is optional [0.06ms] +(pass) parseGoalCommand — English intervals > `/loop` alias [0.11ms] +(pass) parseGoalCommand — English intervals > `/aloop` strips the namespaced canonical prefix [0.12ms] +(pass) parseGoalCommand — English intervals > `/agoal` strips the namespaced compatibility prefix [0.10ms] +(pass) parseGoalCommand — Chinese intervals > `每5分钟` [0.23ms] +(pass) parseGoalCommand — Chinese intervals > `每 5 分钟` with spaces [0.07ms] +(pass) parseGoalCommand — Chinese intervals > `5分钟` bare (no 每) [0.15ms] (pass) parseGoalCommand — Chinese intervals > `每小时` [0.05ms] (pass) parseGoalCommand — Chinese intervals > `每天` [0.06ms] -(pass) parseGoalCommand — Chinese intervals > `每2小时` [0.13ms] -(pass) parseGoalCommand — rejection paths > no interval — reject [0.13ms] +(pass) parseGoalCommand — Chinese intervals > `每2小时` [0.07ms] +(pass) parseGoalCommand — rejection paths > no interval — reject [0.14ms] (pass) parseGoalCommand — rejection paths > empty input — reject [0.05ms] -(pass) parseGoalCommand — rejection paths > seconds rejected with informative error [0.10ms] -(pass) parseGoalCommand — rejection paths > Chinese 秒 rejected [0.08ms] -(pass) parseGoalCommand — rejection paths > text becomes empty after stripping interval — reject [0.07ms] -(pass) parseGoalCommand — rejection paths > `/goal hourly` alone — reject (no text) [0.04ms] -(pass) parseGoalCommand — rejection paths > MIN_INTERVAL_MS is 60s [0.04ms] -(pass) parseGoalCommand — defence-in-depth > `1 min` exact minimum is accepted [0.06ms] +(pass) parseGoalCommand — rejection paths > seconds rejected with informative error [0.09ms] +(pass) parseGoalCommand — rejection paths > Chinese 秒 rejected [0.06ms] +(pass) parseGoalCommand — rejection paths > text becomes empty after stripping interval — reject [0.08ms] +(pass) parseGoalCommand — rejection paths > `/goal hourly` alone — reject (no text) [0.05ms] +(pass) parseGoalCommand — rejection paths > MIN_INTERVAL_MS is 60s [0.03ms] +(pass) parseGoalCommand — defence-in-depth > `1 min` exact minimum is accepted [0.04ms] (pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `5m` parses to 5 × 60_000 ms (the canonical CLI emission) [0.06ms] (pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `30m` / `90m` arbitrary minutes parse correctly [0.10ms] (pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `2h` parses to 2 hours [0.08ms] (pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `1d` parses to 24 hours [0.07ms] (pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > single-letter and word-form yield the same interval (no semantic drift) [0.07ms] -(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `5min` still wins over `5m` (longest-prefix declaration order) [0.07ms] -(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > single-letter inside a larger word is NOT swallowed (lookahead guard) [0.05ms] -(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `30s` is rejected with sub-minute error (parser + CLI aligned) [0.08ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `5min` still wins over `5m` (longest-prefix declaration order) [0.06ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > single-letter inside a larger word is NOT swallowed (lookahead guard) [0.06ms] +(pass) parseGoalCommand — #144 round-6 single-letter units (CLI parity) > `30s` is rejected with sub-minute error (parser + CLI aligned) [0.09ms] src/goals/loops-grok-wire.test.ts: -(pass) grok ACP MCP injection — RFC-025 M3 wire > when LOOPS env unset, only commhub server (back-compat) [0.27ms] -(pass) grok ACP MCP injection — RFC-025 M3 wire > when LOOPS env set, commhub + loops servers both present [0.10ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > when LOOPS env unset, only commhub server (back-compat) [0.21ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > when LOOPS env set, commhub + loops servers both present [0.07ms] (pass) grok ACP MCP injection — RFC-025 M3 wire > loops server entry: ACP http schema (type+url+headers array) [0.08ms] -(pass) grok ACP MCP injection — RFC-025 M3 wire > loops headers: Authorization Bearer + transport tag + alias hint [0.16ms] -(pass) grok ACP MCP injection — RFC-025 M3 wire > loops entry localhost URL only (per security constraint) [0.12ms] -(pass) grok ACP MCP injection — RFC-025 M3 wire > loops + commhub independent: commhub headers don't leak token, loops headers don't leak ntok [0.15ms] -(pass) grok ACP MCP injection — #693 upload stdio > adds stdio commhub_upload when uploadMcpCommand provided [0.19ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops headers: Authorization Bearer + transport tag + alias hint [0.12ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops entry localhost URL only (per security constraint) [0.17ms] +(pass) grok ACP MCP injection — RFC-025 M3 wire > loops + commhub independent: commhub headers don't leak token, loops headers don't leak ntok [0.14ms] +(pass) grok ACP MCP injection — #693 upload stdio > adds stdio commhub_upload when uploadMcpCommand provided [0.20ms] src/goals/completion-detect.test.ts: -(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel on its own line [0.11ms] -(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel at end of text without trailing newline [0.03ms] -(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel at start of text [0.02ms] -(pass) isGoalCompleteSentinel — POSITIVE (must detect) > English GOAL_COMPLETE underscore on its own line [0.06ms] -(pass) isGoalCompleteSentinel — POSITIVE (must detect) > English GOAL COMPLETE (space) on its own line [0.02ms] -(pass) isGoalCompleteSentinel — POSITIVE (must detect) > sentinel with leading/trailing whitespace on the line [0.02ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel on its own line [0.16ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel at end of text without trailing newline [0.10ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > Chinese sentinel at start of text [0.03ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > English GOAL_COMPLETE underscore on its own line [0.09ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > English GOAL COMPLETE (space) on its own line [0.03ms] +(pass) isGoalCompleteSentinel — POSITIVE (must detect) > sentinel with leading/trailing whitespace on the line [0.03ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > bare 'completed' in progress report [0.02ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > 'X completed' phrase mid-sentence [0.02ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > Chinese '已完成' as section header (not the goal-complete sentinel) [0.03ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > Chinese '已完成 X 项' enumeration in body [0.04ms] -(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > 'goal completed' as a phrase inside prose (was caught by old regex) [0.04ms] +(pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > 'goal completed' as a phrase inside prose (was caught by old regex) [0.02ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > '目标已完成' substring without standalone line (old regex would match) [0.02ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > lowercased 'goal_complete' (sentinel is case-sensitive on English) [0.02ms] (pass) isGoalCompleteSentinel — NEGATIVE (regression gate, must NOT detect) > empty / null / undefined input [0.04ms] @@ -1012,210 +1006,210 @@ src/goals/schedule.test.ts: (pass) computeNextWakeAt — interval mode > interval 5min from a baseline returns baseline + 5min [0.07ms] (pass) computeNextWakeAt — interval mode > interval 24h returns +24h [0.04ms] (pass) computeNextWakeAt — interval mode > interval is timezone-independent (UTC anchor same result regardless of node TZ) [0.05ms] -(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called at 10:00 Asia/Shanghai → tomorrow 09:00 (already past today) [4.37ms] -(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called at 08:00 Asia/Shanghai → today 09:00 (still upcoming) [0.44ms] -(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called AT 09:00 exactly → today (boundary include) [0.55ms] -(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > falls back to node default TZ if schedule has no timezone [0.69ms] -(pass) computeNextWakeAt — weekday mode > Monday 09:00 Asia/Shanghai, called Sun 10:00 → tomorrow (Mon) 09:00 [0.65ms] -(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30 Asia/Shanghai, called Sun 10:00 → Monday 18:30 (next eligible) [0.40ms] -(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30, called Mon 18:00 → today 18:30 (today eligible AND time still upcoming) [0.29ms] -(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30, called Mon 19:00 → today is Mon but past 18:30 → Wed 18:30 [0.76ms] -(pass) computeNextWakeAt — weekday mode > Friday 09:00, called Saturday → next Friday (full week wrap-around) [0.78ms] -(pass) computeNextWakeAt — weekday mode > workdays ['mon','tue','wed','thu','fri'] for daily standup is supported [0.39ms] -(pass) computeNextWakeAt — DST edge cases (US Eastern) > 09:00 America/New_York in summer (EDT) → 13:00 UTC [0.51ms] -(pass) computeNextWakeAt — DST edge cases (US Eastern) > 09:00 America/New_York in winter (EST) → 14:00 UTC [0.66ms] -(pass) computeNextWakeAt — DST edge cases (US Eastern) > daily 02:30 wake DOES NOT skip on DST spring-forward day (just shifts that day) [0.57ms] -(pass) computeNextWakeAt — DST edge cases (US Eastern) > daily 03:30 exists on spring-forward day (post-jump, unambiguous EDT) [0.74ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called Sat noon → fires at FIRST 01:30 EDT (before fall-back) [0.63ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AT first 01:30 EDT boundary → NEXT DAY (not second 01:30 EST same day) [0.57ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called between the two occurrences (05:45 UTC) → next day [0.51ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AT fall-back moment (06:00 UTC) → next day (skip 2nd occurrence) [0.46ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AFTER second occurrence (06:30 UTC) → next day [0.51ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 02:30 (post-fallback UNAMBIGUOUS) still fires on fall-back day — was buggy before P1.3 [0.53ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 03:00 (fully post-fallback) on fall-back day — regression for iterated offset [0.66ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 01:30 on fall-back Sunday → first occurrence EDT [0.46ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 02:30 on fall-back Sunday → same day (was CRASH before P1.3) [0.47ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 01:30 called AT first fire → NEXT Sunday (not same-day 2nd occurrence) [0.98ms] -(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > time_of_day 09:00 on fall-back day (outside ambiguous window) unchanged [0.33ms] -(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule → uses interval_ms from goal context, returns now + interval [0.07ms] -(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule + zero fallback interval → still returns now (no negative offset) [0.08ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called at 10:00 Asia/Shanghai → tomorrow 09:00 (already past today) [7.87ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called at 08:00 Asia/Shanghai → today 09:00 (still upcoming) [0.47ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > 09:00 Asia/Shanghai, called AT 09:00 exactly → today (boundary include) [0.83ms] +(pass) computeNextWakeAt — time_of_day mode (per-TZ wall clock) > falls back to node default TZ if schedule has no timezone [0.78ms] +(pass) computeNextWakeAt — weekday mode > Monday 09:00 Asia/Shanghai, called Sun 10:00 → tomorrow (Mon) 09:00 [0.87ms] +(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30 Asia/Shanghai, called Sun 10:00 → Monday 18:30 (next eligible) [0.58ms] +(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30, called Mon 18:00 → today 18:30 (today eligible AND time still upcoming) [0.43ms] +(pass) computeNextWakeAt — weekday mode > Mon/Wed/Fri 18:30, called Mon 19:00 → today is Mon but past 18:30 → Wed 18:30 [0.78ms] +(pass) computeNextWakeAt — weekday mode > Friday 09:00, called Saturday → next Friday (full week wrap-around) [1.24ms] +(pass) computeNextWakeAt — weekday mode > workdays ['mon','tue','wed','thu','fri'] for daily standup is supported [0.75ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > 09:00 America/New_York in summer (EDT) → 13:00 UTC [0.79ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > 09:00 America/New_York in winter (EST) → 14:00 UTC [0.64ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > daily 02:30 wake DOES NOT skip on DST spring-forward day (just shifts that day) [0.72ms] +(pass) computeNextWakeAt — DST edge cases (US Eastern) > daily 03:30 exists on spring-forward day (post-jump, unambiguous EDT) [0.78ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called Sat noon → fires at FIRST 01:30 EDT (before fall-back) [0.77ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AT first 01:30 EDT boundary → NEXT DAY (not second 01:30 EST same day) [0.69ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called between the two occurrences (05:45 UTC) → next day [0.61ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AT fall-back moment (06:00 UTC) → next day (skip 2nd occurrence) [0.62ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 01:30, called AFTER second occurrence (06:30 UTC) → next day [0.68ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 02:30 (post-fallback UNAMBIGUOUS) still fires on fall-back day — was buggy before P1.3 [0.66ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > daily 03:00 (fully post-fallback) on fall-back day — regression for iterated offset [0.62ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 01:30 on fall-back Sunday → first occurrence EDT [0.51ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 02:30 on fall-back Sunday → same day (was CRASH before P1.3) [0.48ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > weekday Sun 01:30 called AT first fire → NEXT Sunday (not same-day 2nd occurrence) [1.10ms] +(pass) computeNextWakeAt — DST fall-back (autumn) — RFC-025 P1.3 > time_of_day 09:00 on fall-back day (outside ambiguous window) unchanged [0.39ms] +(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule → uses interval_ms from goal context, returns now + interval [0.09ms] +(pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule + zero fallback interval → still returns now (no negative offset) [0.07ms] (pass) computeNextWakeAt — legacy interval-only (back-compat regression) > undefined schedule + missing fallback interval throws (programmer error) [0.15ms] (pass) computeNextWakeAt — parser-rejected edge cases (defensive) > invalid time format '25:99' throws [0.16ms] -(pass) computeNextWakeAt — parser-rejected edge cases (defensive) > empty weekday list throws (caught by parser too, defense in depth) [0.14ms] +(pass) computeNextWakeAt — parser-rejected edge cases (defensive) > empty weekday list throws (caught by parser too, defense in depth) [0.12ms] (pass) computeNextWakeAt — parser-rejected edge cases (defensive) > unknown weekday name throws [0.14ms] src/goals/self-loop-tools.test.ts: -(pass) list_my_loops > empty store → {goals: [], total: 0} [2.42ms] -(pass) list_my_loops > includes goal_id_short + cadence schedule shape [0.81ms] -(pass) create_my_loop > interval string '5m' creates goal [0.64ms] -(pass) create_my_loop > cron-lite time_of_day creates goal with schedule field [1.64ms] -(pass) create_my_loop > missing task → invalid_args [0.32ms] +(pass) list_my_loops > empty store → {goals: [], total: 0} [3.34ms] +(pass) list_my_loops > includes goal_id_short + cadence schedule shape [0.92ms] +(pass) create_my_loop > interval string '5m' creates goal [0.69ms] +(pass) create_my_loop > cron-lite time_of_day creates goal with schedule field [2.00ms] +(pass) create_my_loop > missing task → invalid_args [0.31ms] (pass) create_my_loop > missing both schedule and interval → invalid_schedule [0.36ms] -(pass) create_my_loop > sub-minute interval rejected (parser 60s floor) [0.37ms] -(pass) create_my_loop > max active goals cap (3 cap → 4th rejected) [1.46ms] -(pass) edit_my_loop > change interval + report new value [1.61ms] -(pass) edit_my_loop > paused=true → status=paused [1.17ms] -(pass) edit_my_loop > cooldown — edit within 30s of last update rejected [0.56ms] -(pass) edit_my_loop > unknown goal_id → goal_not_found [0.32ms] -(pass) edit_my_loop > P0.3 unpause resets consecutive_failures (fresh 5-strike window) [1.13ms] -(pass) edit_my_loop > P0.3 paused=false when already active does NOT wipe mid-failure counter [1.33ms] -(pass) edit_my_loop > P0.3 paused=true does NOT reset consecutive_failures [1.19ms] -(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > pushes next_wake_at forward, interval_ms unchanged [1.50ms] -(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > invalid next_wake_in → invalid_interval [0.67ms] +(pass) create_my_loop > sub-minute interval rejected (parser 60s floor) [0.42ms] +(pass) create_my_loop > max active goals cap (3 cap → 4th rejected) [1.75ms] +(pass) edit_my_loop > change interval + report new value [1.56ms] +(pass) edit_my_loop > paused=true → status=paused [1.28ms] +(pass) edit_my_loop > cooldown — edit within 30s of last update rejected [0.65ms] +(pass) edit_my_loop > unknown goal_id → goal_not_found [0.37ms] +(pass) edit_my_loop > P0.3 unpause resets consecutive_failures (fresh 5-strike window) [1.39ms] +(pass) edit_my_loop > P0.3 paused=false when already active does NOT wipe mid-failure counter [1.40ms] +(pass) edit_my_loop > P0.3 paused=true does NOT reset consecutive_failures [1.21ms] +(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > pushes next_wake_at forward, interval_ms unchanged [1.79ms] +(pass) reschedule_my_loop (★ ScheduleWakeup 范式) > invalid next_wake_in → invalid_interval [0.69ms] (pass) reschedule_my_loop (★ ScheduleWakeup 范式) > cooldown applies [0.53ms] -(pass) complete_my_loop (★ 达标归档) > status → 'complete' [1.27ms] -(pass) complete_my_loop (★ 达标归档) > unknown goal_id → goal_not_found [0.31ms] -(pass) cancel_my_loop > status → 'cancelled' [1.12ms] -(pass) cancel_my_loop > batch cancel (3 in 30s) triggers confirm-back on 4th [3.35ms] -(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad timezone in schedule → invalid_schedule, NOT written [0.58ms] -(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad weekday → invalid_schedule, NOT written [0.43ms] -(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad time format → invalid_schedule, NOT written [0.41ms] -(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > edit_my_loop: bad timezone on edit → invalid_schedule, EXISTING goal untouched [0.85ms] -(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: VALID structured schedule still works (regression) [1.44ms] -(pass) SELF_LOOP_TOOL_SPECS — registration table > exports 6 tools with stable names [0.27ms] +(pass) complete_my_loop (★ 达标归档) > status → 'complete' [1.30ms] +(pass) complete_my_loop (★ 达标归档) > unknown goal_id → goal_not_found [0.43ms] +(pass) cancel_my_loop > status → 'cancelled' [1.15ms] +(pass) cancel_my_loop > batch cancel (3 in 30s) triggers confirm-back on 4th [3.61ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad timezone in schedule → invalid_schedule, NOT written [0.63ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad weekday → invalid_schedule, NOT written [0.58ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: bad time format → invalid_schedule, NOT written [0.45ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > edit_my_loop: bad timezone on edit → invalid_schedule, EXISTING goal untouched [0.84ms] +(pass) #302 round-2 — preflight computeNextWakeAt (self-lock prevention) > create_my_loop: VALID structured schedule still works (regression) [1.60ms] +(pass) SELF_LOOP_TOOL_SPECS — registration table > exports 6 tools with stable names [0.31ms] (pass) SELF_LOOP_TOOL_SPECS — registration table > every spec has non-empty description (LLM-discoverable) [0.22ms] -(pass) SELF_LOOP_TOOL_SPECS — registration table > description guides per RFC-025 §3.2 (intent-parse + report-back + safety) [0.38ms] +(pass) SELF_LOOP_TOOL_SPECS — registration table > description guides per RFC-025 §3.2 (intent-parse + report-back + safety) [0.48ms] src/goals/codex-wake.test.ts: -(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > startThread path → captures threadId, returns text + failed=false [1.54ms] -(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > startThread with thread.id still null → threadId undefined (SDK didn't expose id yet) [0.20ms] -(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > empty agent_message stream → returns '(无回复)' fallback [0.16ms] -(pass) runCodexWakeForGoal — subsequent wake (has codex_thread_id) > resumeThread succeeds → captures (possibly updated) threadId [0.25ms] -(pass) runCodexWakeForGoal — subsequent wake (has codex_thread_id) > resume returns thread whose .id was updated by SDK → reflects new id [0.20ms] -(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > resumeThread throws → startThread fallback, threadRebuilt=true, rebuildReason populated [0.47ms] -(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > startThread fallback also throws → failed=true with both errors surfaced [0.27ms] -(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > first wake + startThread throws → failed=true, threadRebuilt=false [0.32ms] -(pass) runCodexWakeForGoal — run-time error after thread obtained > runStreamed throws on first wake → failed=true, threadId still captured if SDK set it [0.29ms] -(pass) runCodexWakeForGoal — run-time error after thread obtained > runStreamed throws on resume → failed=true, threadRebuilt=false (resume itself worked) [0.27ms] -(pass) runCodexWakeForGoal — DI plumbing > newCodex called per wake (not cached across wakes — fresh client each time) [0.33ms] -(pass) runCodexWakeForGoal — DI plumbing > buildOpts passed verbatim to start/resume Thread [0.42ms] -(pass) runCodexWakeForGoal — DI plumbing > warn callback fires on resume-fail; log callback fires on success [0.44ms] -(pass) runCodexWakeForGoal — DI plumbing > missing log/warn deps → no throw (defaults are noops) [0.17ms] +(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > startThread path → captures threadId, returns text + failed=false [2.33ms] +(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > startThread with thread.id still null → threadId undefined (SDK didn't expose id yet) [0.28ms] +(pass) runCodexWakeForGoal — first wake (no codex_thread_id) > empty agent_message stream → returns '(无回复)' fallback [0.47ms] +(pass) runCodexWakeForGoal — subsequent wake (has codex_thread_id) > resumeThread succeeds → captures (possibly updated) threadId [0.40ms] +(pass) runCodexWakeForGoal — subsequent wake (has codex_thread_id) > resume returns thread whose .id was updated by SDK → reflects new id [0.23ms] +(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > resumeThread throws → startThread fallback, threadRebuilt=true, rebuildReason populated [0.73ms] +(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > startThread fallback also throws → failed=true with both errors surfaced [0.34ms] +(pass) runCodexWakeForGoal — resume-fail fallback (the critical path) > first wake + startThread throws → failed=true, threadRebuilt=false [0.24ms] +(pass) runCodexWakeForGoal — run-time error after thread obtained > runStreamed throws on first wake → failed=true, threadId still captured if SDK set it [0.28ms] +(pass) runCodexWakeForGoal — run-time error after thread obtained > runStreamed throws on resume → failed=true, threadRebuilt=false (resume itself worked) [0.32ms] +(pass) runCodexWakeForGoal — DI plumbing > newCodex called per wake (not cached across wakes — fresh client each time) [0.37ms] +(pass) runCodexWakeForGoal — DI plumbing > buildOpts passed verbatim to start/resume Thread [0.46ms] +(pass) runCodexWakeForGoal — DI plumbing > warn callback fires on resume-fail; log callback fires on success [0.43ms] +(pass) runCodexWakeForGoal — DI plumbing > missing log/warn deps → no throw (defaults are noops) [0.21ms] src/goals/scheduler.test.ts: -(pass) decideTickWork — basic selection > empty list → empty buckets [0.16ms] -(pass) decideTickWork — basic selection > single active goal due now → due [0.26ms] -(pass) decideTickWork — basic selection > single active goal due 1ms ago → due [0.07ms] -(pass) decideTickWork — basic selection > single active goal due 1ms in future → pending, not due [0.08ms] -(pass) decideTickWork — basic selection > multiple active goals: only the overdue ones wake; pending stay [0.19ms] -(pass) decideTickWork — status filtering > each non-active status is skipped (never appears in due) [0.13ms] -(pass) decideTickWork — status filtering > mixed batch: only active+due appear in due bucket [0.16ms] -(pass) decideTickWork — status filtering > wake order preserves input order — deterministic, no shuffling [0.10ms] -(pass) decideTickWork — invalid timestamp recovery > missing next_wake_at → treated as overdue (surface to wake handler) [0.08ms] +(pass) decideTickWork — basic selection > empty list → empty buckets [0.18ms] +(pass) decideTickWork — basic selection > single active goal due now → due [0.31ms] +(pass) decideTickWork — basic selection > single active goal due 1ms ago → due [0.08ms] +(pass) decideTickWork — basic selection > single active goal due 1ms in future → pending, not due [0.07ms] +(pass) decideTickWork — basic selection > multiple active goals: only the overdue ones wake; pending stay [0.17ms] +(pass) decideTickWork — status filtering > each non-active status is skipped (never appears in due) [0.17ms] +(pass) decideTickWork — status filtering > mixed batch: only active+due appear in due bucket [0.18ms] +(pass) decideTickWork — status filtering > wake order preserves input order — deterministic, no shuffling [0.15ms] +(pass) decideTickWork — invalid timestamp recovery > missing next_wake_at → treated as overdue (surface to wake handler) [0.06ms] (pass) decideTickWork — invalid timestamp recovery > empty string next_wake_at → treated as overdue [0.06ms] -(pass) decideTickWork — invalid timestamp recovery > garbage next_wake_at (Date.parse → NaN) → treated as overdue [0.05ms] -(pass) decideTickWork — invalid timestamp recovery > non-string next_wake_at (number 0 from corrupt JSON) → treated as overdue [0.05ms] -(pass) decideTickWork — invalid timestamp recovery > inactive + invalid timestamp → still skipped (status wins over wake check) [0.07ms] -(pass) decideTickWork — counter sanity > active + skipped sums to total goals; pending + due sums to active [0.13ms] +(pass) decideTickWork — invalid timestamp recovery > garbage next_wake_at (Date.parse → NaN) → treated as overdue [0.07ms] +(pass) decideTickWork — invalid timestamp recovery > non-string next_wake_at (number 0 from corrupt JSON) → treated as overdue [0.06ms] +(pass) decideTickWork — invalid timestamp recovery > inactive + invalid timestamp → still skipped (status wins over wake check) [0.06ms] +(pass) decideTickWork — counter sanity > active + skipped sums to total goals; pending + due sums to active [0.15ms] src/goals/store.test.ts: -(pass) GoalStore — basic lifecycle > fresh store: load with no file → ok, empty list [0.66ms] -(pass) GoalStore — basic lifecycle > upsert → get → list roundtrip [0.77ms] -(pass) GoalStore — basic lifecycle > delete → flushes to disk [1.44ms] -(pass) GoalStore — basic lifecycle > setStatus → in-memory + persisted [1.32ms] -(pass) GoalStore — basic lifecycle > setStatus on unknown id → undefined, no throw [0.29ms] -(pass) GoalStore — basic lifecycle > mutate applies in-place + bumps updated_at [6.64ms] -(pass) GoalStore — basic lifecycle > mutate on unknown id → undefined, mutator NOT invoked [0.40ms] -(pass) GoalStore — restart persistence > two instances see the same goals (= restart simulation) [0.91ms] -(pass) GoalStore — restart persistence > status change survives reload [1.17ms] -(pass) GoalStore — corruption recovery (#2) > invalid JSON → ok=false, .corrupt backup, empty store [2.00ms] -(pass) GoalStore — corruption recovery (#2) > unknown schema version → recovery [0.67ms] -(pass) GoalStore — corruption recovery (#2) > malformed shape (goals not array) → recovery [0.61ms] -(pass) GoalStore — Grok preview persistence boundary > recursively migrates task/progress/error, final writes, and archives at 0600 [3.40ms] -(pass) GoalStore — Grok preview persistence boundary > scrubs a broad-mode corrupt backup and replaces the live file with an empty safe store [1.47ms] -(pass) GoalStore — Grok preview persistence boundary > recursively scrubs a parseable unsupported-schema backup [1.38ms] -(pass) P0 runtime gate — name resolution > isClaudeRuntime accepts every claude alias [0.16ms] +(pass) GoalStore — basic lifecycle > fresh store: load with no file → ok, empty list [0.89ms] +(pass) GoalStore — basic lifecycle > upsert → get → list roundtrip [1.02ms] +(pass) GoalStore — basic lifecycle > delete → flushes to disk [1.74ms] +(pass) GoalStore — basic lifecycle > setStatus → in-memory + persisted [1.39ms] +(pass) GoalStore — basic lifecycle > setStatus on unknown id → undefined, no throw [0.34ms] +(pass) GoalStore — basic lifecycle > mutate applies in-place + bumps updated_at [6.81ms] +(pass) GoalStore — basic lifecycle > mutate on unknown id → undefined, mutator NOT invoked [0.43ms] +(pass) GoalStore — restart persistence > two instances see the same goals (= restart simulation) [1.36ms] +(pass) GoalStore — restart persistence > status change survives reload [1.18ms] +(pass) GoalStore — corruption recovery (#2) > invalid JSON → ok=false, .corrupt backup, empty store [2.07ms] +(pass) GoalStore — corruption recovery (#2) > unknown schema version → recovery [0.78ms] +(pass) GoalStore — corruption recovery (#2) > malformed shape (goals not array) → recovery [0.53ms] +(pass) GoalStore — Grok preview persistence boundary > recursively migrates task/progress/error, final writes, and archives at 0600 [4.13ms] +(pass) GoalStore — Grok preview persistence boundary > scrubs a broad-mode corrupt backup and replaces the live file with an empty safe store [1.82ms] +(pass) GoalStore — Grok preview persistence boundary > recursively scrubs a parseable unsupported-schema backup [1.61ms] +(pass) P0 runtime gate — name resolution > isClaudeRuntime accepts every claude alias [0.17ms] (pass) P0 runtime gate — name resolution > isClaudeRuntime rejects codex / grok / unknown / empty [0.10ms] -(pass) P0 runtime gate — name resolution > runtimeBucket maps to canonical buckets [0.16ms] -(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > newGoal({runtime: 'claude-agent-sdk'}) succeeds (was the load-bearing bug) [0.10ms] -(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > newGoal succeeds for every recognized runtime alias (no per-bucket carve-out) [0.17ms] -(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > GoalStore.upsert accepts a claude-runtime goal end-to-end [0.61ms] -(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > isClaudeRuntime still classifies (kept for cross-bucket detection, not gating) [0.06ms] -(pass) P0 runtime gate — archiveAndClear > with live goals: backup file created, store emptied, reload sees empty [1.64ms] -(pass) P0 runtime gate — archiveAndClear > with no live file: returns undefined, no throw, store still flushes empty [0.53ms] -(pass) P0 runtime gate — archiveAndClear > backup filenames are unique across rapid calls [14.77ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + empty → ok (scheduler runs; was 'skip' pre-#144) [0.31ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + only claude-active goals → ok (scheduler runs) [0.25ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + empty → ok [0.08ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + only codex goals → ok [0.15ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > grok + only grok goals → ok [0.08ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + active codex/grok goals → archive + runScheduler=true (recover after archive) [0.30ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + grok-active leftover → archive (NOT fatal exit anymore) [0.10ms] +(pass) P0 runtime gate — name resolution > runtimeBucket maps to canonical buckets [0.14ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > newGoal({runtime: 'claude-agent-sdk'}) succeeds (was the load-bearing bug) [0.18ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > newGoal succeeds for every recognized runtime alias (no per-bucket carve-out) [0.20ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > GoalStore.upsert accepts a claude-runtime goal end-to-end [0.63ms] +(pass) #144 round-6 — claude runtime gate REMOVED, scheduler is universal > isClaudeRuntime still classifies (kept for cross-bucket detection, not gating) [0.07ms] +(pass) P0 runtime gate — archiveAndClear > with live goals: backup file created, store emptied, reload sees empty [1.62ms] +(pass) P0 runtime gate — archiveAndClear > with no live file: returns undefined, no throw, store still flushes empty [0.52ms] +(pass) P0 runtime gate — archiveAndClear > backup filenames are unique across rapid calls [15.62ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + empty → ok (scheduler runs; was 'skip' pre-#144) [0.35ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + only claude-active goals → ok (scheduler runs) [0.24ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + empty → ok [0.04ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + only codex goals → ok [0.10ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > grok + only grok goals → ok [0.07ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude + active codex/grok goals → archive + runScheduler=true (recover after archive) [0.27ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > codex + grok-active leftover → archive (NOT fatal exit anymore) [0.12ms] (pass) #144 round-6 — decideStartupAction (refined-B matrix) > grok + codex-active leftover → archive [0.07ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > inactive foreign-bucket goals do NOT trigger archive (only `active` counts) [0.13ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude with only inactive foreign leftover → ok (just cleanup pending) [0.06ms] -(pass) #144 round-6 — decideStartupAction (refined-B matrix) > unknown bucket → skip (no scheduler, no auto-archive) [0.07ms] -(pass) GoalStore — mutex serialisation (#1+#3) > 50 concurrent upserts → all 50 persist (no torn writes) [19.69ms] -(pass) GoalStore — mutex serialisation (#1+#3) > interleaved upsert + setStatus + delete stays consistent [12.45ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > inactive foreign-bucket goals do NOT trigger archive (only `active` counts) [0.12ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > claude with only inactive foreign leftover → ok (just cleanup pending) [0.09ms] +(pass) #144 round-6 — decideStartupAction (refined-B matrix) > unknown bucket → skip (no scheduler, no auto-archive) [0.09ms] +(pass) GoalStore — mutex serialisation (#1+#3) > 50 concurrent upserts → all 50 persist (no torn writes) [25.07ms] +(pass) GoalStore — mutex serialisation (#1+#3) > interleaved upsert + setStatus + delete stays consistent [14.75ms] src/runtime/grok-copresence/profile-process.test.ts: -(pass) Grok co-presence profile is pinned for the whole process > same input yields two exact, non-overlapping process capabilities [125.31ms] +(pass) Grok co-presence profile is pinned for the whole process > same input yields two exact, non-overlapping process capabilities [141.23ms] src/runtime/grok-copresence/jsonl.test.ts: -(pass) Grok copresence envelope and user parsing > parses only an exact, query-anchored Agent Network envelope [0.45ms] -(pass) Grok copresence envelope and user parsing > extracts the first authoritative user_query from string or Grok text-array content [0.53ms] -(pass) Grok copresence envelope and user parsing > does not trust a syntactically valid prefix unless the bridge registered it [2.45ms] -(pass) Grok copresence envelope and user parsing > nested user_query text cannot turn an owned network task into human delegation [0.41ms] -(pass) Grok copresence turn reducer > waits for completion and replies with the last non-empty assistant record [0.54ms] +(pass) Grok copresence envelope and user parsing > parses only an exact, query-anchored Agent Network envelope [0.35ms] +(pass) Grok copresence envelope and user parsing > extracts the first authoritative user_query from string or Grok text-array content [0.41ms] +(pass) Grok copresence envelope and user parsing > does not trust a syntactically valid prefix unless the bridge registered it [1.93ms] +(pass) Grok copresence envelope and user parsing > nested user_query text cannot turn an owned network task into human delegation [0.51ms] +(pass) Grok copresence turn reducer > waits for completion and replies with the last non-empty assistant record [0.69ms] (pass) Grok copresence turn reducer > keeps the last no-tool assistant when later tool-bearing chatter exists [0.27ms] (pass) Grok copresence turn reducer > handles completion/chat-history polling order without returning an empty reply [0.35ms] -(pass) Grok copresence turn reducer > does not finalize an intermediate assistant visible before the completion event [0.24ms] +(pass) Grok copresence turn reducer > does not finalize an intermediate assistant visible before the completion event [0.31ms] (pass) Grok copresence turn reducer > retains a completion observed before even the network user line [0.39ms] -(pass) Grok copresence turn reducer > retains an event-first human completion only for a trusted PTY submission [0.28ms] -(pass) Grok copresence turn reducer > never carries an unowned idle completion into a later network task [0.27ms] -(pass) Grok copresence turn reducer > binds an event-first completion to the exact registered network task [0.38ms] -(pass) Grok copresence turn reducer > consumes sanitized sample A block content and turn_number boundary [0.26ms] -(pass) Grok copresence turn reducer > consumes sanitized sample B and selects only the 14th no-tool assistant [0.63ms] -(pass) Grok copresence turn reducer > ignores standalone system-reminder user records without abandoning a network turn [0.25ms] -(pass) Grok copresence turn reducer > fails a terminal record without turn_started and never binds it to the next user [0.21ms] -(pass) Grok copresence turn reducer > never maps a human turn or failed network turn to a network reply [0.46ms] -(pass) Grok copresence turn reducer > abandons an unfinished network turn rather than attaching its answer to a human turn [0.39ms] -(pass) Grok copresence turn reducer > pairs events correctly when chat_history leads by two unnumbered turns [0.61ms] -(pass) Grok copresence turn reducer > does not let a new start overtake an abandoned numbered terminal [0.43ms] -(pass) Grok completion compatibility and defensive parsing > recognizes only top-level turn_ended with an exact successful outcome [0.34ms] -(pass) Grok completion compatibility and defensive parsing > binds turn_started turn_number while permission lifecycle remains inert [0.28ms] +(pass) Grok copresence turn reducer > retains an event-first human completion only for a trusted PTY submission [0.29ms] +(pass) Grok copresence turn reducer > never carries an unowned idle completion into a later network task [0.30ms] +(pass) Grok copresence turn reducer > binds an event-first completion to the exact registered network task [0.42ms] +(pass) Grok copresence turn reducer > consumes sanitized sample A block content and turn_number boundary [0.29ms] +(pass) Grok copresence turn reducer > consumes sanitized sample B and selects only the 14th no-tool assistant [0.67ms] +(pass) Grok copresence turn reducer > ignores standalone system-reminder user records without abandoning a network turn [0.26ms] +(pass) Grok copresence turn reducer > fails a terminal record without turn_started and never binds it to the next user [0.31ms] +(pass) Grok copresence turn reducer > never maps a human turn or failed network turn to a network reply [0.55ms] +(pass) Grok copresence turn reducer > abandons an unfinished network turn rather than attaching its answer to a human turn [1.02ms] +(pass) Grok copresence turn reducer > pairs events correctly when chat_history leads by two unnumbered turns [0.67ms] +(pass) Grok copresence turn reducer > does not let a new start overtake an abandoned numbered terminal [0.48ms] +(pass) Grok completion compatibility and defensive parsing > recognizes only top-level turn_ended with an exact successful outcome [0.25ms] +(pass) Grok completion compatibility and defensive parsing > binds turn_started turn_number while permission lifecycle remains inert [0.29ms] (pass) Grok completion compatibility and defensive parsing > fails a started turn when turn_ended has no outcome [0.24ms] -(pass) Grok completion compatibility and defensive parsing > fails closed on an overlapping turn_started epoch [0.24ms] -(pass) Grok completion compatibility and defensive parsing > retains only a bounded tail of raw completion candidates [0.19ms] -(pass) Grok completion compatibility and defensive parsing > contains malformed and overlong lines instead of parsing or retaining them [1.27ms] -(pass) Grok completion compatibility and defensive parsing > incrementally joins split lines and drops a fragmented oversized line once [1.38ms] -(pass) persistent JSONL tail cursor > starts fresh at end by default, with an explicit start override [0.33ms] -(pass) persistent JSONL tail cursor > continues and fails closed on truncate or inode rotation [0.21ms] -(pass) persistent JSONL tail cursor > treats corrupt persisted state as non-replayable and advances JSON-safely [0.21ms] +(pass) Grok completion compatibility and defensive parsing > fails closed on an overlapping turn_started epoch [0.22ms] +(pass) Grok completion compatibility and defensive parsing > retains only a bounded tail of raw completion candidates [0.18ms] +(pass) Grok completion compatibility and defensive parsing > contains malformed and overlong lines instead of parsing or retaining them [1.42ms] +(pass) Grok completion compatibility and defensive parsing > incrementally joins split lines and drops a fragmented oversized line once [1.46ms] +(pass) persistent JSONL tail cursor > starts fresh at end by default, with an explicit start override [0.43ms] +(pass) persistent JSONL tail cursor > continues and fails closed on truncate or inode rotation [0.20ms] +(pass) persistent JSONL tail cursor > treats corrupt persisted state as non-replayable and advances JSON-safely [0.27ms] src/runtime/grok-copresence/attach.test.ts: -(pass) Grok co-presence local attach server > serves one owner-only client and cleans its socket on close [19.08ms] -(pass) Grok co-presence local attach server > rejects a second client without disturbing the attached human [7.98ms] -(pass) Grok co-presence local attach server > routes input and resize frames only through serialized arbiter callbacks [2.82ms] -(pass) Grok co-presence local attach server > fails closed when an inbound frame exceeds the configured bound [6.47ms] -(pass) Grok co-presence local attach server > refuses symlinks and regular files at the socket path [0.76ms] +(pass) Grok co-presence local attach server > serves one owner-only client and cleans its socket on close [23.66ms] +(pass) Grok co-presence local attach server > rejects a second client without disturbing the attached human [5.33ms] +(pass) Grok co-presence local attach server > routes input and resize frames only through serialized arbiter callbacks [3.18ms] +(pass) Grok co-presence local attach server > fails closed when an inbound frame exceeds the configured bound [20.91ms] +(pass) Grok co-presence local attach server > refuses symlinks and regular files at the socket path [1.25ms] src/runtime/grok-copresence/state.test.ts: -(pass) Grok co-presence arbitration > lets the first human byte win a simultaneous human/network race [1.55ms] +(pass) Grok co-presence arbitration > lets the first human byte win a simultaneous human/network race [1.44ms] (pass) Grok co-presence arbitration > gives a newly active human composer priority over an existing FIFO [0.35ms] -(pass) Grok co-presence arbitration > dequeues network tasks FIFO and never preempts an active turn [0.70ms] -(pass) Grok co-presence arbitration > cancels only queued timeouts and rejects duplicate task ids [0.45ms] -(pass) Grok co-presence arbitration > retains the active network task and FIFO across disconnect/reconnect [0.95ms] -(pass) Grok co-presence arbitration > marks approvals waiting for the human without emitting a response [0.26ms] -(pass) Grok co-presence arbitration > clears an already-waiting preview todo resolution in either active turn without completing it [0.32ms] +(pass) Grok co-presence arbitration > dequeues network tasks FIFO and never preempts an active turn [0.64ms] +(pass) Grok co-presence arbitration > cancels only queued timeouts and rejects duplicate task ids [0.38ms] +(pass) Grok co-presence arbitration > retains the active network task and FIFO across disconnect/reconnect [0.94ms] +(pass) Grok co-presence arbitration > marks approvals waiting for the human without emitting a response [0.24ms] +(pass) Grok co-presence arbitration > clears an already-waiting preview todo resolution in either active turn without completing it [0.27ms] src/runtime/grok-copresence/profile-wiring.test.ts: -(pass) Grok co-presence profile wiring > pins validated config before dynamically loading the runtime [1.84ms] -(pass) Grok co-presence profile wiring > cannot mutate the capability according to a logical turn owner [0.14ms] +(pass) Grok co-presence profile wiring > pins validated config before dynamically loading the runtime [2.24ms] +(pass) Grok co-presence profile wiring > cannot mutate the capability according to a logical turn owner [0.16ms] src/runtime/grok-copresence/leader-lifecycle.test.ts: -(pass) Grok auto-Leader lifecycle identity > rejects a different kernel executable hidden behind a pinned argv0 [0.89ms] -(pass) Grok auto-Leader lifecycle identity > rejects a live native listener whose argv0 forges the pinned executable [358.36ms] -(pass) Grok auto-Leader lifecycle identity > terminates one exact generation and removes only its stale socket [96.15ms] -(pass) Grok auto-Leader lifecycle identity > does not adopt a listener whose generation marker differs [156.01ms] -(pass) Grok auto-Leader lifecycle identity > does not signal or unlink after the socket pathname is replaced [53.40ms] -(pass) Grok auto-Leader lifecycle identity > revalidates the exact identity before escalating a TERM-resistant Leader [615.14ms] -(pass) Grok auto-Leader lifecycle identity > does not escalate when a TERM-resistant Leader replaces its listener [388.61ms] -(pass) Grok auto-Leader lifecycle identity > does not signal after the configured binary inode is replaced [55.52ms] -(pass) Grok auto-Leader lifecycle identity > retains the stale socket when another process from the generation remains [282.49ms] +(pass) Grok auto-Leader lifecycle identity > rejects a different kernel executable hidden behind a pinned argv0 [1.13ms] +(pass) Grok auto-Leader lifecycle identity > rejects a live native listener whose argv0 forges the pinned executable [361.96ms] +(pass) Grok auto-Leader lifecycle identity > terminates one exact generation and removes only its stale socket [96.84ms] +(pass) Grok auto-Leader lifecycle identity > does not adopt a listener whose generation marker differs [180.37ms] +(pass) Grok auto-Leader lifecycle identity > does not signal or unlink after the socket pathname is replaced [74.03ms] +(pass) Grok auto-Leader lifecycle identity > revalidates the exact identity before escalating a TERM-resistant Leader [610.51ms] +(pass) Grok auto-Leader lifecycle identity > does not escalate when a TERM-resistant Leader replaces its listener [391.68ms] +(pass) Grok auto-Leader lifecycle identity > does not signal after the configured binary inode is replaced [79.00ms] +(pass) Grok auto-Leader lifecycle identity > retains the stale socket when another process from the generation remains [299.72ms] src/runtime/grok-copresence/allowlist-near-miss.test.ts: -(pass) grok copresence preview tool profile is an exact value set > a profile tool with an otherwise valid tuple is accepted [0.51ms] +(pass) grok copresence preview tool profile is an exact value set > a profile tool with an otherwise valid tuple is accepted [0.50ms] (pass) grok copresence preview tool profile is an exact value set > refuses "todo_write2" [0.04ms] (pass) grok copresence preview tool profile is an exact value set > refuses "search_tool2" (pass) grok copresence preview tool profile is an exact value set > refuses "use_tool2" @@ -1234,139 +1228,139 @@ src/runtime/grok-copresence/allowlist-near-miss.test.ts: (pass) grok copresence preview tool profile is an exact value set > refuses "use_tool" (pass) grok copresence preview tool profile is an exact value set > refuses "use​tool" (pass) grok copresence preview tool profile is an exact value set > refuses "use_to​ol" -(pass) grok copresence preview tool profile is an exact value set > refuses "" +(pass) grok copresence preview tool profile is an exact value set > refuses "" [0.02ms] (pass) grok copresence preview tool profile is an exact value set > refuses " " -(pass) grok copresence preview tool profile is an exact value set > refuses "Search_Tool" [0.02ms] +(pass) grok copresence preview tool profile is an exact value set > refuses "Search_Tool" (pass) grok copresence preview tool profile is an exact value set > refuses "TODO_WRITE" (pass) grok copresence preview tool profile is an exact value set > refuses "use_tool\r" (pass) grok copresence preview tool profile is an exact value set > refuses "use_tool\u0000" (pass) grok copresence preview tool profile is an exact value set > refuses "x_todo_write" (pass) grok copresence preview tool profile is an exact value set > refuses "use-tool" (pass) grok copresence preview tool profile is an exact value set > refuses "todo-write" -(pass) grok copresence preview tool profile is an exact value set > the profile is exactly the three pinned tools [0.12ms] +(pass) grok copresence preview tool profile is an exact value set > the profile is exactly the three pinned tools [0.11ms] src/runtime/grok-copresence/profile-selection.test.ts: -(pass) Grok co-presence process capability profile > accepts only the two exact startup profiles [0.38ms] -(pass) Grok co-presence process capability profile > defaults closed and rejects an invalid process profile [0.14ms] +(pass) Grok co-presence process capability profile > accepts only the two exact startup profiles [0.19ms] +(pass) Grok co-presence process capability profile > defaults closed and rejects an invalid process profile [0.09ms] src/runtime/grok-copresence/runtime.test.ts: -(pass) Grok copresence launch and injection policy > keeps the fixed-tool auto-resolution exception exact and limited to active turns [0.50ms] -(pass) Grok copresence launch and injection policy > admits exact automatic lifecycles only for the fixed preview tool boundary [0.34ms] -(pass) Grok copresence launch and injection policy > exposes only reviewed value-free task failure codes and exact JSONL subcodes [0.64ms] -(pass) Grok copresence launch and injection policy > keeps the JSONL subcode allowlist direct, frozen, and actual-path-only [0.32ms] -(pass) Grok copresence launch and injection policy > locks the probed Grok TUI build exactly [0.22ms] -(pass) Grok copresence launch and injection policy > pins one TUI-effective commhub-only agent profile and hard-denies fallback routes [1.03ms] -(pass) Grok copresence launch and injection policy > rejects terminal escape injection and reserved origin markup [0.40ms] -(pass) Grok copresence launch and injection policy > recognizes the pinned TUI composer footer across ANSI fragments [0.24ms] -(pass) Grok copresence launch and injection policy > rejects external permission sources and noninteractive modes [1.45ms] -(pass) Grok copresence runtime integration > terminates the independently persistent auto-Leader and its unchanged stale socket [580.11ms] -(pass) Grok copresence runtime integration > cleans and hardens the exact pinned footprint only after confirmed close [579.89ms] -(pass) Grok copresence runtime integration > cleans each exact sandbox placeholder at its confirmed recovery boundary [894.29ms] -(pass) Grok copresence runtime integration > removes an old placeholder before a recovery generation reuses its PID [1188.53ms] -(pass) Grok copresence runtime integration > queues network input until the pinned TUI composer is ready [1229.43ms] -(pass) Grok copresence runtime integration > maps keyless fake-writer file mutations to exact value-free tail subcodes [3984.24ms] -(pass) Grok copresence runtime integration > continues exactly once across prefix-preserving atomic chat rewrites [2337.40ms] -(pass) Grok copresence runtime integration > rejects an atomic replacement that preserves only the consumed prefix [700.57ms] -(pass) Grok copresence runtime integration > rejects a same-inode shrink below the highest observed size even when offset remains valid [575.20ms] -(pass) Grok copresence runtime integration > does not expose an intermediate atomic generation before its successor preserves it [1077.85ms] -(pass) Grok copresence runtime integration > does not expose a pinned generation unlinked between path check and read [1113.77ms] -(pass) Grok copresence runtime integration > maps chat and events reset callback failures and stops polling after fatal [1504.47ms] -(pass) Grok copresence runtime integration > maps keyless reducer, lifecycle, and combined flush invariants at their boundaries [2469.43ms] -(pass) Grok copresence runtime integration > close waits for and tears down a Leader spawned by in-flight recovery [932.12ms] -(pass) Grok copresence runtime integration > retains containment and lifetime locks when a closing recovery PTY will not stop [2913.69ms] -(pass) Grok copresence runtime integration > excludes a different runtime from the same canonical project for the full TUI lifetime [1099.34ms] -(pass) Grok copresence runtime integration > contains an exited recovery generation before reusing its PID [1784.74ms] -(pass) Grok copresence runtime integration > retains final-cleanup ownership after every failed recovery PID is consumed [908.29ms] -(pass) Grok copresence runtime integration > reports exact submission and trusted consumption, never queued admission [1767.19ms] -(pass) Grok copresence runtime integration > arbitrates a live PTY, settles final JSONL, attaches once, and resumes [4051.67ms] -(pass) Grok copresence runtime integration > fails closed on automatic permission resolution without a human action [591.47ms] -(pass) Grok copresence runtime integration > accepts only the pinned preview todo_write automatic resolution tuple [1866.55ms] -(pass) Grok copresence runtime integration > keeps the shared TUI alive when the pinned preview auto-resolves todo_write in a human turn [1776.40ms] -(pass) Grok copresence runtime integration > keeps the shared TUI alive across exact search_tool then use_tool in a human turn [1672.64ms] -(pass) Grok copresence runtime integration > rejects every mutated preview todo_write automatic resolution tuple [4044.99ms] -(pass) Grok copresence runtime integration > preserves exact permission lifecycle order across coalesced and split event reads [2340.82ms] -(pass) Grok copresence runtime integration > fails closed on malformed or oversized permission lifecycle JSONL [1188.62ms] -(pass) Grok copresence runtime integration > rejects terminal reordering around automatic permission lifecycles [1717.17ms] -(pass) Grok copresence runtime integration > allows repeated fixed-tool automatic permission lifecycles in one network turn [1084.82ms] -(pass) Grok copresence runtime integration > never replies with a tool-bearing assistant when the final log is delayed past settling [1875.27ms] -(pass) Grok copresence runtime integration > rejects a completed turn that never resolved its approval [561.54ms] -(pass) Grok copresence runtime integration > does not resume a TUI that crashed at an approval prompt [608.36ms] -(pass) Grok copresence runtime integration > rejects a permission record that landed just before the crash poll [953.60ms] -(pass) Grok copresence runtime integration > refuses process-level resume with a persisted unresolved approval [192.82ms] -(pass) Grok copresence runtime integration > permits process-level resume after a persisted approval was resolved [536.01ms] -(pass) Grok copresence runtime integration > arms both resume tails before spawn-time permission records can be skipped [299.42ms] -(pass) Grok copresence runtime integration > discards spawn-time orphan completions before accepting the first new network task [1108.69ms] -(pass) Grok copresence runtime integration > drains more than one tail chunk before attach and fully cleans a startup rejection [878.41ms] -(pass) Grok copresence runtime integration > accepts the pinned startup auto-approval transition [533.93ms] -(pass) Grok copresence runtime integration > reruns the spawn audit and refuses recovery when it fails [781.25ms] -(pass) Grok copresence runtime integration > keeps auto-approval across recovery before scheduling [1734.82ms] -(pass) Grok copresence runtime integration > jointly drains chat and events until both recovery cursors are stable [1851.03ms] -(pass) Grok copresence runtime integration > rejects a beforeSpawn callback that widens a controlled child setting [201.19ms] -(pass) Grok copresence runtime integration > gives every real lifetime-lock holder only the exact helper environment [552.75ms] +(pass) Grok copresence launch and injection policy > keeps the fixed-tool auto-resolution exception exact and limited to active turns [0.53ms] +(pass) Grok copresence launch and injection policy > admits exact automatic lifecycles only for the fixed preview tool boundary [0.35ms] +(pass) Grok copresence launch and injection policy > exposes only reviewed value-free task failure codes and exact JSONL subcodes [0.57ms] +(pass) Grok copresence launch and injection policy > keeps the JSONL subcode allowlist direct, frozen, and actual-path-only [0.34ms] +(pass) Grok copresence launch and injection policy > locks the probed Grok TUI build exactly [0.24ms] +(pass) Grok copresence launch and injection policy > pins one TUI-effective commhub-only agent profile and hard-denies fallback routes [0.88ms] +(pass) Grok copresence launch and injection policy > rejects terminal escape injection and reserved origin markup [0.51ms] +(pass) Grok copresence launch and injection policy > recognizes the pinned TUI composer footer across ANSI fragments [0.27ms] +(pass) Grok copresence launch and injection policy > rejects external permission sources and noninteractive modes [1.42ms] +(pass) Grok copresence runtime integration > terminates the independently persistent auto-Leader and its unchanged stale socket [592.80ms] +(pass) Grok copresence runtime integration > cleans and hardens the exact pinned footprint only after confirmed close [572.13ms] +(pass) Grok copresence runtime integration > cleans each exact sandbox placeholder at its confirmed recovery boundary [917.23ms] +(pass) Grok copresence runtime integration > removes an old placeholder before a recovery generation reuses its PID [1199.68ms] +(pass) Grok copresence runtime integration > queues network input until the pinned TUI composer is ready [1215.76ms] +(pass) Grok copresence runtime integration > maps keyless fake-writer file mutations to exact value-free tail subcodes [4028.98ms] +(pass) Grok copresence runtime integration > continues exactly once across prefix-preserving atomic chat rewrites [2299.54ms] +(pass) Grok copresence runtime integration > rejects an atomic replacement that preserves only the consumed prefix [700.42ms] +(pass) Grok copresence runtime integration > rejects a same-inode shrink below the highest observed size even when offset remains valid [599.32ms] +(pass) Grok copresence runtime integration > does not expose an intermediate atomic generation before its successor preserves it [1141.02ms] +(pass) Grok copresence runtime integration > does not expose a pinned generation unlinked between path check and read [1110.30ms] +(pass) Grok copresence runtime integration > maps chat and events reset callback failures and stops polling after fatal [1528.62ms] +(pass) Grok copresence runtime integration > maps keyless reducer, lifecycle, and combined flush invariants at their boundaries [2552.40ms] +(pass) Grok copresence runtime integration > close waits for and tears down a Leader spawned by in-flight recovery [934.48ms] +(pass) Grok copresence runtime integration > retains containment and lifetime locks when a closing recovery PTY will not stop [2930.77ms] +(pass) Grok copresence runtime integration > excludes a different runtime from the same canonical project for the full TUI lifetime [1150.02ms] +(pass) Grok copresence runtime integration > contains an exited recovery generation before reusing its PID [1819.02ms] +(pass) Grok copresence runtime integration > retains final-cleanup ownership after every failed recovery PID is consumed [933.85ms] +(pass) Grok copresence runtime integration > reports exact submission and trusted consumption, never queued admission [1785.26ms] +(pass) Grok copresence runtime integration > arbitrates a live PTY, settles final JSONL, attaches once, and resumes [4069.81ms] +(pass) Grok copresence runtime integration > fails closed on automatic permission resolution without a human action [613.58ms] +(pass) Grok copresence runtime integration > accepts only the pinned preview todo_write automatic resolution tuple [1887.04ms] +(pass) Grok copresence runtime integration > keeps the shared TUI alive when the pinned preview auto-resolves todo_write in a human turn [1807.85ms] +(pass) Grok copresence runtime integration > keeps the shared TUI alive across exact search_tool then use_tool in a human turn [1696.84ms] +(pass) Grok copresence runtime integration > rejects every mutated preview todo_write automatic resolution tuple [4315.25ms] +(pass) Grok copresence runtime integration > preserves exact permission lifecycle order across coalesced and split event reads [2390.47ms] +(pass) Grok copresence runtime integration > fails closed on malformed or oversized permission lifecycle JSONL [1162.13ms] +(pass) Grok copresence runtime integration > rejects terminal reordering around automatic permission lifecycles [1807.29ms] +(pass) Grok copresence runtime integration > allows repeated fixed-tool automatic permission lifecycles in one network turn [1110.09ms] +(pass) Grok copresence runtime integration > never replies with a tool-bearing assistant when the final log is delayed past settling [1913.52ms] +(pass) Grok copresence runtime integration > rejects a completed turn that never resolved its approval [595.60ms] +(pass) Grok copresence runtime integration > does not resume a TUI that crashed at an approval prompt [604.85ms] +(pass) Grok copresence runtime integration > rejects a permission record that landed just before the crash poll [907.34ms] +(pass) Grok copresence runtime integration > refuses process-level resume with a persisted unresolved approval [210.74ms] +(pass) Grok copresence runtime integration > permits process-level resume after a persisted approval was resolved [586.21ms] +(pass) Grok copresence runtime integration > arms both resume tails before spawn-time permission records can be skipped [311.32ms] +(pass) Grok copresence runtime integration > discards spawn-time orphan completions before accepting the first new network task [1124.46ms] +(pass) Grok copresence runtime integration > drains more than one tail chunk before attach and fully cleans a startup rejection [909.51ms] +(pass) Grok copresence runtime integration > accepts the pinned startup auto-approval transition [571.62ms] +(pass) Grok copresence runtime integration > reruns the spawn audit and refuses recovery when it fails [833.10ms] +(pass) Grok copresence runtime integration > keeps auto-approval across recovery before scheduling [1728.49ms] +(pass) Grok copresence runtime integration > jointly drains chat and events until both recovery cursors are stable [1871.05ms] +(pass) Grok copresence runtime integration > rejects a beforeSpawn callback that widens a controlled child setting [223.43ms] +(pass) Grok copresence runtime integration > gives every real lifetime-lock holder only the exact helper environment [557.68ms] src/runtime/opencode-acp/events.test.ts: -(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_message_chunk with text content → replyText += content.text [2.34ms] -(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_thought_chunk with text → thoughtText, NOT replyText (grok discipline) [0.20ms] -(pass) reduceOpencodeAcpNotification — session/update dispatch > tool_call and tool_call_update both bump toolCalls [0.08ms] -(pass) reduceOpencodeAcpNotification — session/update dispatch > usage_update snaps totalTokens into state.usage [0.11ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_message_chunk with text content → replyText += content.text [2.48ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > agent_thought_chunk with text → thoughtText, NOT replyText (grok discipline) [0.18ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > tool_call and tool_call_update both bump toolCalls [0.07ms] +(pass) reduceOpencodeAcpNotification — session/update dispatch > usage_update snaps totalTokens into state.usage [0.09ms] (pass) reduceOpencodeAcpNotification — session/update dispatch > available_commands_update consumed silently (session-init only) [0.07ms] (pass) reduceOpencodeAcpNotification — session/update dispatch > agent_message_chunk without text content adds a warning [0.08ms] (pass) reduceOpencodeAcpNotification — session/update dispatch > unknown method returns ignored without mutating state [0.10ms] (pass) reduceOpencodeAcpNotification — session/update dispatch > unknown sessionUpdate subtype returns ignored (forward-compat) [0.05ms] -(pass) reduceOpencodeAcpResponse — session/prompt terminal response > captures stopReason + usage from result [0.20ms] -(pass) reduceOpencodeAcpResponse — session/prompt terminal response > missing stopReason still marks turn complete [0.04ms] -(pass) reduceOpencodeAcpFrames — replay the Phase 0b captured turn > full one-word turn: 10 thought chunks + 1 message chunk + usage + response [0.36ms] -(pass) reduceOpencodeAcpFrames — replay the Phase 0b captured turn > thinking-only terminal turn (no agent_message_chunk) — replyText stays empty [0.14ms] +(pass) reduceOpencodeAcpResponse — session/prompt terminal response > captures stopReason + usage from result [0.23ms] +(pass) reduceOpencodeAcpResponse — session/prompt terminal response > missing stopReason still marks turn complete [0.07ms] +(pass) reduceOpencodeAcpFrames — replay the Phase 0b captured turn > full one-word turn: 10 thought chunks + 1 message chunk + usage + response [0.47ms] +(pass) reduceOpencodeAcpFrames — replay the Phase 0b captured turn > thinking-only terminal turn (no agent_message_chunk) — replyText stays empty [0.16ms] src/runtime/opencode-acp/child-env.test.ts: -(pass) buildOpencodeChildEnv — deny-by-default boundary > locks the exact hardened ancestor candidate set [0.10ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects sticky world-writable /tmp instead of silently degrading [4.22ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > passes only runtime/network allowlist and controls all state roots [22.04ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > safe inline policy disables every local tool without replacing provider/model [12.94ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > unsafe opt-in explicitly overrides the wizard's persisted safe policy [7.67ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > detects exact managed config sources across Linux, Windows, and macOS [1.12ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > safe runtime renders ordinary same-uid config through a strict allowlist [13.50ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > copies only blessed API auth fields into fresh data and keeps persistent state outside the child [14.74ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > never exposes planted persistent DB/log/cache/state/tmp descendants in safe or unsafe mode [19.77ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > removes a partially built launch tree when env construction fails [11.02ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > pre-spawn revalidation hard-fails when an ancestor discovery candidate appears [15.36ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > keeps active roots but reclaims a dead-owner crash root without following symlinks [38.20ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > reclaims dead-owner roots after the node workDir is deleted or recreated [78.82ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > a transient cleanup pathname swap is retried after child exit [26.99ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > a dead owner marker is retained while an orphan child still references the root [45.14ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > an exact exited-process identity exemption never hides a live descendant or PID mismatch [52.70ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects symlinks at workDir and every security-sensitive state layer [15.29ms] -(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects permissive modes and foreign ownership without repairing them [1.40ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > locks the exact hardened ancestor candidate set [0.13ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects sticky world-writable /tmp instead of silently degrading [4.59ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > passes only runtime/network allowlist and controls all state roots [25.34ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > safe inline policy disables every local tool without replacing provider/model [12.72ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > unsafe opt-in explicitly overrides the wizard's persisted safe policy [7.89ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > detects exact managed config sources across Linux, Windows, and macOS [1.24ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > safe runtime renders ordinary same-uid config through a strict allowlist [13.69ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > copies only blessed API auth fields into fresh data and keeps persistent state outside the child [14.90ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > never exposes planted persistent DB/log/cache/state/tmp descendants in safe or unsafe mode [22.34ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > removes a partially built launch tree when env construction fails [12.02ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > pre-spawn revalidation hard-fails when an ancestor discovery candidate appears [16.23ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > keeps active roots but reclaims a dead-owner crash root without following symlinks [38.22ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > reclaims dead-owner roots after the node workDir is deleted or recreated [58.41ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > a transient cleanup pathname swap is retried after child exit [24.03ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > a dead owner marker is retained while an orphan child still references the root [41.28ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > an exact exited-process identity exemption never hides a live descendant or PID mismatch [53.10ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects symlinks at workDir and every security-sensitive state layer [14.02ms] +(pass) buildOpencodeChildEnv — deny-by-default boundary > rejects permissive modes and foreign ownership without repairing them [1.12ms] src/runtime/opencode-acp/profile-state.test.ts: -(pass) OpenCode private profile state > loads, atomically updates, backs up, and writes a session [12.41ms] -(pass) OpenCode private profile state > a post-load config symlink cannot redirect session writeback [1.10ms] -(pass) OpenCode private profile state > boot refuses a config symlink before self-heal can write its target [1.05ms] -(pass) OpenCode private profile state > backup refuses a pre-planted .prev symlink [0.83ms] -(pass) OpenCode private profile state > runtime hint rejects suspicious config leaves for every runtime [1.10ms] +(pass) OpenCode private profile state > loads, atomically updates, backs up, and writes a session [12.35ms] +(pass) OpenCode private profile state > a post-load config symlink cannot redirect session writeback [1.04ms] +(pass) OpenCode private profile state > boot refuses a config symlink before self-heal can write its target [0.91ms] +(pass) OpenCode private profile state > backup refuses a pre-planted .prev symlink [0.79ms] +(pass) OpenCode private profile state > runtime hint rejects suspicious config leaves for every runtime [0.75ms] src/runtime/opencode-acp/client.test.ts: -(pass) OpencodeAcpClient — request/response correlation > request() resolves with the matching response's result [63.31ms] -(pass) OpencodeAcpClient — request/response correlation > error response rejects the promise with a shaped message [57.57ms] -(pass) OpencodeAcpClient — streaming notifications > emits 'notification' for every session/update frame [56.06ms] -(pass) OpencodeAcpClient — streaming notifications > id-carrying reverse requests get an explicit method-not-found response [55.90ms] -(pass) OpencodeAcpClient — process lifecycle > child exit rejects all pending requests [58.57ms] -(pass) OpencodeAcpClient — process lifecycle > isRunning flips false after stop() [1.82ms] -(pass) OpencodeAcpClient — process lifecycle > explicit child env is not merged with the client's process.env [55.85ms] +(pass) OpencodeAcpClient — request/response correlation > request() resolves with the matching response's result [69.04ms] +(pass) OpencodeAcpClient — request/response correlation > error response rejects the promise with a shaped message [59.37ms] +(pass) OpencodeAcpClient — streaming notifications > emits 'notification' for every session/update frame [64.63ms] +(pass) OpencodeAcpClient — streaming notifications > id-carrying reverse requests get an explicit method-not-found response [65.76ms] +(pass) OpencodeAcpClient — process lifecycle > child exit rejects all pending requests [62.48ms] +(pass) OpencodeAcpClient — process lifecycle > isRunning flips false after stop() [1.98ms] +(pass) OpencodeAcpClient — process lifecycle > explicit child env is not merged with the client's process.env [60.04ms] src/runtime/opencode-acp/runtime.test.ts: [opencode-acp] session/new — ses_test... -(pass) openOpencodeRuntime — cwd and tool policy > safe default keeps spawn + ACP session in one external launch workspace [126.67ms] +(pass) openOpencodeRuntime — cwd and tool policy > safe default keeps spawn + ACP session in one external launch workspace [135.73ms] [opencode-acp] session/new — ses_probe_au... -(pass) openOpencodeRuntime — cwd and tool policy > version probe root is credential-free and gone before runtime auth is materialized [130.36ms] +(pass) openOpencodeRuntime — cwd and tool policy > version probe root is credential-free and gone before runtime auth is materialized [129.81ms] [opencode-acp] session/load ok — resumed ses_existing... -(pass) openOpencodeRuntime — cwd and tool policy > safe session/load reuses the exact spawn PWD as its ACP cwd [132.06ms] +(pass) openOpencodeRuntime — cwd and tool policy > safe session/load reuses the exact spawn PWD as its ACP cwd [126.44ms] [opencode-acp] session/new — ses_test... -(pass) openOpencodeRuntime — cwd and tool policy > explicit unsafe flag restores project cwd and emits a trusted-task warning [134.39ms] +(pass) openOpencodeRuntime — cwd and tool policy > explicit unsafe flag restores project cwd and emits a trusted-task warning [121.83ms] [opencode-acp] session/new — ses_evidence... -(pass) openOpencodeRuntime — cwd and tool policy > reports submission before exact prompt-response consumption [134.60ms] +(pass) openOpencodeRuntime — cwd and tool policy > reports submission before exact prompt-response consumption [141.62ms] [opencode-acp] session/new — ses_test... -(pass) openOpencodeRuntime — opening lifecycle > normal stop removes the launch root and copied vendor auth [127.78ms] +(pass) openOpencodeRuntime — opening lifecycle > normal stop removes the launch root and copied vendor auth [140.00ms] [opencode-acp] session/new — ses_test... [opencode-acp] session/new — ses_test... [opencode-acp] session/new — ses_test... @@ -1392,695 +1386,695 @@ src/runtime/opencode-acp/runtime.test.ts: [opencode-acp] session/new — ses_test... [opencode-acp] session/new — ses_test... [opencode-acp] session/new — ses_test... -(pass) openOpencodeRuntime — opening lifecycle > repeated open/stop cycles do not accumulate launch roots [3148.65ms] -(pass) openOpencodeRuntime — opening lifecycle > an ancestor candidate planted by the version probe hard-fails before ACP spawn [66.49ms] -(pass) openOpencodeRuntime — opening lifecycle > package replacement after credential-free probe is rejected and runtime auth root is discarded [65.78ms] -(pass) openOpencodeRuntime — opening lifecycle > in-place binary self-modification after probe is rejected before credential spawn [64.48ms] -(pass) openOpencodeRuntime — opening lifecycle > production rejects canonical same-version packages below project cwd or node workDir [24.65ms] -(pass) openOpencodeRuntime — opening lifecycle > initialize failure force-kills the child before rejecting [128.04ms] -(pass) openOpencodeRuntime — opening lifecycle > onClient exposes a stalled-handshake child synchronously for shutdown [53.09ms] +(pass) openOpencodeRuntime — opening lifecycle > repeated open/stop cycles do not accumulate launch roots [3342.46ms] +(pass) openOpencodeRuntime — opening lifecycle > an ancestor candidate planted by the version probe hard-fails before ACP spawn [76.26ms] +(pass) openOpencodeRuntime — opening lifecycle > package replacement after credential-free probe is rejected and runtime auth root is discarded [69.19ms] +(pass) openOpencodeRuntime — opening lifecycle > in-place binary self-modification after probe is rejected before credential spawn [67.21ms] +(pass) openOpencodeRuntime — opening lifecycle > production rejects canonical same-version packages below project cwd or node workDir [28.72ms] +(pass) openOpencodeRuntime — opening lifecycle > initialize failure force-kills the child before rejecting [131.00ms] +(pass) openOpencodeRuntime — opening lifecycle > onClient exposes a stalled-handshake child synchronously for shutdown [62.34ms] [opencode-acp] session/new — ses_idle... -(pass) opencodeThink — failed-turn lifecycle > prompt idle timeout force-kills the child before rejecting [188.10ms] +(pass) opencodeThink — failed-turn lifecycle > prompt idle timeout force-kills the child before rejecting [184.66ms] [opencode-acp] session/new — ses_rescue_i... [opencode-acp] #383 thinking-only terminal turn (chunks=0 thoughtChunks=1) — re-prompting for plain-text final -(pass) opencodeThink — failed-turn lifecycle > a failed thinking-only rescue discards the child before returning [161.85ms] +(pass) opencodeThink — failed-turn lifecycle > a failed thinking-only rescue discards the child before returning [181.31ms] src/runtime/opencode-acp/binary.test.ts: -(pass) resolvePinnedOpencodeBinary > locks the non-root uid=gid umask-0002 compatibility policy [0.22ms] -(pass) resolvePinnedOpencodeBinary > accepts the canonical package entrypoint and probes it from the external cwd [44.18ms] -(pass) resolvePinnedOpencodeBinary > accepts an npm-style PATH shim but returns the canonical package binary [24.53ms] -(pass) resolvePinnedOpencodeBinary > rejects a same-version fake package inside the project before executing it [1.17ms] -(pass) resolvePinnedOpencodeBinary > rejects forged package metadata and noncanonical entrypoints [3.21ms] -(pass) resolvePinnedOpencodeBinary > rejects unsafe file, package-directory, ancestor, and owner modes [2.98ms] -(pass) resolvePinnedOpencodeBinary > still enforces exact --version output after package identity succeeds [24.23ms] -(pass) resolvePinnedOpencodeBinary > refuses a caller-selected version other than the vetted release pin [0.83ms] -(pass) resolvePinnedOpencodeBinary > rejects a same-version package in a monorepo ancestor before probing it [1.59ms] -(pass) resolvePinnedOpencodeBinary > discovers a workspace ancestor when the configured project leaf is absent [0.83ms] -(pass) resolvePinnedOpencodeBinary > launcher absolute path wins over a hostile search PATH [24.06ms] -(pass) resolvePinnedOpencodeBinary > rejects non-absolute overrides [0.18ms] +(pass) resolvePinnedOpencodeBinary > locks the non-root uid=gid umask-0002 compatibility policy [0.17ms] +(pass) resolvePinnedOpencodeBinary > accepts the canonical package entrypoint and probes it from the external cwd [53.85ms] +(pass) resolvePinnedOpencodeBinary > accepts an npm-style PATH shim but returns the canonical package binary [28.30ms] +(pass) resolvePinnedOpencodeBinary > rejects a same-version fake package inside the project before executing it [1.24ms] +(pass) resolvePinnedOpencodeBinary > rejects forged package metadata and noncanonical entrypoints [3.45ms] +(pass) resolvePinnedOpencodeBinary > rejects unsafe file, package-directory, ancestor, and owner modes [3.29ms] +(pass) resolvePinnedOpencodeBinary > still enforces exact --version output after package identity succeeds [25.32ms] +(pass) resolvePinnedOpencodeBinary > refuses a caller-selected version other than the vetted release pin [1.04ms] +(pass) resolvePinnedOpencodeBinary > rejects a same-version package in a monorepo ancestor before probing it [1.60ms] +(pass) resolvePinnedOpencodeBinary > discovers a workspace ancestor when the configured project leaf is absent [0.82ms] +(pass) resolvePinnedOpencodeBinary > launcher absolute path wins over a hostile search PATH [27.38ms] +(pass) resolvePinnedOpencodeBinary > rejects non-absolute overrides [0.33ms] src/runtime/grok-build-acp/events.test.ts: -(pass) Grok ACP event reducer — fixture replay > T6 prompt fixture accumulates final reply chunks [5.93ms] -(pass) Grok ACP event reducer — fixture replay > T8 session/load skips replay chunks from the previous turn [0.82ms] -(pass) Grok ACP event reducer — fixture replay > T9 abort + resume accumulates only the resumed turn reply [0.59ms] +(pass) Grok ACP event reducer — fixture replay > T6 prompt fixture accumulates final reply chunks [7.93ms] +(pass) Grok ACP event reducer — fixture replay > T8 session/load skips replay chunks from the previous turn [1.14ms] +(pass) Grok ACP event reducer — fixture replay > T9 abort + resume accumulates only the resumed turn reply [0.84ms] src/runtime/grok-build-acp/resume-hint.test.ts: -(pass) fetchUnresolvedOutbound > returns empty array when the hub has no outbound rows for this sender [0.49ms] -(pass) fetchUnresolvedOutbound > filters to only delivered/started status [0.38ms] -(pass) fetchUnresolvedOutbound > caps results at topN (preserves server-side recency order) [0.42ms] -(pass) fetchUnresolvedOutbound > forwards the sender alias and a sane limit to the listTasks hook (no node_id fallback path) [0.22ms] -(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — sends from_node_id ONLY when probe confirmed server supports it [0.21ms] -(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — without probe confirmation, never sends from_node_id (old-server safety) [0.19ms] -(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — when probe explicitly returned false, falls back even with node_id available [0.20ms] -(pass) fetchUnresolvedOutbound > #146 PR-4 — empty / null nodeId falls back to from_name path [0.27ms] -(pass) fetchUnresolvedOutbound > graceful fallback when list_tasks throws — returns empty, does not propagate [0.29ms] -(pass) fetchUnresolvedOutbound > graceful fallback for malformed payloads — non-array tasks [0.54ms] -(pass) fetchUnresolvedOutbound > clamps absurd opts: topN > 50 is capped, limit > 100 is capped [0.23ms] -(pass) fetchUnresolvedOutbound > 二审 — drops rows whose from_node_id does not match ours (server bug defence) [0.33ms] -(pass) fetchUnresolvedOutbound > 二审 — when row has no from_node_id, falls back to from_name match [0.33ms] -(pass) fetchUnresolvedOutbound > 二审 — drops rows with NEITHER from_node_id nor from_name (conservative) [0.34ms] -(pass) fetchUnresolvedOutbound > 二审 — prefers from_node_id over from_name when both present (handles rename correctly) [0.29ms] +(pass) fetchUnresolvedOutbound > returns empty array when the hub has no outbound rows for this sender [0.67ms] +(pass) fetchUnresolvedOutbound > filters to only delivered/started status [0.45ms] +(pass) fetchUnresolvedOutbound > caps results at topN (preserves server-side recency order) [0.70ms] +(pass) fetchUnresolvedOutbound > forwards the sender alias and a sane limit to the listTasks hook (no node_id fallback path) [0.29ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — sends from_node_id ONLY when probe confirmed server supports it [0.26ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — without probe confirmation, never sends from_node_id (old-server safety) [0.29ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 二审 — when probe explicitly returned false, falls back even with node_id available [0.23ms] +(pass) fetchUnresolvedOutbound > #146 PR-4 — empty / null nodeId falls back to from_name path [0.33ms] +(pass) fetchUnresolvedOutbound > graceful fallback when list_tasks throws — returns empty, does not propagate [0.33ms] +(pass) fetchUnresolvedOutbound > graceful fallback for malformed payloads — non-array tasks [0.34ms] +(pass) fetchUnresolvedOutbound > clamps absurd opts: topN > 50 is capped, limit > 100 is capped [0.20ms] +(pass) fetchUnresolvedOutbound > 二审 — drops rows whose from_node_id does not match ours (server bug defence) [0.29ms] +(pass) fetchUnresolvedOutbound > 二审 — when row has no from_node_id, falls back to from_name match [0.38ms] +(pass) fetchUnresolvedOutbound > 二审 — drops rows with NEITHER from_node_id nor from_name (conservative) [0.33ms] +(pass) fetchUnresolvedOutbound > 二审 — prefers from_node_id over from_name when both present (handles rename correctly) [0.28ms] (pass) fetchUnresolvedOutbound > 二审 — when WE have no nodeId, identity check uses from_name only [0.29ms] -(pass) buildResumeHint > returns null for an empty list — caller skips the prepend with no noise [0.13ms] -(pass) buildResumeHint > single task is listed with target alias + task id (8-char) + content preview [0.31ms] -(pass) buildResumeHint > hint wording: explicit do-NOT-redispatch instruction in both Chinese phrasing and English keyword [0.17ms] -(pass) buildResumeHint > hint promotes send_message as the legitimate alternative for status check-ins [0.11ms] -(pass) buildResumeHint > hint mentions server-side dedup as a safety net but tells the LLM not to rely on it [0.11ms] -(pass) buildResumeHint > hint avoids to-do framing — would push the LLM into reprocessing [0.13ms] -(pass) buildResumeHint > long content is truncated to 120 chars including ellipsis [0.20ms] +(pass) buildResumeHint > returns null for an empty list — caller skips the prepend with no noise [0.11ms] +(pass) buildResumeHint > single task is listed with target alias + task id (8-char) + content preview [0.35ms] +(pass) buildResumeHint > hint wording: explicit do-NOT-redispatch instruction in both Chinese phrasing and English keyword [0.21ms] +(pass) buildResumeHint > hint promotes send_message as the legitimate alternative for status check-ins [0.09ms] +(pass) buildResumeHint > hint mentions server-side dedup as a safety net but tells the LLM not to rely on it [0.10ms] +(pass) buildResumeHint > hint avoids to-do framing — would push the LLM into reprocessing [0.14ms] +(pass) buildResumeHint > long content is truncated to 120 chars including ellipsis [0.19ms] (pass) buildResumeHint > content with triple-backticks is defanged (prevents code-fence injection from resumed task body) [0.07ms] -(pass) buildResumeHint > missing fields fall back gracefully without throwing [0.12ms] -(pass) buildResumeHint > multi-task list preserves order from the input (server-side recency) [0.11ms] +(pass) buildResumeHint > missing fields fall back gracefully without throwing [0.05ms] +(pass) buildResumeHint > multi-task list preserves order from the input (server-side recency) [0.09ms] src/runtime/grok-build-acp/client.test.ts: -(pass) GrokAcpClient > starts the ACP server as `grok agent stdio` without inventing a model flag [67.36ms] -(pass) GrokAcpClient > handles ACP server-to-client fs and permission requests [65.77ms] -(pass) GrokAcpClient > coerces non-integer fs error codes to numeric JSON-RPC codes [63.05ms] -(pass) GrokAcpClient > requestWithIdleTimeout does not fire while agent is streaming notifications [790.82ms] -(pass) GrokAcpClient > requestWithIdleTimeout fires when agent goes silent past threshold [1206.12ms] -(pass) GrokAcpClient > preserves valid integer error codes [59.53ms] +(pass) GrokAcpClient > starts the ACP server as `grok agent stdio` without inventing a model flag [69.67ms] +(pass) GrokAcpClient > handles ACP server-to-client fs and permission requests [90.45ms] +(pass) GrokAcpClient > coerces non-integer fs error codes to numeric JSON-RPC codes [74.79ms] +(pass) GrokAcpClient > requestWithIdleTimeout does not fire while agent is streaming notifications [798.24ms] +(pass) GrokAcpClient > requestWithIdleTimeout fires when agent goes silent past threshold [1208.89ms] +(pass) GrokAcpClient > preserves valid integer error codes [75.76ms] src/runtime/grok-build-acp/timeout-resolve.test.ts: -(pass) resolveGrokAcpTimeout > env wins over flags and default (mirrors cli.ts precedence) [1.99ms] -(pass) resolveGrokAcpTimeout > flag wins over default when env is unset [0.10ms] +(pass) resolveGrokAcpTimeout > env wins over flags and default (mirrors cli.ts precedence) [2.03ms] +(pass) resolveGrokAcpTimeout > flag wins over default when env is unset [0.07ms] (pass) resolveGrokAcpTimeout > flag string is parsed (config.json values arrive as strings or numbers) [0.06ms] (pass) resolveGrokAcpTimeout > default fires when neither env nor flag is set [0.11ms] -(pass) resolveGrokAcpTimeout > empty string env is ignored (operator unset the var) [0.05ms] -(pass) resolveGrokAcpTimeout > null and empty flag are ignored — falls through to default [0.05ms] -(pass) resolveGrokAcpTimeout > non-numeric / negative / NaN inputs fall through (the silent-default trap) [0.09ms] +(pass) resolveGrokAcpTimeout > empty string env is ignored (operator unset the var) [0.06ms] +(pass) resolveGrokAcpTimeout > null and empty flag are ignored — falls through to default [0.06ms] +(pass) resolveGrokAcpTimeout > non-numeric / negative / NaN inputs fall through (the silent-default trap) [0.08ms] src/runtime/grok-build-acp/runtime.test.ts: -(pass) runGrokAcpTurn runtime evidence > separates prompt submission from exact prompt-response consumption [88.87ms] +(pass) runGrokAcpTurn runtime evidence > separates prompt submission from exact prompt-response consumption [84.00ms] src/runtime/opencode-copresence/inbox-wiring.test.ts: -(pass) OpenCode copresence CommHub message wiring > work and informational drains are independent lanes [0.52ms] -(pass) OpenCode copresence CommHub message wiring > new_message SSE uses a non-blocking informational lane [0.19ms] -(pass) OpenCode copresence CommHub message wiring > message is displayed as a non-replying TUI notification in the fast drain [0.21ms] -(pass) OpenCode copresence CommHub message wiring > the task drain does not claim OpenCode copresence messages [0.19ms] -(pass) OpenCode copresence CommHub message wiring > network tasks pass their authenticated sender into the shared TUI turn [0.17ms] -(pass) OpenCode copresence CommHub message wiring > startup and SSE reconnect both recover pending informational messages [0.30ms] -(pass) OpenCode copresence CommHub message wiring > runtime startup is single-flight and shutdown waits for an in-flight open [0.14ms] -(pass) OpenCode copresence CommHub message wiring > tmux SIGHUP enters the same cleanup path as SIGTERM [0.38ms] +(pass) OpenCode copresence CommHub message wiring > work and informational drains are independent lanes [0.71ms] +(pass) OpenCode copresence CommHub message wiring > new_message SSE uses a non-blocking informational lane [0.22ms] +(pass) OpenCode copresence CommHub message wiring > message is displayed as a non-replying TUI notification in the fast drain [0.20ms] +(pass) OpenCode copresence CommHub message wiring > the task drain does not claim OpenCode copresence messages [0.16ms] +(pass) OpenCode copresence CommHub message wiring > network tasks pass their authenticated sender into the shared TUI turn [0.16ms] +(pass) OpenCode copresence CommHub message wiring > startup and SSE reconnect both recover pending informational messages [0.31ms] +(pass) OpenCode copresence CommHub message wiring > runtime startup is single-flight and shutdown waits for an in-flight open [0.13ms] +(pass) OpenCode copresence CommHub message wiring > tmux SIGHUP enters the same cleanup path as SIGTERM [0.36ms] src/runtime/opencode-copresence/runtime.test.ts: -(pass) OpenCode native serve+attach copresence > requires an explicit provider/model for production copresence [0.39ms] -(pass) OpenCode native serve+attach copresence > requires an explicit provider/model at the vetted launch seam too [1.85ms] -(pass) OpenCode native serve+attach copresence > wires one token-bound CommHub MCP without reopening local tools [2.00ms] -(pass) OpenCode native serve+attach copresence > uses one authenticated loopback session for FIFO network turns and emits an owner-only attach launcher [184.08ms] -(pass) OpenCode native serve+attach copresence > shows the network sender in both the toast title and message body [181.72ms] -(pass) OpenCode native serve+attach copresence > shows the normalized network-task sender in the shared TUI turn [200.67ms] -(pass) OpenCode native serve+attach copresence > waits for an already-busy human session before injecting a network turn [618.79ms] -(pass) OpenCode native serve+attach copresence > refuses a reply owned by a human turn that won the idle-to-submit race [207.23ms] -(pass) OpenCode native serve+attach copresence > uses OpenCode's ascending message ID shape across sequential network turns [229.07ms] -(pass) OpenCode native serve+attach copresence > does not treat a missing session status and missing session record as idle [437.97ms] +(pass) OpenCode native serve+attach copresence > requires an explicit provider/model for production copresence [0.46ms] +(pass) OpenCode native serve+attach copresence > requires an explicit provider/model at the vetted launch seam too [1.94ms] +(pass) OpenCode native serve+attach copresence > wires one token-bound CommHub MCP without reopening local tools [1.91ms] +(pass) OpenCode native serve+attach copresence > uses one authenticated loopback session for FIFO network turns and emits an owner-only attach launcher [242.05ms] +(pass) OpenCode native serve+attach copresence > shows the network sender in both the toast title and message body [175.92ms] +(pass) OpenCode native serve+attach copresence > shows the normalized network-task sender in the shared TUI turn [205.99ms] +(pass) OpenCode native serve+attach copresence > waits for an already-busy human session before injecting a network turn [618.97ms] +(pass) OpenCode native serve+attach copresence > refuses a reply owned by a human turn that won the idle-to-submit race [150.48ms] +(pass) OpenCode native serve+attach copresence > uses OpenCode's ascending message ID shape across sequential network turns [247.48ms] +(pass) OpenCode native serve+attach copresence > does not treat a missing session status and missing session record as idle [488.82ms] (pass) OpenCode native serve+attach copresence > binds teardown authority to a detached pid, pgrp, and process start ticks [2.01ms] src/runtime/codex-app-server/session-manager.test.ts: -(pass) createCodexSessionManager > the production Codex inbox path is wired through the shared holder [1.09ms] -(pass) createCodexSessionManager > concurrent Dashboard handlers share one complete open attempt [0.71ms] -(pass) createCodexSessionManager > a rejected open is cleared and the next row can retry [0.29ms] -(pass) createCodexSessionManager > stopped and explicitly invalidated sessions are never reused [0.29ms] -(pass) createCodexSessionManager > a session that dies during bootstrap is not published [0.14ms] +(pass) createCodexSessionManager > the production Codex inbox path is wired through the shared holder [1.16ms] +(pass) createCodexSessionManager > concurrent Dashboard handlers share one complete open attempt [0.89ms] +(pass) createCodexSessionManager > a rejected open is cleared and the next row can retry [0.43ms] +(pass) createCodexSessionManager > stopped and explicitly invalidated sessions are never reused [0.30ms] +(pass) createCodexSessionManager > a session that dies during bootstrap is not published [0.21ms] src/runtime/codex-app-server/runtime.test.ts: -(pass) buildOwnedAppServerArgs > no opts → bare app-server (codex defaults apply) [0.11ms] -(pass) buildOwnedAppServerArgs > approval_policy only → single -c override before --listen [0.05ms] -(pass) buildOwnedAppServerArgs > sandbox_mode only → single -c override [0.08ms] -(pass) buildOwnedAppServerArgs > auto-approve posture (never + danger-full-access) → both overrides, policy first [0.06ms] -(pass) buildOwnedAppServerArgs > commhubMcpUrl → adds url + bearer-token-env-var -c overrides [0.06ms] +(pass) buildOwnedAppServerArgs > no opts → bare app-server (codex defaults apply) [0.17ms] +(pass) buildOwnedAppServerArgs > approval_policy only → single -c override before --listen [0.08ms] +(pass) buildOwnedAppServerArgs > sandbox_mode only → single -c override [0.04ms] +(pass) buildOwnedAppServerArgs > auto-approve posture (never + danger-full-access) → both overrides, policy first [0.05ms] +(pass) buildOwnedAppServerArgs > commhubMcpUrl → adds url + bearer-token-env-var -c overrides [0.05ms] (pass) buildOwnedAppServerArgs > the CommHub bearer TOKEN never appears in argv (only the env-var NAME) [0.12ms] -(pass) buildOwnedAppServerArgs > full production posture (yolo + commhub MCP) → stable order, --listen last [0.11ms] -(pass) recoverSharedTurnOnAttach > invokes persisted active-turn recovery before shared runtime is returned [0.48ms] -(pass) recoverSharedTurnOnAttach > history read failure is visible and never reported as steerable [0.27ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > FIFO admission reports neither submission nor consumption [21.06ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > exact runtime submission and task_started report each level once [0.96ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > exact task activity resets the response idle deadline for a long-running turn [71.23ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > activity from another task cannot keep a silent owned task alive [57.04ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > a started task whose client identity never confirms has a bounded, distinct response timeout [27.69ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > a never-started FIFO task has its own finite, distinct queue deadline [80.85ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > lost task_started after FIFO removal remains finite [80.92ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > a failed start or steer requeued after the queue deadline cannot leave a ghost row [113.94ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > queued wait does not consume the model-response timeout budget [91.58ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > another task starting cannot arm this task's timeout [116.04ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > resolves from authoritative reconciliation when turn/completed is missed [7.41ms] -(pass) codexAppServerThink — terminal-event reconciliation watchdog > forwards the authenticated Dashboard steering decision to the bridge [5.43ms] -(pass) codexAppServerReplyOrThrow > failed bridge outcomes enter processTask's thrown failure path [0.28ms] -(pass) codexAppServerReplyOrThrow > successful empty replies preserve the existing fallback [0.06ms] +(pass) buildOwnedAppServerArgs > full production posture (yolo + commhub MCP) → stable order, --listen last [0.12ms] +(pass) recoverSharedTurnOnAttach > invokes persisted active-turn recovery before shared runtime is returned [0.50ms] +(pass) recoverSharedTurnOnAttach > history read failure is visible and never reported as steerable [0.29ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > FIFO admission reports neither submission nor consumption [22.37ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > exact runtime submission and task_started report each level once [0.94ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > exact task activity resets the response idle deadline for a long-running turn [74.20ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > activity from another task cannot keep a silent owned task alive [57.66ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > a started task whose client identity never confirms has a bounded, distinct response timeout [27.80ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > a never-started FIFO task has its own finite, distinct queue deadline [81.06ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > lost task_started after FIFO removal remains finite [81.13ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > a failed start or steer requeued after the queue deadline cannot leave a ghost row [113.77ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > queued wait does not consume the model-response timeout budget [86.27ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > another task starting cannot arm this task's timeout [114.75ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > resolves from authoritative reconciliation when turn/completed is missed [7.73ms] +(pass) codexAppServerThink — terminal-event reconciliation watchdog > forwards the authenticated Dashboard steering decision to the bridge [5.60ms] +(pass) codexAppServerReplyOrThrow > failed bridge outcomes enter processTask's thrown failure path [0.30ms] +(pass) codexAppServerReplyOrThrow > successful empty replies preserve the existing fallback [0.12ms] 1281 pass 0 fail 4365 expect() calls -Ran 1281 tests across 91 files. [114.57s] +Ran 1281 tests across 91 files. [117.17s] [L0b] every agent-node/tests file, dispatched by kind tests_dir_executed=6 tests_dir_discovered=6 tests_dir_failed=0 [L1] witnessed-red: disconnect readable attachment content from runtime MUTATION_RED readable-attachment-runtime-disconnected rc=1 RESULT: PASS ``` -### test745 +## test745(agent-network) ``` # test745 — complete agent-network unit domain -source_commit=92d9612949a4207eae4facab2b337c1f23de65e0 +source_commit=a4fd375f2b2e4f35e1a1dcea0a5f093f1439796a bun=1.3.14 node=v22.23.2 git=git version 2.39.5 uid=1000 test_files=46 [L0] full agent-network/src unit suite as non-root bun test v1.3.14 (0d9b296a) src/cli-args.test.ts: -(pass) CLI argument parsing > pins the complete presence-only flag set [0.22ms] -(pass) CLI argument parsing > --accept-dev-channels does not swallow a following positional operand [0.44ms] -(pass) CLI argument parsing > --accept-dev-channels works after a positional operand [0.09ms] -(pass) CLI argument parsing > --dev-open does not swallow a following positional operand [0.02ms] -(pass) CLI argument parsing > --dev-open works after a positional operand [0.02ms] +(pass) CLI argument parsing > pins the complete presence-only flag set [0.16ms] +(pass) CLI argument parsing > --accept-dev-channels does not swallow a following positional operand [0.35ms] +(pass) CLI argument parsing > --accept-dev-channels works after a positional operand [0.07ms] +(pass) CLI argument parsing > --dev-open does not swallow a following positional operand [0.01ms] +(pass) CLI argument parsing > --dev-open works after a positional operand [0.01ms] (pass) CLI argument parsing > --dry-run does not swallow a following positional operand (pass) CLI argument parsing > --dry-run works after a positional operand (pass) CLI argument parsing > --follow does not swallow a following positional operand (pass) CLI argument parsing > --follow works after a positional operand (pass) CLI argument parsing > --no-auto-self does not swallow a following positional operand -(pass) CLI argument parsing > --no-auto-self works after a positional operand [0.01ms] +(pass) CLI argument parsing > --no-auto-self works after a positional operand (pass) CLI argument parsing > --no-yolo does not swallow a following positional operand (pass) CLI argument parsing > --no-yolo works after a positional operand (pass) CLI argument parsing > --resume-latest does not swallow a following positional operand (pass) CLI argument parsing > --resume-latest works after a positional operand -(pass) CLI argument parsing > --self does not swallow a following positional operand [0.01ms] +(pass) CLI argument parsing > --self does not swallow a following positional operand (pass) CLI argument parsing > --self works after a positional operand (pass) CLI argument parsing > --f does not swallow a following positional operand [0.01ms] (pass) CLI argument parsing > --f works after a positional operand -(pass) CLI argument parsing > presence-only flags do not accept an explicit true or false value [0.09ms] -(pass) CLI argument parsing > value flags, repeatable flags, and multiple positionals retain their behavior [0.12ms] -(pass) CLI argument parsing > key=value remains unsupported and is treated as the complete key [0.07ms] +(pass) CLI argument parsing > presence-only flags do not accept an explicit true or false value [0.05ms] +(pass) CLI argument parsing > value flags, repeatable flags, and multiple positionals retain their behavior [0.09ms] +(pass) CLI argument parsing > key=value remains unsupported and is treated as the complete key [0.06ms] src/normalize-runtime.test.ts: -(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > legacy normalization: unknown string → claude-agent-sdk [1.05ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > legacy normalization: unknown string → claude-agent-sdk [0.16ms] (pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > empty string → claude-agent-sdk [0.04ms] (pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > undefined (no arg) → claude-agent-sdk [0.04ms] (pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > undefined profile arg → claude-agent-sdk [0.04ms] -(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > profile with missing runtime field → claude-agent-sdk [0.04ms] -(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > profile with empty-string runtime field → claude-agent-sdk [0.03ms] -(pass) normalizeRuntimeStrict — execution boundaries fail closed > missing and empty runtime still select the documented default [0.13ms] -(pass) normalizeRuntimeStrict — execution boundaries fail closed > canonical names and supported aliases are accepted [0.05ms] -(pass) normalizeRuntimeStrict — execution boundaries fail closed > a non-empty unknown runtime is rejected [0.21ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > profile with missing runtime field → claude-agent-sdk [0.07ms] +(pass) normalizeRuntime — fallback default is claude-agent-sdk (Vincent no-Max) > profile with empty-string runtime field → claude-agent-sdk [0.04ms] +(pass) normalizeRuntimeStrict — execution boundaries fail closed > missing and empty runtime still select the documented default [0.11ms] +(pass) normalizeRuntimeStrict — execution boundaries fail closed > canonical names and supported aliases are accepted [0.04ms] +(pass) normalizeRuntimeStrict — execution boundaries fail closed > a non-empty unknown runtime is rejected [0.19ms] (pass) normalizeRuntime — explicit choices are preserved > explicit 'claude-code-cli' → claude-code-cli (operator opt-in still works) [0.03ms] (pass) normalizeRuntime — explicit choices are preserved > explicit 'claude-agent-sdk' → claude-agent-sdk [0.02ms] (pass) normalizeRuntime — explicit choices are preserved > alias 'claude' → claude-agent-sdk (existing canonicalization) [0.02ms] -(pass) normalizeRuntime — explicit choices are preserved > alias 'claude-sdk' → claude-agent-sdk [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > alias 'agent-sdk' (string form) → claude-agent-sdk [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > 'codex' / 'codex-sdk' → codex-sdk [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > 'grok' / 'grok-build' / 'grok-build-acp' → grok-build-acp [0.04ms] -(pass) normalizeRuntime — explicit choices are preserved > explicit Grok co-presence names → grok-build-cli [0.05ms] -(pass) normalizeRuntime — explicit choices are preserved > explicit 'opencode-cli' → opencode-cli (canonical launcher name) [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > alias 'opencode' → opencode-cli (short form) [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='opencode-cli' → opencode-cli [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='opencode' → opencode-cli [0.04ms] -(pass) normalizeRuntime — explicit choices are preserved > explicit 'codex-app-server' → codex-app-server [0.02ms] -(pass) normalizeRuntime — explicit choices are preserved > alias 'codex-tui' → codex-app-server [0.22ms] -(pass) normalizeRuntime — explicit choices are preserved > alias 'codex-appserver' → codex-app-server [0.04ms] -(pass) normalizeRuntime — explicit choices are preserved > 'codex-sdk' still → codex-sdk (not shadowed by the app-server branch) [0.03ms] -(pass) normalizeRuntime — explicit choices are preserved > 'codex' still → codex-sdk (legacy short alias unchanged) [0.02ms] -(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='codex-app-server' → codex-app-server [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'claude-sdk' → claude-agent-sdk [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'agent-sdk' (string form) → claude-agent-sdk [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > 'codex' / 'codex-sdk' → codex-sdk [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > 'grok' / 'grok-build' / 'grok-build-acp' → grok-build-acp [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit Grok co-presence names → grok-build-cli [0.07ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit 'opencode-cli' → opencode-cli (canonical launcher name) [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'opencode' → opencode-cli (short form) [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='opencode-cli' → opencode-cli [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='opencode' → opencode-cli [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > explicit 'codex-app-server' → codex-app-server [0.01ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'codex-tui' → codex-app-server [0.18ms] +(pass) normalizeRuntime — explicit choices are preserved > alias 'codex-appserver' → codex-app-server [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > 'codex-sdk' still → codex-sdk (not shadowed by the app-server branch) [0.02ms] +(pass) normalizeRuntime — explicit choices are preserved > 'codex' still → codex-sdk (legacy short alias unchanged) [0.03ms] +(pass) normalizeRuntime — explicit choices are preserved > profile with runtime='codex-app-server' → codex-app-server [0.02ms] (pass) normalizeRuntime — profile object paths > profile with runtime='claude-code-cli' → claude-code-cli (explicit, preserved) [0.02ms] (pass) normalizeRuntime — profile object paths > profile with runtime='agent-sdk' + codexRuntime='codex' → codex-sdk (legacy hybrid) [0.03ms] (pass) normalizeRuntime — profile object paths > profile with runtime='agent-sdk' + no codexRuntime → claude-agent-sdk [0.03ms] -(pass) normalizeRuntime — profile object paths > legacy profile normalization keeps unknown → default for display/migration [0.61ms] +(pass) normalizeRuntime — profile object paths > legacy profile normalization keeps unknown → default for display/migration [0.57ms] src/batch-workdir-wiring.test.ts: -(pass) batch workdir wiring > normalizes create workdir before mkdir or chdir [0.66ms] -(pass) batch workdir wiring > normalizes cleanup workdir before filesystem mutation [0.29ms] +(pass) batch workdir wiring > normalizes create workdir before mkdir or chdir [0.56ms] +(pass) batch workdir wiring > normalizes cleanup workdir before filesystem mutation [0.31ms] src/top-level-help-contract.test.ts: -(pass) top-level help matches the implemented command parsers > advertises only the implemented config and batch shapes [183.53ms] -(pass) top-level help matches the implemented command parsers > includes the provider required by opencode auth-login [222.24ms] +(pass) top-level help matches the implemented command parsers > advertises only the implemented config and batch shapes [294.14ms] +(pass) top-level help matches the implemented command parsers > includes the provider required by opencode auth-login [209.84ms] src/opencode-pin.test.ts: -(pass) opencode-pin — built-in fallback > release builtin pin is the revalidated opencode-ai@1.18.1 [0.30ms] -(pass) opencode-pin — built-in fallback > returns the built-in constant when no override file exists [0.48ms] -(pass) opencode-pin — built-in fallback > missing/untrusted package hint preserves detail and exact install command [0.24ms] -(pass) opencode-pin — override file write + read round-trip > a smoke marker for the exact release pin is recognized [1.43ms] -(pass) opencode-pin — override file write + read round-trip > a locally-smoked different version cannot override the release pin [0.35ms] -(pass) opencode-pin — validation refuses malformed / unvalidated overrides > hand-edited file with version but NO smokePassedAt → falls back to built-in [0.35ms] -(pass) opencode-pin — validation refuses malformed / unvalidated overrides > version string doesn't match semver → falls back to built-in [0.29ms] -(pass) opencode-pin — validation refuses malformed / unvalidated overrides > smokePassedAt not an ISO timestamp → falls back to built-in [0.32ms] -(pass) opencode-pin — validation refuses malformed / unvalidated overrides > malformed JSON → falls back to built-in without throwing [0.42ms] +(pass) opencode-pin — built-in fallback > release builtin pin is the revalidated opencode-ai@1.18.1 [0.25ms] +(pass) opencode-pin — built-in fallback > returns the built-in constant when no override file exists [0.44ms] +(pass) opencode-pin — built-in fallback > missing/untrusted package hint preserves detail and exact install command [0.25ms] +(pass) opencode-pin — override file write + read round-trip > a smoke marker for the exact release pin is recognized [1.22ms] +(pass) opencode-pin — override file write + read round-trip > a locally-smoked different version cannot override the release pin [0.36ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > hand-edited file with version but NO smokePassedAt → falls back to built-in [0.29ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > version string doesn't match semver → falls back to built-in [0.53ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > smokePassedAt not an ISO timestamp → falls back to built-in [0.29ms] +(pass) opencode-pin — validation refuses malformed / unvalidated overrides > malformed JSON → falls back to built-in without throwing [0.36ms] src/tmux-attach.test.ts: -(pass) tmux attach resolution > parses opaque IDs and Unicode names [1.01ms] -(pass) tmux attach resolution > selects the exact TUI instead of prefix siblings [0.27ms] -(pass) tmux attach resolution > does not fall back to a bridge or node session [0.08ms] +(pass) tmux attach resolution > parses opaque IDs and Unicode names [1.23ms] +(pass) tmux attach resolution > selects the exact TUI instead of prefix siblings [0.26ms] +(pass) tmux attach resolution > does not fall back to a bridge or node session [0.07ms] src/owner-env-file.test.ts: -(pass) loadOwnerOnlyEnvFile > loads the isolated commhub credential without overriding explicit identity [1.15ms] -(pass) loadOwnerOnlyEnvFile > rejects relative, broad-mode, and symlinked credential files [0.89ms] +(pass) loadOwnerOnlyEnvFile > loads the isolated commhub credential without overriding explicit identity [1.36ms] +(pass) loadOwnerOnlyEnvFile > rejects relative, broad-mode, and symlinked credential files [3.69ms] src/opencode-owner-mode.test.ts: -(pass) OpenCode owner/mode policy > accepts umask-0002 modes only for a non-root uid=gid layout [0.17ms] -(pass) OpenCode owner/mode policy > always rejects world write and keeps root/foreign ownership strict [0.10ms] +(pass) OpenCode owner/mode policy > accepts umask-0002 modes only for a non-root uid=gid layout [0.23ms] +(pass) OpenCode owner/mode policy > always rejects world write and keeps root/foreign ownership strict [0.13ms] src/channel-attachments.test.ts: -(pass) Claude channel attachments > pins the readable extension allowlist as an exact value set [1.63ms] -(pass) Claude channel attachments > cache roots are alias-isolated even for path-shaped aliases [0.48ms] -(pass) Claude channel attachments > downloads an authenticated Dashboard PNG and surfaces an owner-local Read path [5.24ms] -(pass) Claude channel attachments > downloads an authenticated non-image file for the Read-capable channel [1.59ms] -(pass) Claude channel attachments > does not fetch or inject a non-allowlisted file type [0.24ms] -(pass) Claude channel attachments > download failure preserves the original text and exposes no token [0.70ms] -(pass) Claude channel attachments > rejects traversal-shaped file ids before any fetch [0.24ms] -(pass) Claude channel attachments > does not trust a sender-provided local path [0.65ms] +(pass) Claude channel attachments > pins the readable extension allowlist as an exact value set [0.46ms] +(pass) Claude channel attachments > cache roots are alias-isolated even for path-shaped aliases [0.33ms] +(pass) Claude channel attachments > downloads an authenticated Dashboard PNG and surfaces an owner-local Read path [4.13ms] +(pass) Claude channel attachments > downloads an authenticated non-image file for the Read-capable channel [1.28ms] +(pass) Claude channel attachments > does not fetch or inject a non-allowlisted file type [0.60ms] +(pass) Claude channel attachments > download failure preserves the original text and exposes no token [0.76ms] +(pass) Claude channel attachments > rejects traversal-shaped file ids before any fetch [0.26ms] +(pass) Claude channel attachments > does not trust a sender-provided local path [0.59ms] src/codex-model-default.test.ts: -(pass) Codex model defaults > all Codex creation runtime spellings use the supported default [0.17ms] -(pass) Codex model defaults > shared Codex choice catalog has one supported default [0.17ms] +(pass) Codex model defaults > all Codex creation runtime spellings use the supported default [0.18ms] +(pass) Codex model defaults > shared Codex choice catalog has one supported default [0.10ms] src/copresence-identity.test.ts: -(pass) Test 1: UUID round-trip > writeMarker persists exactly the provided uuid (single source of truth) [7.97ms] +(pass) Test 1: UUID round-trip > writeMarker persists exactly the provided uuid (single source of truth) [4.47ms] (pass) Test 1: UUID round-trip > writeMarker refuses empty uuid (guard against silent regeneration) [0.42ms] -(pass) Test 1: UUID round-trip > writeMarker refuses non-string uuid [0.32ms] -(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when listAllPids throws [1.06ms] -(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when a member's environ read throws [0.72ms] -(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when a stat read throws [0.36ms] -(pass) Test 3: foreign member in PGID → SKIP > group with unmarked co-resident refuses homogeneity [0.31ms] -(pass) Test 3: foreign member in PGID → SKIP > group where every member carries the marker is ok [0.31ms] -(pass) Test 4: main-dead-child-alive (environ scan is authority) > scan finds workers even when marker's stored pids are gone [0.81ms] -(pass) Test 5: child setsid → new PGID > detached child grouped under its current pgid, not marker's stored pgid [0.32ms] -(pass) Test 6: PID-reuse defense is the boot_id + environ-scan invariant > environ scan only returns pids whose current environ carries the uuid [0.29ms] -(pass) Test 7: partial-start rollback (marker gate) > MISSING marker after partial start prevents any process action [0.41ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > null body → SCHEMA_INVALID (no TypeError from `in` operator) [0.59ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > bare number → SCHEMA_INVALID [0.48ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > empty array → SCHEMA_INVALID [0.80ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > empty object → SCHEMA_INVALID (missing required fields) [0.65ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > wrong types in schema → SCHEMA_INVALID [0.56ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > syntactically invalid JSON → PARSE_ERROR [0.52ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > wrong mode → WRONG_MODE (even with valid JSON) [0.55ms] -(pass) Test 8: malformed marker → structured refuse (never throws) > symlink → SYMLINK (refuses to follow) [0.51ms] -(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > NOT_REGULAR: directory at marker path with mode 0600 (skips SYMLINK+WRONG_MODE) [0.51ms] -(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > OWNER_MISMATCH: valid marker file whose lstat.uid differs from process.getuid() (SECURITY CRITICAL) [3.01ms] -(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > STALE_BOOT_ID: valid schema but boot_id differs from current /proc boot_id [0.99ms] -(pass) Test 9: self-context refuses stop from within the tree > caller's own environ carrying the marker is detected [0.48ms] -(pass) Test 9: self-context refuses stop from within the tree > ancestor carrying the marker is detected via PPID walk [0.46ms] -(pass) Test 9: self-context refuses stop from within the tree > clean caller (no marker in ancestry) returns self=false [0.32ms] -(pass) Test 10: non-copresence codex-app-server → legacy path (zero diff) > readMarker returns MISSING for an ordinary codex-app-server node dir [0.55ms] -(pass) Test 11: 二次 stop is idempotent (MISSING = already stopped) > 2nd read after successful removeMarker returns MISSING (no side effects) [3.48ms] -(pass) Test 11: 二次 stop is idempotent (MISSING = already stopped) > removeMarker on already-missing marker does not throw [0.31ms] -(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > verified groups get SIGTERM, still-alive groups then get SIGKILL [6.72ms] -(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > groups with foreign members are SKIPPED, never signaled [2.51ms] -(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > no marker-carrying pids anywhere → immediate success (idempotent) [0.54ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > other-user EACCES on environ → skip that pid (expected, not fail) [0.64ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Defect A defense: own-uid EACCES pid IN SCOPE (anchored) → reap refuses to delete marker [0.97ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 1: own-uid EACCES pid OUT OF SCOPE → informational only, teardown still succeeds [0.88ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 1: unreadable pid sharing a marker carrier's PGROUP is in scope (no anchors needed) [0.43ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 8/invariant 5: an anchor whose starttime no longer matches is REJECTED (pid reuse) [0.39ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 7: post-kill RESCAN unreadable half also preserves the marker [0.73ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > zombie process environ EACCES → skip (mm freed, expected) [0.39ms] -(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > EACCES-carrying process that vanishes during discrimination → skip [0.27ms] -(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > group containing a zombie same-uid member still verifies OK for the live marker members [0.33ms] -(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > group containing an other-user EACCES member still verifies OK for our members [0.27ms] -(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > empty group (no live marker members) → EMPTY_GROUP refuse (never ok:true) [0.20ms] -(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > own-uid non-zombie unreadable → ENUM_ERROR (fail-closed) [0.23ms] -(pass) Finding #2: killPgroup pgid<=0 guard > realKiller().killPgroup(0, TERM) throws — kill(-0) would target caller's own pgroup [0.46ms] -(pass) Finding #2: killPgroup pgid<=0 guard > realKiller().pgroupAlive(0) throws [0.34ms] -(pass) Finding #3: reapMarkerGroups uses async sleep (not busy-wait) > grace period is truly asynchronous — event loop ticks during it [103.65ms] -(pass) Finding #3: reapMarkerGroups uses async sleep (not busy-wait) > injected sleep function is used (tests can override with fast/deterministic version) [1.00ms] -(pass) Finding #7: readMarker PLATFORM_UNSUPPORTED on non-Linux > on non-Linux, readMarker refuses cleanly regardless of on-disk state [3.13ms] -(pass) Finding #4: writeMarker accepts partial sessions object > writeMarker with only appsrv session succeeds and readMarker returns ok [3.49ms] -(pass) Finding #4: writeMarker accepts partial sessions object > writeMarker with empty sessions object still succeeds (uuid is what matters) [3.01ms] -(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > an unrelated OTHER-uid pid whose stat is unreadable does NOT poison the group [0.55ms] -(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > a pid hidden so thoroughly that even its uid is unknown does NOT poison the group [0.65ms] -(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > an OWN-uid pid whose stat is unreadable still fails closed (we cannot rule out membership) [0.64ms] -(pass) Blocker 4: readMarker checks MISSING before PLATFORM_UNSUPPORTED > non-Linux + NO marker file → MISSING (silent legacy fall-through, no scary warning) [0.61ms] -(pass) Blocker 4: readMarker checks MISSING before PLATFORM_UNSUPPORTED > non-Linux + marker file present → PLATFORM_UNSUPPORTED (we genuinely cannot act on it) [8.15ms] -(pass) Blockers 5+6: prepareIdentityForStart > no marker on disk → writes the new marker, reaps nothing [1.09ms] -(pass) Blockers 5+6: prepareIdentityForStart > Blocker 6: a PRESERVED marker is reaped by its OWN uuid before the new one is written [0.93ms] -(pass) Blockers 5+6: prepareIdentityForStart > Blocker 6: if the old generation cannot be reaped, start is BLOCKED and nothing is overwritten [0.52ms] -(pass) Blockers 5+6: prepareIdentityForStart > a marker from a previous BOOT is discarded without a reap (its pids cannot exist) [0.49ms] -(pass) Blockers 5+6: prepareIdentityForStart > an unreadable/suspicious marker BLOCKS start rather than overwriting it [0.69ms] -(pass) Blockers 5+6: prepareIdentityForStart > Blocker 5: the marker is written with an EMPTY sessions object (before any session exists) [0.35ms] -(pass) Blockers 5+6: prepareIdentityForStart > refuses an empty uuid (guards against a silently regenerated identity) [0.34ms] +(pass) Test 1: UUID round-trip > writeMarker refuses non-string uuid [0.27ms] +(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when listAllPids throws [1.03ms] +(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when a member's environ read throws [0.80ms] +(pass) Test 2: enumeration failure is loud (fail-closed) > verifyGroupHomogeneity fails-closed when a stat read throws [0.32ms] +(pass) Test 3: foreign member in PGID → SKIP > group with unmarked co-resident refuses homogeneity [0.26ms] +(pass) Test 3: foreign member in PGID → SKIP > group where every member carries the marker is ok [0.26ms] +(pass) Test 4: main-dead-child-alive (environ scan is authority) > scan finds workers even when marker's stored pids are gone [0.72ms] +(pass) Test 5: child setsid → new PGID > detached child grouped under its current pgid, not marker's stored pgid [0.28ms] +(pass) Test 6: PID-reuse defense is the boot_id + environ-scan invariant > environ scan only returns pids whose current environ carries the uuid [0.20ms] +(pass) Test 7: partial-start rollback (marker gate) > MISSING marker after partial start prevents any process action [0.69ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > null body → SCHEMA_INVALID (no TypeError from `in` operator) [2.00ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > bare number → SCHEMA_INVALID [0.54ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > empty array → SCHEMA_INVALID [0.51ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > empty object → SCHEMA_INVALID (missing required fields) [0.50ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > wrong types in schema → SCHEMA_INVALID [0.58ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > syntactically invalid JSON → PARSE_ERROR [0.44ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > wrong mode → WRONG_MODE (even with valid JSON) [0.42ms] +(pass) Test 8: malformed marker → structured refuse (never throws) > symlink → SYMLINK (refuses to follow) [0.46ms] +(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > NOT_REGULAR: directory at marker path with mode 0600 (skips SYMLINK+WRONG_MODE) [0.41ms] +(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > OWNER_MISMATCH: valid marker file whose lstat.uid differs from process.getuid() (SECURITY CRITICAL) [3.45ms] +(pass) Test 8b: filesystem/environment refuse guards (mutation-sensitive) > STALE_BOOT_ID: valid schema but boot_id differs from current /proc boot_id [0.91ms] +(pass) Test 9: self-context refuses stop from within the tree > caller's own environ carrying the marker is detected [0.45ms] +(pass) Test 9: self-context refuses stop from within the tree > ancestor carrying the marker is detected via PPID walk [0.29ms] +(pass) Test 9: self-context refuses stop from within the tree > clean caller (no marker in ancestry) returns self=false [0.23ms] +(pass) Test 10: non-copresence codex-app-server → legacy path (zero diff) > readMarker returns MISSING for an ordinary codex-app-server node dir [0.52ms] +(pass) Test 11: 二次 stop is idempotent (MISSING = already stopped) > 2nd read after successful removeMarker returns MISSING (no side effects) [2.90ms] +(pass) Test 11: 二次 stop is idempotent (MISSING = already stopped) > removeMarker on already-missing marker does not throw [0.29ms] +(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > verified groups get SIGTERM, still-alive groups then get SIGKILL [7.48ms] +(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > groups with foreign members are SKIPPED, never signaled [2.11ms] +(pass) reapMarkerGroups: end-to-end (mocked /proc + kill) > no marker-carrying pids anywhere → immediate success (idempotent) [0.44ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > other-user EACCES on environ → skip that pid (expected, not fail) [0.57ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Defect A defense: own-uid EACCES pid IN SCOPE (anchored) → reap refuses to delete marker [0.85ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 1: own-uid EACCES pid OUT OF SCOPE → informational only, teardown still succeeds [0.53ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 1: unreadable pid sharing a marker carrier's PGROUP is in scope (no anchors needed) [0.40ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 8/invariant 5: an anchor whose starttime no longer matches is REJECTED (pid reuse) [0.60ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > Blocker 7: post-kill RESCAN unreadable half also preserves the marker [0.89ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > zombie process environ EACCES → skip (mm freed, expected) [0.55ms] +(pass) Blocker 1: scanEnvironForMarker EACCES discrimination > EACCES-carrying process that vanishes during discrimination → skip [0.35ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > group containing a zombie same-uid member still verifies OK for the live marker members [0.35ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > group containing an other-user EACCES member still verifies OK for our members [0.40ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > empty group (no live marker members) → EMPTY_GROUP refuse (never ok:true) [0.22ms] +(pass) Blocker 2: verifyGroupHomogeneity zombie discrimination + EMPTY_GROUP > own-uid non-zombie unreadable → ENUM_ERROR (fail-closed) [0.33ms] +(pass) Finding #2: killPgroup pgid<=0 guard > realKiller().killPgroup(0, TERM) throws — kill(-0) would target caller's own pgroup [0.44ms] +(pass) Finding #2: killPgroup pgid<=0 guard > realKiller().pgroupAlive(0) throws [0.35ms] +(pass) Finding #3: reapMarkerGroups uses async sleep (not busy-wait) > grace period is truly asynchronous — event loop ticks during it [102.93ms] +(pass) Finding #3: reapMarkerGroups uses async sleep (not busy-wait) > injected sleep function is used (tests can override with fast/deterministic version) [1.01ms] +(pass) Finding #7: readMarker PLATFORM_UNSUPPORTED on non-Linux > on non-Linux, readMarker refuses cleanly regardless of on-disk state [3.32ms] +(pass) Finding #4: writeMarker accepts partial sessions object > writeMarker with only appsrv session succeeds and readMarker returns ok [2.94ms] +(pass) Finding #4: writeMarker accepts partial sessions object > writeMarker with empty sessions object still succeeds (uuid is what matters) [3.53ms] +(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > an unrelated OTHER-uid pid whose stat is unreadable does NOT poison the group [0.67ms] +(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > a pid hidden so thoroughly that even its uid is unknown does NOT poison the group [0.73ms] +(pass) Blocker 3: verifyGroupHomogeneity stat-unreadable pids are bounded by ownership > an OWN-uid pid whose stat is unreadable still fails closed (we cannot rule out membership) [0.41ms] +(pass) Blocker 4: readMarker checks MISSING before PLATFORM_UNSUPPORTED > non-Linux + NO marker file → MISSING (silent legacy fall-through, no scary warning) [0.50ms] +(pass) Blocker 4: readMarker checks MISSING before PLATFORM_UNSUPPORTED > non-Linux + marker file present → PLATFORM_UNSUPPORTED (we genuinely cannot act on it) [2.80ms] +(pass) Blockers 5+6: prepareIdentityForStart > no marker on disk → writes the new marker, reaps nothing [1.03ms] +(pass) Blockers 5+6: prepareIdentityForStart > Blocker 6: a PRESERVED marker is reaped by its OWN uuid before the new one is written [1.18ms] +(pass) Blockers 5+6: prepareIdentityForStart > Blocker 6: if the old generation cannot be reaped, start is BLOCKED and nothing is overwritten [0.58ms] +(pass) Blockers 5+6: prepareIdentityForStart > a marker from a previous BOOT is discarded without a reap (its pids cannot exist) [0.42ms] +(pass) Blockers 5+6: prepareIdentityForStart > an unreadable/suspicious marker BLOCKS start rather than overwriting it [0.57ms] +(pass) Blockers 5+6: prepareIdentityForStart > Blocker 5: the marker is written with an EMPTY sessions object (before any session exists) [0.38ms] +(pass) Blockers 5+6: prepareIdentityForStart > refuses an empty uuid (guards against a silently regenerated identity) [0.37ms] src/claude-vendor-env-wiring.test.ts: -(pass) node create captures vendor shell env before profile construction [0.21ms] -(pass) every dotenv-writing create preflights before any node-state side effect [0.26ms] -(pass) the dotenv writer itself reuses the side-effect-free planner [0.17ms] +(pass) node create captures vendor shell env before profile construction [0.20ms] +(pass) every dotenv-writing create preflights before any node-state side effect [0.22ms] +(pass) the dotenv writer itself reuses the side-effect-free planner [0.14ms] src/copresence-cli-wiring.test.ts: -(pass) cli.ts copresence start ordering (structural gate) > the copresence start path really does create tmux sessions with -e (anchor for the tests below) [0.05ms] -(pass) cli.ts copresence start ordering (structural gate) > Blocker 5: prepareIdentityForStart runs BEFORE the first tmux new-session [0.05ms] -(pass) cli.ts copresence start ordering (structural gate) > Blocker 5: no marker write happens before the identity preparation call [0.04ms] -(pass) cli.ts copresence start ordering (structural gate) > Blocker 6: a blocked preparation aborts the start (never falls through to session creation) [0.08ms] +(pass) cli.ts copresence start ordering (structural gate) > the copresence start path really does create tmux sessions with -e (anchor for the tests below) [0.06ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 5: prepareIdentityForStart runs BEFORE the first tmux new-session [0.10ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 5: no marker write happens before the identity preparation call [0.07ms] +(pass) cli.ts copresence start ordering (structural gate) > Blocker 6: a blocked preparation aborts the start (never falls through to session creation) [0.07ms] (pass) cli.ts copresence start ordering (structural gate) > Blocker 12: the tmux capability preflight runs BEFORE the first tmux new-session [0.04ms] -(pass) cli.ts copresence stop wiring (structural gate) > Blockers 1+2: the stop-time reap is given the marker's recorded pids as scope anchors [0.37ms] -(pass) cli.ts copresence stop wiring (structural gate) > marker removal happens only on a successful reap [0.23ms] +(pass) cli.ts copresence stop wiring (structural gate) > Blockers 1+2: the stop-time reap is given the marker's recorded pids as scope anchors [0.28ms] +(pass) cli.ts copresence stop wiring (structural gate) > marker removal happens only on a successful reap [0.28ms] src/grok-copresence-disclosure.test.ts: -(pass) grok co-presence disclosure > default profile reports the exact three tools and no web [0.18ms] -(pass) grok co-presence disclosure > WebSearch profile reports general web_search without widening other tools [0.12ms] -(pass) grok co-presence disclosure > near-match tools are disclosed as invalid rather than a reviewed profile [0.13ms] -(pass) grok co-presence disclosure > resume warns that a changed config cannot mutate the existing session [0.07ms] +(pass) grok co-presence disclosure > default profile reports the exact three tools and no web [0.28ms] +(pass) grok co-presence disclosure > WebSearch profile reports general web_search without widening other tools [0.11ms] +(pass) grok co-presence disclosure > near-match tools are disclosed as invalid rather than a reviewed profile [0.14ms] +(pass) grok co-presence disclosure > resume warns that a changed config cannot mutate the existing session [0.08ms] src/opencode-agent-node-pair.test.ts: -(pass) OpenCode agent-node release pairing > pins the exact versions being released together [0.11ms] -(pass) OpenCode agent-node release pairing > rejects latest 2.4.x-style help and accepts the RFC-029 capability [0.07ms] -(pass) OpenCode agent-node release pairing > admits only the exact preview package identity with safe file modes [12.82ms] -(pass) OpenCode agent-node release pairing > skips an exact project-local impersonator and selects the later global package [7.22ms] +(pass) OpenCode agent-node release pairing > pins the exact versions being released together [0.14ms] +(pass) OpenCode agent-node release pairing > rejects latest 2.4.x-style help and accepts the RFC-029 capability [0.08ms] +(pass) OpenCode agent-node release pairing > admits only the exact preview package identity with safe file modes [9.17ms] +(pass) OpenCode agent-node release pairing > skips an exact project-local impersonator and selects the later global package [6.23ms] src/batch-workdir.test.ts: -(pass) normalizeBatchWorkdir > expands current-user tilde before a batch changes cwd [0.39ms] -(pass) normalizeBatchWorkdir > anchors a relative workdir once to the caller cwd [0.08ms] -(pass) normalizeBatchWorkdir > keeps an absolute workdir absolute [0.06ms] -(pass) normalizeBatchWorkdir > rejects empty and unsupported named-user shorthands [0.18ms] +(pass) normalizeBatchWorkdir > expands current-user tilde before a batch changes cwd [0.23ms] +(pass) normalizeBatchWorkdir > anchors a relative workdir once to the caller cwd [0.06ms] +(pass) normalizeBatchWorkdir > keeps an absolute workdir absolute [0.03ms] +(pass) normalizeBatchWorkdir > rejects empty and unsupported named-user shorthands [0.12ms] src/copresence-identity.real.test.ts: -(pass) REAL /proc integration (Linux only) > A: scan on real /proc with a nonce uuid does not throw and finds nothing [1.18ms] -(pass) REAL /proc integration (Linux only) > B: live marker member + REAL zombie sibling in the same pgroup → homogeneity ok:true (escalation stays possible) [77.39ms] -(pass) REAL /proc integration (Linux only) > C: readEnviron(1) EACCESes and readOwnerUid(1) is root (non-root only) [0.32ms] -(pass) REAL /proc integration (Linux only) > D: POSITIVE — spawned marker carrier is found by the scan [26.10ms] -(pass) REAL /proc integration (Linux only) > E: END-TO-END — scan → group → homogeneity all succeed on real /proc [30.84ms] -(pass) REAL /proc integration (Linux only) > F: REAL REAP — reapMarkerGroups(realEnumerator, realKiller) kills a real carrier and returns success [378.87ms] -(pass) REAL /proc integration (Linux only) > G: CLEAN-HOST REAP — nothing carries the uuid → success on THIS host (blocker 1 regression) [1.12ms] -(pass) REAL /proc integration (Linux only) > H: NON-DUMPABLE — marker-carrying non-dumpable child of a carrier is accounted for, not dropped (blocker 2) [97.54ms] -(pass) REAL /proc integration (Linux only) > I: readOwnerUid reports the REAL uid of a non-dumpable process (environ inode owner lies) [51.41ms] -(pass) REAL /proc integration (Linux only) > J: REAL START SEAM — prepareIdentityForStart reclaims a live previous generation and installs the new marker [341.06ms] -(pass) REAL /proc integration (Linux only) > K: REAL START SEAM — a previous generation that cannot be reaped BLOCKS the start and its marker survives [76.43ms] -(pass) REAL /proc integration (Linux only) > L: anchorsFromMarker feeds real recorded pane pids into the scope test [26.67ms] +(pass) REAL /proc integration (Linux only) > A: scan on real /proc with a nonce uuid does not throw and finds nothing [1.74ms] +(pass) REAL /proc integration (Linux only) > B: live marker member + REAL zombie sibling in the same pgroup → homogeneity ok:true (escalation stays possible) [114.80ms] +(pass) REAL /proc integration (Linux only) > C: readEnviron(1) EACCESes and readOwnerUid(1) is root (non-root only) [0.51ms] +(pass) REAL /proc integration (Linux only) > D: POSITIVE — spawned marker carrier is found by the scan [30.07ms] +(pass) REAL /proc integration (Linux only) > E: END-TO-END — scan → group → homogeneity all succeed on real /proc [33.50ms] +(pass) REAL /proc integration (Linux only) > F: REAL REAP — reapMarkerGroups(realEnumerator, realKiller) kills a real carrier and returns success [396.41ms] +(pass) REAL /proc integration (Linux only) > G: CLEAN-HOST REAP — nothing carries the uuid → success on THIS host (blocker 1 regression) [0.92ms] +(pass) REAL /proc integration (Linux only) > H: NON-DUMPABLE — marker-carrying non-dumpable child of a carrier is accounted for, not dropped (blocker 2) [100.79ms] +(pass) REAL /proc integration (Linux only) > I: readOwnerUid reports the REAL uid of a non-dumpable process (environ inode owner lies) [54.67ms] +(pass) REAL /proc integration (Linux only) > J: REAL START SEAM — prepareIdentityForStart reclaims a live previous generation and installs the new marker [338.51ms] +(pass) REAL /proc integration (Linux only) > K: REAL START SEAM — a previous generation that cannot be reaped BLOCKS the start and its marker survives [82.19ms] +(pass) REAL /proc integration (Linux only) > L: anchorsFromMarker feeds real recorded pane pids into the scope test [33.59ms] src/claude-code-cli-tty-preflight.test.ts: -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > body contains the claude-code-cli spawn (anchor for the assertions below) [0.09ms] -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: non-TTY stdin preflight fires BEFORE the claude spawn [0.09ms] -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: non-TTY branch exits non-zero [0.21ms] -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: message names claude-code-cli and recommends --accept-dev-channels first (--tmux listed with its precondition) [0.25ms] -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Success gate: 'session pinned' / 'session saved' only fires on exit code 0 [0.20ms] -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Exit-code propagation: non-zero child exit calls process.exit(code) [0.13ms] -(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Spawn-error path: child.on('error') exits non-zero (was silent → false success) [0.19ms] -(pass) --tmux escape-hatch headless (#486 CR regression gate) > body contains the --tmux branch (anchor) [0.10ms] -(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux branch has a headless (no-TTY) codepath (`new-session -d`) [0.17ms] -(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: does NOT inherit stdin on detached spawn (was `stdio:"inherit"`) [0.20ms] -(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: verifies session liveness after detached spawn [0.22ms] -(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: propagates non-zero exit on failure paths [0.40ms] -(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: prints attach hint after successful startup [0.32ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > body contains the claude-code-cli spawn (anchor for the assertions below) [0.11ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: non-TTY stdin preflight fires BEFORE the claude spawn [0.11ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: non-TTY branch exits non-zero [0.25ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Refuse: message names claude-code-cli and recommends --accept-dev-channels first (--tmux listed with its precondition) [0.30ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Success gate: 'session pinned' / 'session saved' only fires on exit code 0 [0.24ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Exit-code propagation: non-zero child exit calls process.exit(code) [0.11ms] +(pass) claude-code-cli spawn preflight (#486 P0 regression gate) > Spawn-error path: child.on('error') exits non-zero (was silent → false success) [0.12ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > body contains the --tmux branch (anchor) [0.09ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux branch has a headless (no-TTY) codepath (`new-session -d`) [0.14ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: does NOT inherit stdin on detached spawn (was `stdio:"inherit"`) [0.18ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: verifies session liveness after detached spawn [0.14ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: propagates non-zero exit on failure paths [0.12ms] +(pass) --tmux escape-hatch headless (#486 CR regression gate) > --tmux headless: prints attach hint after successful startup [0.09ms] src/dashboard-managed-process.test.ts: -(pass) managed Dashboard listener decisions > empty port starts; same healthy managed release remains untouched [0.55ms] -(pass) managed Dashboard listener decisions > only an exact managed stale npx listener may be terminated [0.08ms] -(pass) managed Dashboard listener decisions > unmanaged, ambiguous, reused, foreign, and global listeners fail closed [0.26ms] -(pass) record parser and command identity reject malformed state [0.19ms] +(pass) managed Dashboard listener decisions > empty port starts; same healthy managed release remains untouched [0.43ms] +(pass) managed Dashboard listener decisions > only an exact managed stale npx listener may be terminated [0.12ms] +(pass) managed Dashboard listener decisions > unmanaged, ambiguous, reused, foreign, and global listeners fail closed [0.23ms] +(pass) record parser and command identity reject malformed state [0.18ms] src/token-cli.test.ts: -(pass) parseTokenCreateName > keeps the legacy positional form [0.26ms] -(pass) parseTokenCreateName > accepts separated and equals --name forms [0.11ms] -(pass) parseTokenCreateName > fails closed for missing, empty, unknown, mixed, or extra operands [0.22ms] +(pass) parseTokenCreateName > keeps the legacy positional form [0.23ms] +(pass) parseTokenCreateName > accepts separated and equals --name forms [0.10ms] +(pass) parseTokenCreateName > fails closed for missing, empty, unknown, mixed, or extra operands [0.23ms] src/cli-args-wiring.test.ts: -(pass) CLI option and positional parsing share cli-args.ts [3.02ms] +(pass) CLI option and positional parsing share cli-args.ts [3.62ms] src/private-state.test.ts: -(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 0 [4.55ms] -(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 2 [3.35ms] -(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 22 [3.31ms] -(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 77 [3.79ms] -(pass) #472 private state writer > atomically replaces a legacy 0664 target with a 0600 inode [3.29ms] -(pass) #472 private state writer > replaces a leaf symlink instead of writing through it [7.13ms] -(pass) #472 private state writer > repairs a legacy file and parent before reading [0.66ms] -(pass) #472 private state writer > read repair refuses a symlink instead of chmod-following it [0.64ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 0 [4.69ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 2 [3.17ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 22 [4.66ms] +(pass) #472 private state writer > publishes 0600 files and 0700 parent under umask 77 [3.99ms] +(pass) #472 private state writer > atomically replaces a legacy 0664 target with a 0600 inode [3.90ms] +(pass) #472 private state writer > replaces a leaf symlink instead of writing through it [5.45ms] +(pass) #472 private state writer > repairs a legacy file and parent before reading [0.67ms] +(pass) #472 private state writer > read repair refuses a symlink instead of chmod-following it [0.71ms] src/tmux-capability.test.ts: -(pass) parseTmuxVersion > parses the shapes real tmux builds print [0.43ms] -(pass) parseTmuxVersion > returns null when there is no version to find [0.23ms] -(pass) tmuxSupportsSessionEnv > 3.2 is the floor; the letter suffix is a patch marker and never lifts a version over it [0.14ms] -(pass) tmuxSupportsSessionEnv > major version dominates the minor comparison [0.05ms] -(pass) checkTmuxCapability > too old → actionable verdict naming the required version [0.27ms] -(pass) checkTmuxCapability > tmux absent → missing verdict, not a crash [0.14ms] -(pass) checkTmuxCapability > unparseable version → unknown (permissive: never refuse a tmux that may be fine) [0.07ms] +(pass) parseTmuxVersion > parses the shapes real tmux builds print [0.44ms] +(pass) parseTmuxVersion > returns null when there is no version to find [0.09ms] +(pass) tmuxSupportsSessionEnv > 3.2 is the floor; the letter suffix is a patch marker and never lifts a version over it [0.13ms] +(pass) tmuxSupportsSessionEnv > major version dominates the minor comparison [0.07ms] +(pass) checkTmuxCapability > too old → actionable verdict naming the required version [0.29ms] +(pass) checkTmuxCapability > tmux absent → missing verdict, not a crash [0.16ms] +(pass) checkTmuxCapability > unparseable version → unknown (permissive: never refuse a tmux that may be fine) [0.11ms] (pass) checkTmuxCapability > modern tmux → ok [0.05ms] -(pass) assertTmuxSupportsSessionEnv (cli wrapper) > old tmux aborts the start with an explanation [0.42ms] -(pass) assertTmuxSupportsSessionEnv (cli wrapper) > missing tmux aborts the start [0.10ms] -(pass) assertTmuxSupportsSessionEnv (cli wrapper) > modern tmux is silent and does not abort [0.05ms] -(pass) assertTmuxSupportsSessionEnv (cli wrapper) > unknown version warns but does NOT abort [0.46ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > old tmux aborts the start with an explanation [0.63ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > missing tmux aborts the start [0.12ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > modern tmux is silent and does not abort [0.06ms] +(pass) assertTmuxSupportsSessionEnv (cli wrapper) > unknown version warns but does NOT abort [0.17ms] src/opencode-launch-env.test.ts: -(pass) hardenOpencodeAgentNodeEnv > restores launcher PATH and strips every pre-entrypoint loader hook [0.41ms] -(pass) hardenOpencodeAgentNodeEnv > does not mutate the caller's env object [0.15ms] -(pass) hardenOpencodeAgentNodeEnv > strips case-variant loader and PATH keys for Windows semantics [0.18ms] +(pass) hardenOpencodeAgentNodeEnv > restores launcher PATH and strips every pre-entrypoint loader hook [0.48ms] +(pass) hardenOpencodeAgentNodeEnv > does not mutate the caller's env object [0.16ms] +(pass) hardenOpencodeAgentNodeEnv > strips case-variant loader and PATH keys for Windows semantics [0.14ms] src/secret-shell-guidance.test.ts: -(pass) #379 secret shell guidance > keeps the existing POSIX export form [0.27ms] +(pass) #379 secret shell guidance > keeps the existing POSIX export form [0.19ms] (pass) #379 secret shell guidance > uses PowerShell syntax and quote escaping on Windows [0.07ms] src/opencode-auth-login.test.ts: -(pass) OpenCode manual auth-login sandbox > builds deterministic provider-specific API-key login argv [0.50ms] -(pass) OpenCode manual auth-login sandbox > uses a fresh all-XDG tree and strips ambient credentials/config hooks [28.63ms] -(pass) OpenCode manual auth-login sandbox > strictly consumes only the selected provider API record through a private leaf [18.88ms] -(pass) OpenCode manual auth-login sandbox > refuses OAuth, mixed-provider and symlink auth shapes without disclosing secrets [20.12ms] -(pass) OpenCode manual auth-login sandbox > persistent planted DB/log links are never exposed and cleanup never follows descendant links [20.19ms] -(pass) OpenCode manual auth-login sandbox > cleanup unlinks a swapped root symlink but never removes its outside target [23.15ms] -(pass) OpenCode manual auth-login sandbox > cleanup quarantines the tracked inode but leaves a regular root-name replacement untouched [15.70ms] -(pass) OpenCode manual auth-login sandbox > a live tracked root whose literal name ends in deleted is still removed [16.64ms] -(pass) OpenCode manual auth-login sandbox > Linux reports nlink zero for a removed directory retained by fd [9.52ms] -(pass) OpenCode manual auth-login sandbox > cleanup retains inode ownership after bounded failure and succeeds on retry [18.54ms] -(pass) OpenCode manual auth-login sandbox > refuses a concurrent live owner marker [23.57ms] -(pass) OpenCode manual auth-login sandbox > refuses a provider that does not match the node's unique configured preset [10.15ms] -(pass) OpenCode manual auth-login sandbox > prunes a dead owner's stale root without following its planted links [29.51ms] -(pass) OpenCode manual auth-login sandbox > PID reuse does not retain a stale credential root [27.39ms] -(pass) OpenCode manual auth-login sandbox > stale sweep resumes a crash-left quarantine while its owner marker remains [35.39ms] -(pass) OpenCode manual auth-login sandbox > stale sweep removes an empty quarantine left after marker-last deletion [17.73ms] -(pass) OpenCode manual auth-login sandbox > spawn-time revalidation rejects a hostile ancestor discovery candidate [15.62ms] -(pass) OpenCode manual auth-login sandbox > with helper always cleans the fresh root when the action throws [15.89ms] +(pass) OpenCode manual auth-login sandbox > builds deterministic provider-specific API-key login argv [0.49ms] +(pass) OpenCode manual auth-login sandbox > uses a fresh all-XDG tree and strips ambient credentials/config hooks [34.18ms] +(pass) OpenCode manual auth-login sandbox > strictly consumes only the selected provider API record through a private leaf [23.21ms] +(pass) OpenCode manual auth-login sandbox > refuses OAuth, mixed-provider and symlink auth shapes without disclosing secrets [24.55ms] +(pass) OpenCode manual auth-login sandbox > persistent planted DB/log links are never exposed and cleanup never follows descendant links [28.31ms] +(pass) OpenCode manual auth-login sandbox > cleanup unlinks a swapped root symlink but never removes its outside target [26.94ms] +(pass) OpenCode manual auth-login sandbox > cleanup quarantines the tracked inode but leaves a regular root-name replacement untouched [25.25ms] +(pass) OpenCode manual auth-login sandbox > a live tracked root whose literal name ends in deleted is still removed [27.77ms] +(pass) OpenCode manual auth-login sandbox > Linux reports nlink zero for a removed directory retained by fd [10.99ms] +(pass) OpenCode manual auth-login sandbox > cleanup retains inode ownership after bounded failure and succeeds on retry [24.13ms] +(pass) OpenCode manual auth-login sandbox > refuses a concurrent live owner marker [27.25ms] +(pass) OpenCode manual auth-login sandbox > refuses a provider that does not match the node's unique configured preset [13.57ms] +(pass) OpenCode manual auth-login sandbox > prunes a dead owner's stale root without following its planted links [48.22ms] +(pass) OpenCode manual auth-login sandbox > PID reuse does not retain a stale credential root [34.25ms] +(pass) OpenCode manual auth-login sandbox > stale sweep resumes a crash-left quarantine while its owner marker remains [38.89ms] +(pass) OpenCode manual auth-login sandbox > stale sweep removes an empty quarantine left after marker-last deletion [20.00ms] +(pass) OpenCode manual auth-login sandbox > spawn-time revalidation rejects a hostile ancestor discovery candidate [22.17ms] +(pass) OpenCode manual auth-login sandbox > with helper always cleans the fresh root when the action throws [19.66ms] src/node-start-help.test.ts: -(pass) #518 node start help exposes the recommended headless flag > real `anet node start --help` names --accept-dev-channels and its operating boundary [172.04ms] -(pass) #518 node start help exposes the recommended headless flag > asking for help performs no node-start work [165.22ms] +(pass) #518 node start help exposes the recommended headless flag > real `anet node start --help` names --accept-dev-channels and its operating boundary [265.59ms] +(pass) #518 node start help exposes the recommended headless flag > asking for help performs no node-start work [214.07ms] src/claude-code-cli-dependency-preflight.test.ts: -(pass) #485 claude-code-cli dependency preflight > create remains a warning while start fails closed [0.14ms] -(pass) #485 claude-code-cli dependency preflight > dependency refusal runs before launch side effects [0.08ms] +(pass) #485 claude-code-cli dependency preflight > create remains a warning while start fails closed [0.28ms] +(pass) #485 claude-code-cli dependency preflight > dependency refusal runs before launch side effects [0.09ms] src/bootstrap-password-db.test.ts: (pass) bootstrap password database binding > turns the local default into an explicit absolute path [0.62ms] -(pass) bootstrap password database binding > anchors a relative COMMHUB_DB to the hub launch cwd [0.26ms] -(pass) bootstrap password database binding > rejects an unusable default before opening a database [0.31ms] -(pass) bootstrap password database binding > does not invent a SQLite target for a PostgreSQL Hub [0.26ms] -(pass) bootstrap password database binding > updates only the explicitly resolved database, never ambient HOME [61.89ms] -(pass) bootstrap password database binding > child refuses a missing explicit path without falling back to HOME [40.36ms] +(pass) bootstrap password database binding > anchors a relative COMMHUB_DB to the hub launch cwd [0.24ms] +(pass) bootstrap password database binding > rejects an unusable default before opening a database [1.81ms] +(pass) bootstrap password database binding > does not invent a SQLite target for a PostgreSQL Hub [0.35ms] +(pass) bootstrap password database binding > updates only the explicitly resolved database, never ambient HOME [76.17ms] +(pass) bootstrap password database binding > child refuses a missing explicit path without falling back to HOME [47.30ms] src/gitignore-writeback.test.ts: -(pass) ensureGitignoreRule — file does not exist > creates file with the rule + trailing newline [2.53ms] -(pass) ensureGitignoreRule — file does not exist > trims surrounding whitespace from the rule before writing [0.35ms] -(pass) ensureGitignoreRule — file exists, rule absent > appends rule and reports 'appended' [0.78ms] -(pass) ensureGitignoreRule — file exists, rule absent > adds missing trailing newline before appending [0.72ms] -(pass) ensureGitignoreRule — file exists, rule absent > empty file → appended, not created [0.45ms] -(pass) ensureGitignoreRule — rule already present (idempotent) > exact match returns already-present + does not modify file [0.41ms] -(pass) ensureGitignoreRule — rule already present (idempotent) > trimmed match (rule with surrounding whitespace) treats as present [0.31ms] -(pass) ensureGitignoreRule — rule already present (idempotent) > commented-out rule does NOT count as present [0.34ms] -(pass) ensureGitignoreRule — rule already present (idempotent) > multiple invocations are idempotent (call 3 times) [0.43ms] -(pass) ensureGitignoreRule — multiple distinct rules don't collide > two different rules go to two different lines [0.36ms] -(pass) ensureGitignoreRule — multiple distinct rules don't collide > similar-but-different rules don't false-match (`.anet/` vs `.anet/foo`) [0.41ms] -(pass) ensureGitignoreRules — batch > empty rules list is a no-op [0.29ms] +(pass) ensureGitignoreRule — file does not exist > creates file with the rule + trailing newline [2.81ms] +(pass) ensureGitignoreRule — file does not exist > trims surrounding whitespace from the rule before writing [0.34ms] +(pass) ensureGitignoreRule — file exists, rule absent > appends rule and reports 'appended' [0.69ms] +(pass) ensureGitignoreRule — file exists, rule absent > adds missing trailing newline before appending [0.71ms] +(pass) ensureGitignoreRule — file exists, rule absent > empty file → appended, not created [0.41ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > exact match returns already-present + does not modify file [0.39ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > trimmed match (rule with surrounding whitespace) treats as present [0.50ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > commented-out rule does NOT count as present [0.50ms] +(pass) ensureGitignoreRule — rule already present (idempotent) > multiple invocations are idempotent (call 3 times) [1.26ms] +(pass) ensureGitignoreRule — multiple distinct rules don't collide > two different rules go to two different lines [0.39ms] +(pass) ensureGitignoreRule — multiple distinct rules don't collide > similar-but-different rules don't false-match (`.anet/` vs `.anet/foo`) [0.38ms] +(pass) ensureGitignoreRules — batch > empty rules list is a no-op [0.25ms] (pass) ensureGitignoreRules — batch > creates file with all rules on first call [0.40ms] -(pass) ensureGitignoreRules — batch > second batch call is fully idempotent [0.40ms] -(pass) ensureGitignoreRules — batch > partial overlap — only new rules appended [0.42ms] -(pass) ensureGitignoreRule — defensive > empty rule throws [0.30ms] -(pass) ensureGitignoreRule — defensive > whitespace-only rule throws [0.24ms] +(pass) ensureGitignoreRules — batch > second batch call is fully idempotent [0.36ms] +(pass) ensureGitignoreRules — batch > partial overlap — only new rules appended [0.40ms] +(pass) ensureGitignoreRule — defensive > empty rule throws [0.31ms] +(pass) ensureGitignoreRule — defensive > whitespace-only rule throws [0.26ms] src/secret-shell-guidance-wiring.test.ts: -(pass) #379 create and migrate both use platform-aware secret guidance [3.20ms] +(pass) #379 create and migrate both use platform-aware secret guidance [3.32ms] src/opencode-runtime-binding.test.ts: -(pass) external OpenCode runtime binding > survives regular config runtime downgrade and proves the original exact runtime [10.06ms] -(pass) external OpenCode runtime binding > read returns undefined only for absent state and deterministic keys separate nodes [15.05ms] -(pass) external OpenCode runtime binding > an absent exact leaf does not impose POSIX modes on ordinary runtime state [1.44ms] -(pass) external OpenCode runtime binding > unbound legacy symlink or junction-style node paths remain invisible [5.16ms] -(pass) external OpenCode runtime binding > Windows synthetic permission bits do not disable structural security checks [0.59ms] -(pass) external OpenCode runtime binding > secure removal is idempotent and removes the exact binding [6.35ms] -(pass) external OpenCode runtime binding > secure removal refuses tampered content without unlinking it [5.92ms] -(pass) external OpenCode runtime binding > rejects binding-directory and leaf symlinks [5.70ms] -(pass) external OpenCode runtime binding > rejects dangling binding-root and exact-leaf symlinks [2.77ms] -(pass) external OpenCode runtime binding > rejects permissive modes, hard links, and foreign ownership [6.19ms] -(pass) external OpenCode runtime binding > rejects private but tampered runtime, identity, and extra fields [6.60ms] -(pass) external OpenCode runtime binding > rejects binding roots that overlap the canonical project in either direction [4.02ms] -(pass) external OpenCode runtime binding > a symlinked node workDir cannot remove another project's binding [6.94ms] -(pass) assertOpencodeNodeStateUntracked > allows ordinary non-Git projects [1.64ms] -(pass) assertOpencodeNodeStateUntracked > allows ordinary untracked projects inside a Git worktree checkout [34.35ms] -(pass) assertOpencodeNodeStateUntracked > rejects forged Git worktree file markers [1.50ms] -(pass) assertOpencodeNodeStateUntracked > allows ignored/untracked state but rejects git add -f tracked state [18.22ms] -(pass) assertOpencodeNodeStateUntracked > rejects a force-added dotenv or any tracked file below the node directory [23.51ms] +(pass) external OpenCode runtime binding > survives regular config runtime downgrade and proves the original exact runtime [10.86ms] +(pass) external OpenCode runtime binding > read returns undefined only for absent state and deterministic keys separate nodes [8.85ms] +(pass) external OpenCode runtime binding > an absent exact leaf does not impose POSIX modes on ordinary runtime state [1.41ms] +(pass) external OpenCode runtime binding > unbound legacy symlink or junction-style node paths remain invisible [8.73ms] +(pass) external OpenCode runtime binding > Windows synthetic permission bits do not disable structural security checks [0.66ms] +(pass) external OpenCode runtime binding > secure removal is idempotent and removes the exact binding [6.34ms] +(pass) external OpenCode runtime binding > secure removal refuses tampered content without unlinking it [5.31ms] +(pass) external OpenCode runtime binding > rejects binding-directory and leaf symlinks [5.99ms] +(pass) external OpenCode runtime binding > rejects dangling binding-root and exact-leaf symlinks [2.92ms] +(pass) external OpenCode runtime binding > rejects permissive modes, hard links, and foreign ownership [6.17ms] +(pass) external OpenCode runtime binding > rejects private but tampered runtime, identity, and extra fields [6.82ms] +(pass) external OpenCode runtime binding > rejects binding roots that overlap the canonical project in either direction [3.85ms] +(pass) external OpenCode runtime binding > a symlinked node workDir cannot remove another project's binding [9.19ms] +(pass) assertOpencodeNodeStateUntracked > allows ordinary non-Git projects [1.58ms] +(pass) assertOpencodeNodeStateUntracked > allows ordinary untracked projects inside a Git worktree checkout [51.40ms] +(pass) assertOpencodeNodeStateUntracked > rejects forged Git worktree file markers [1.21ms] +(pass) assertOpencodeNodeStateUntracked > allows ignored/untracked state but rejects git add -f tracked state [17.39ms] +(pass) assertOpencodeNodeStateUntracked > rejects a force-added dotenv or any tracked file below the node directory [24.12ms] src/client.test.ts: -(pass) CommHub.reply calls send_reply MCP tool [2.71ms] +(pass) CommHub.reply calls send_reply MCP tool [2.83ms] src/supervise-child.test.ts: -(pass) superviseChild — shutdown gate stops the loop > shutdownGate=true from the start → runOnce never called [0.77ms] -(pass) superviseChild — shutdown gate stops the loop > shutdownGate flips true after first iteration → exactly one runOnce [0.38ms] -(pass) superviseChild — backoff growth + cap > waits double the delay each iteration, capping at maxDelayMs [16.76ms] -(pass) superviseChild — runOnce that returns WITHOUT markStable is treated as failed (regression pin) > runOnce that returns cleanly without markStable → backoff doubles [16.00ms] -(pass) superviseChild — markStable resets backoff > after iteration that calls markStable, next wait is baseDelayMs again [16.03ms] -(pass) superviseChild — markStable resets backoff > markStable called multiple times in one iteration is idempotent [38.63ms] -(pass) superviseChild — abandonAfterMs > calls onAbandon and returns after cumulative downtime exceeds threshold [16.04ms] -(pass) superviseChild — abandonAfterMs > markStable in any iteration resets downtime — abandon never fires [16.06ms] -(pass) superviseChild — runOnce error handling > runOnce throws → onError fires, loop continues [16.04ms] -(pass) superviseChild — runOnce error handling > runOnce throws AND shutdownGate goes true → loop exits, no further iteration [1.81ms] -(pass) superviseChild — jitter range > jitterRatio=0.25 + random=0 → -25% of delay (lower bound) [14.24ms] -(pass) superviseChild — jitter range > jitterRatio=0.25 + random=1 → +25% of delay (upper bound) [16.28ms] +(pass) superviseChild — shutdown gate stops the loop > shutdownGate=true from the start → runOnce never called [0.61ms] +(pass) superviseChild — shutdown gate stops the loop > shutdownGate flips true after first iteration → exactly one runOnce [0.33ms] +(pass) superviseChild — backoff growth + cap > waits double the delay each iteration, capping at maxDelayMs [12.85ms] +(pass) superviseChild — runOnce that returns WITHOUT markStable is treated as failed (regression pin) > runOnce that returns cleanly without markStable → backoff doubles [16.04ms] +(pass) superviseChild — markStable resets backoff > after iteration that calls markStable, next wait is baseDelayMs again [16.02ms] +(pass) superviseChild — markStable resets backoff > markStable called multiple times in one iteration is idempotent [16.01ms] +(pass) superviseChild — abandonAfterMs > calls onAbandon and returns after cumulative downtime exceeds threshold [16.02ms] +(pass) superviseChild — abandonAfterMs > markStable in any iteration resets downtime — abandon never fires [16.02ms] +(pass) superviseChild — runOnce error handling > runOnce throws → onError fires, loop continues [16.01ms] +(pass) superviseChild — runOnce error handling > runOnce throws AND shutdownGate goes true → loop exits, no further iteration [1.84ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=0 → -25% of delay (lower bound) [14.15ms] +(pass) superviseChild — jitter range > jitterRatio=0.25 + random=1 → +25% of delay (upper bound) [16.02ms] (pass) superviseChild — jitter range > jitterRatio=0 → deterministic waits at exact delay [16.01ms] (pass) superviseChild — jitter range > waitMs floor 100 enforces minimum wait even with tiny base + negative jitter [16.02ms] -(pass) superviseChild — defensive contract > returns (does not throw) when runOnce never resolves and shutdown flips [3.39ms] +(pass) superviseChild — defensive contract > returns (does not throw) when runOnce never resolves and shutdown flips [4.20ms] src/claude-vendor-env.test.ts: -(pass) collectClaudeVendorEnvForCreate > captures known vendor endpoint and credential for claude-agent-sdk [0.39ms] -(pass) collectClaudeVendorEnvForCreate > explicit --env value wins without duplicate capture [0.12ms] -(pass) collectClaudeVendorEnvForCreate > does not capture vendor variables for another runtime [0.04ms] -(pass) collectClaudeVendorEnvForCreate > rejects line-oriented dotenv injection [0.21ms] +(pass) collectClaudeVendorEnvForCreate > captures known vendor endpoint and credential for claude-agent-sdk [0.58ms] +(pass) collectClaudeVendorEnvForCreate > explicit --env value wins without duplicate capture [0.26ms] +(pass) collectClaudeVendorEnvForCreate > does not capture vendor variables for another runtime [0.06ms] +(pass) collectClaudeVendorEnvForCreate > rejects line-oriented dotenv injection [0.24ms] (pass) collectClaudeVendorEnvForCreate > rejects line breaks in explicit --env for every runtime [0.19ms] -(pass) planPlainSecretEnvRewrites > plans the exact dotenv assignment without mutating the profile [0.37ms] -(pass) planPlainSecretEnvRewrites > rejects a secret dotenv value with CRLF before any caller mutation [0.22ms] +(pass) planPlainSecretEnvRewrites > plans the exact dotenv assignment without mutating the profile [0.43ms] +(pass) planPlainSecretEnvRewrites > rejects a secret dotenv value with CRLF before any caller mutation [0.23ms] src/locale-diagnostic-wiring.test.ts: -(pass) #68 doctor reports the pure locale diagnostic as a warning [5.65ms] +(pass) #68 doctor reports the pure locale diagnostic as a warning [4.56ms] src/primary-network.test.ts: -(pass) resolvePrimaryNetwork > uses current_network even when the network list is reversed and renamed [0.91ms] -(pass) resolvePrimaryNetwork > fails explicitly when current_network is missing instead of guessing networks[0] [0.51ms] -(pass) resolvePrimaryNetwork > turns transport and HTTP failures into explicit resolution errors [0.38ms] -(pass) debate, demo-social, and pr-review all use the shared resolver [2.60ms] +(pass) resolvePrimaryNetwork > uses current_network even when the network list is reversed and renamed [0.60ms] +(pass) resolvePrimaryNetwork > fails explicitly when current_network is missing instead of guessing networks[0] [0.34ms] +(pass) resolvePrimaryNetwork > turns transport and HTTP failures into explicit resolution errors [0.40ms] +(pass) debate, demo-social, and pr-review all use the shared resolver [2.38ms] src/opencode-preset.test.ts: -(pass) OPENCODE_PRESETS registry > exports the two blessed presets (anthropic + openai) [0.10ms] +(pass) OPENCODE_PRESETS registry > exports the two blessed presets (anthropic + openai) [0.09ms] (pass) OPENCODE_PRESETS registry > findOpencodePreset('anthropic') returns the record; unknown returns null [0.05ms] -(pass) readPresetKeyFromEnv — env-only, no interactive prompt > returns the trimmed key when the env var is set [0.14ms] -(pass) readPresetKeyFromEnv — env-only, no interactive prompt > returns null when the env var is missing / empty [0.10ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > body shape matches opencode auth.json convention [0.55ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > writes to /.local/share/opencode/auth.json with mode 0o600 [5.45ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > writeOpencodeConfigJson lands under .config/opencode with 0o600 [5.36ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > keyless create atomically clears a private pre-planted auth file [5.27ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > default tool policy disables filesystem, shell, task, and skill tools [0.25ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > writes only blessed provider identity and strips all pre-planted routing/executable config [5.52ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > atomically replaces a private but invalid pre-planted config without parsing it [5.40ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > rejects symlink escapes in workDir, config/data ancestors, and final targets [6.76ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > validates the full tree before mutation so a bad auth side cannot partially rewrite config [0.98ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > rejects permissive modes and foreign owners without chmod-follow repair [2.35ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > prepares .anet/nodes/node before profile secrets and provides atomic private leaf I/O [33.75ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > accepts an ordinary 0775 project root for a non-root uid=gid private group [7.82ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > profile preflight rejects .anet/nodes/node and config/.env symlink chains before secret writes [8.34ms] -(pass) buildAuthJsonBody + writeOpencodeAuthJson > profile preflight rejects writable ancestors and non-private node roots [2.47ms] +(pass) readPresetKeyFromEnv — env-only, no interactive prompt > returns the trimmed key when the env var is set [0.09ms] +(pass) readPresetKeyFromEnv — env-only, no interactive prompt > returns null when the env var is missing / empty [0.06ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > body shape matches opencode auth.json convention [0.54ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > writes to /.local/share/opencode/auth.json with mode 0o600 [5.71ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > writeOpencodeConfigJson lands under .config/opencode with 0o600 [6.79ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > keyless create atomically clears a private pre-planted auth file [6.00ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > default tool policy disables filesystem, shell, task, and skill tools [0.27ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > writes only blessed provider identity and strips all pre-planted routing/executable config [5.70ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > atomically replaces a private but invalid pre-planted config without parsing it [8.26ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > rejects symlink escapes in workDir, config/data ancestors, and final targets [7.90ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > validates the full tree before mutation so a bad auth side cannot partially rewrite config [1.06ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > rejects permissive modes and foreign owners without chmod-follow repair [2.55ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > prepares .anet/nodes/node before profile secrets and provides atomic private leaf I/O [22.43ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > accepts an ordinary 0775 project root for a non-root uid=gid private group [2.98ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > profile preflight rejects .anet/nodes/node and config/.env symlink chains before secret writes [5.60ms] +(pass) buildAuthJsonBody + writeOpencodeAuthJson > profile preflight rejects writable ancestors and non-private node roots [1.95ms] src/opencode-smoke-env.test.ts: -(pass) buildOpencodeSmokeEnv > locks the exact hardened ancestor candidate set [1.16ms] -(pass) buildOpencodeSmokeEnv > rejects sticky world-writable /tmp instead of silently degrading [0.58ms] -(pass) buildOpencodeSmokeEnv > inherits only transport/locale trust settings and controls all OpenCode roots [0.82ms] -(pass) buildOpencodeSmokeEnv > every writable root can be precreated private, including XDG_RUNTIME_DIR [1.13ms] +(pass) buildOpencodeSmokeEnv > locks the exact hardened ancestor candidate set [3.95ms] +(pass) buildOpencodeSmokeEnv > rejects sticky world-writable /tmp instead of silently degrading [0.68ms] +(pass) buildOpencodeSmokeEnv > inherits only transport/locale trust settings and controls all OpenCode roots [0.59ms] +(pass) buildOpencodeSmokeEnv > every writable root can be precreated private, including XDG_RUNTIME_DIR [1.00ms] src/grok-attach-client.test.ts: -(pass) validateGrokAttachSocket rejects symlinks, non-sockets, and foreign owners [3.40ms] -(pass) connectGrokAttach bridges base64 terminal I/O, status, resize, and detach [9.72ms] -(pass) connectGrokAttach splits large input so every NDJSON frame stays bounded [1.17ms] -(pass) connectGrokAttach fails closed on an invalid handshake and oversized frame [1.39ms] -(pass) a single-client rejection before hello preserves the server error [0.62ms] -(pass) hello followed by a fatal frame in the same chunk cannot return a dead session [0.73ms] -(pass) detach force-closes a peer that never completes its half-close [13.12ms] -(pass) callback failure and invalid limits fail before returning an attached client [1.48ms] -(pass) remote detach is surfaced and closes without echoing a detach frame [1.01ms] +(pass) validateGrokAttachSocket rejects symlinks, non-sockets, and foreign owners [2.83ms] +(pass) connectGrokAttach bridges base64 terminal I/O, status, resize, and detach [8.35ms] +(pass) connectGrokAttach splits large input so every NDJSON frame stays bounded [1.28ms] +(pass) connectGrokAttach fails closed on an invalid handshake and oversized frame [1.24ms] +(pass) a single-client rejection before hello preserves the server error [0.54ms] +(pass) hello followed by a fatal frame in the same chunk cannot return a dead session [0.62ms] +(pass) detach force-closes a peer that never completes its half-close [12.54ms] +(pass) callback failure and invalid limits fail before returning an attached client [1.07ms] +(pass) remote detach is surfaced and closes without echoing a detach frame [0.71ms] src/grok-copresence-profile.test.ts: -(pass) Grok copresence profile defaults > builds the Grok agent-node parent environment from an exact empty allowlist [2.65ms] -(pass) Grok copresence profile defaults > does not mistake an old headless-only agent-node for co-presence support [0.16ms] -(pass) Grok copresence profile defaults > builds the npm resolver environment from an exact empty allowlist [0.67ms] -(pass) Grok copresence profile defaults > prepares two distinct empty owner-only npm config files without following symlinks [1.99ms] -(pass) Grok copresence profile defaults > enables copresence only for non-headless grok-build-cli [0.60ms] -(pass) Grok copresence profile defaults > uses the owner-bound state home even when XDG is owner-only [0.53ms] -(pass) Grok copresence profile defaults > falls back to a bounded owner tmp path when the state home is too long [0.34ms] +(pass) Grok copresence profile defaults > builds the Grok agent-node parent environment from an exact empty allowlist [1.80ms] +(pass) Grok copresence profile defaults > does not mistake an old headless-only agent-node for co-presence support [0.11ms] +(pass) Grok copresence profile defaults > builds the npm resolver environment from an exact empty allowlist [0.70ms] +(pass) Grok copresence profile defaults > prepares two distinct empty owner-only npm config files without following symlinks [1.87ms] +(pass) Grok copresence profile defaults > enables copresence only for non-headless grok-build-cli [0.51ms] +(pass) Grok copresence profile defaults > uses the owner-bound state home even when XDG is owner-only [0.47ms] +(pass) Grok copresence profile defaults > falls back to a bounded owner tmp path when the state home is too long [0.18ms] src/opencode-copresence-cli.test.ts: (pass) OpenCode co-presence CLI wiring > persists copresence mode before launching the bridge [0.06ms] -(pass) OpenCode co-presence CLI wiring > starts only exact alias and alias-bridge tmux sessions [0.07ms] +(pass) OpenCode co-presence CLI wiring > starts only exact alias and alias-bridge tmux sessions [0.08ms] (pass) OpenCode co-presence CLI wiring > does not depend on a long-lived tmux server's stale launcher environment [0.05ms] -(pass) OpenCode co-presence CLI wiring > waits for the owner-only runtime launcher before starting the official TUI [0.06ms] -(pass) OpenCode co-presence CLI wiring > the generic --copresence dispatcher selects OpenCode by stored runtime [0.19ms] -(pass) OpenCode co-presence CLI wiring > operator help names the create, attach, and stop commands [0.99ms] +(pass) OpenCode co-presence CLI wiring > waits for the owner-only runtime launcher before starting the official TUI [0.08ms] +(pass) OpenCode co-presence CLI wiring > the generic --copresence dispatcher selects OpenCode by stored runtime [0.17ms] +(pass) OpenCode co-presence CLI wiring > operator help names the create, attach, and stop commands [1.17ms] (pass) OpenCode co-presence CLI wiring > prints an exact tmux target so an exited TUI cannot prefix-match the bridge [0.07ms] src/locale-diagnostic.test.ts: -(pass) #68 locale diagnostic > LC_ALL overrides an otherwise UTF-8 LANG [2.06ms] -(pass) #68 locale diagnostic > LC_CTYPE overrides LANG when LC_ALL is empty [0.14ms] -(pass) #68 locale diagnostic > accepts common UTF-8 spellings [0.09ms] -(pass) #68 locale diagnostic > warns for POSIX, C, non-UTF-8, and unset locale [0.12ms] +(pass) #68 locale diagnostic > LC_ALL overrides an otherwise UTF-8 LANG [1.65ms] +(pass) #68 locale diagnostic > LC_CTYPE overrides LANG when LC_ALL is empty [0.06ms] +(pass) #68 locale diagnostic > accepts common UTF-8 spellings [0.07ms] +(pass) #68 locale diagnostic > warns for POSIX, C, non-UTF-8, and unset locale [0.09ms] (pass) #68 locale diagnostic > does not prescribe POSIX locale variables on Windows [0.05ms] -(pass) #68 locale diagnostic > renders locale values without terminal control or unbounded output [0.24ms] +(pass) #68 locale diagnostic > renders locale values without terminal control or unbounded output [0.39ms] src/opencode-package-binary.test.ts: -(pass) validateOpencodePackageBinary > accepts only the canonical exact npm package entrypoint [3.11ms] -(pass) validateOpencodePackageBinary > rejects a same-version package impersonator inside the project [1.40ms] -(pass) validateOpencodePackageBinary > skips a same-version project shim and selects a later trusted package [2.06ms] -(pass) validateOpencodePackageBinary > rejects a monorepo-root package when invoked from a nested app [3.96ms] -(pass) validateOpencodePackageBinary > ordinary 0664 checkout package.json does not abort boundary discovery [2.55ms] -(pass) validateOpencodePackageBinary > accepts both exact registry spellings of bin.opencode [3.02ms] -(pass) validateOpencodePackageBinary > rejects forged name, version, and bin metadata [2.99ms] -(pass) validateOpencodePackageBinary > rejects world-writable files and package ancestors [2.37ms] -(pass) validateOpencodePackageBinary > rejects a symlinked package.json even when its contents are exact [0.96ms] -(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd 为文件系统根时,禁止根含 / —— 与任何包路径都重叠 [0.19ms] -(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd=/ 时,一个各方面都合法的包也会被拒 [2.44ms] -(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd 是全局安装前缀的祖先时,全局安装的包被判成项目本地 [1.27ms] -(pass) #739 cwd 参与信任判定 > 这条守卫要防的东西必须继续被防住(修 #739 时不许放宽它) [0.89ms] +(pass) validateOpencodePackageBinary > accepts only the canonical exact npm package entrypoint [2.43ms] +(pass) validateOpencodePackageBinary > rejects a same-version package impersonator inside the project [1.53ms] +(pass) validateOpencodePackageBinary > skips a same-version project shim and selects a later trusted package [2.90ms] +(pass) validateOpencodePackageBinary > rejects a monorepo-root package when invoked from a nested app [3.66ms] +(pass) validateOpencodePackageBinary > ordinary 0664 checkout package.json does not abort boundary discovery [1.14ms] +(pass) validateOpencodePackageBinary > accepts both exact registry spellings of bin.opencode [2.36ms] +(pass) validateOpencodePackageBinary > rejects forged name, version, and bin metadata [2.89ms] +(pass) validateOpencodePackageBinary > rejects world-writable files and package ancestors [2.70ms] +(pass) validateOpencodePackageBinary > rejects a symlinked package.json even when its contents are exact [1.26ms] +(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd 为文件系统根时,禁止根含 / —— 与任何包路径都重叠 [0.21ms] +(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd=/ 时,一个各方面都合法的包也会被拒 [1.39ms] +(pass) #739 cwd 参与信任判定 > 缺陷现状:cwd 是全局安装前缀的祖先时,全局安装的包被判成项目本地 [1.21ms] +(pass) #739 cwd 参与信任判定 > 这条守卫要防的东西必须继续被防住(修 #739 时不许放宽它) [0.93ms] src/im/access-resolve.test.ts: -(pass) normalizeAllowFrom — input shapes > real string[] passes through deduped (filter empty strings) [1.97ms] -(pass) normalizeAllowFrom — input shapes > undefined → empty + not malformed [0.06ms] -(pass) normalizeAllowFrom — input shapes > null → empty + not malformed [0.03ms] -(pass) normalizeAllowFrom — input shapes > non-array object → empty + malformed (corrupted access.json shape) [0.05ms] -(pass) normalizeAllowFrom — input shapes > string instead of array → malformed [0.05ms] +(pass) normalizeAllowFrom — input shapes > real string[] passes through deduped (filter empty strings) [2.04ms] +(pass) normalizeAllowFrom — input shapes > undefined → empty + not malformed [0.07ms] +(pass) normalizeAllowFrom — input shapes > null → empty + not malformed [0.04ms] +(pass) normalizeAllowFrom — input shapes > non-array object → empty + malformed (corrupted access.json shape) [0.04ms] +(pass) normalizeAllowFrom — input shapes > string instead of array → malformed [0.03ms] (pass) normalizeAllowFrom — input shapes > array with non-string elements drops them [0.07ms] -(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > empty array → deny with empty-fail-closed kind [0.18ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > empty array → deny with empty-fail-closed kind [0.26ms] (pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > undefined → deny [0.06ms] -(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > malformed → deny + reason mentions malformed [0.11ms] -(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*'] alone allows any sender [0.05ms] -(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*', 'specific_id'] still wildcard-allows (wins precedence) [0.04ms] -(pass) resolveTelegramAccess — explicit id / username matching > senderId in list → allow [0.11ms] -(pass) resolveTelegramAccess — explicit id / username matching > senderUsername match (no id match) → allow [0.05ms] -(pass) resolveTelegramAccess — explicit id / username matching > neither id nor username in list → deny [0.05ms] +(pass) resolveTelegramAccess — fail-closed empty allowFrom (v0.11 security change) > malformed → deny + reason mentions malformed [0.12ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*'] alone allows any sender [0.07ms] +(pass) resolveTelegramAccess — wildcard '*' opens the channel > ['*', 'specific_id'] still wildcard-allows (wins precedence) [0.06ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderId in list → allow [0.10ms] +(pass) resolveTelegramAccess — explicit id / username matching > senderUsername match (no id match) → allow [0.07ms] +(pass) resolveTelegramAccess — explicit id / username matching > neither id nor username in list → deny [0.06ms] (pass) resolveTelegramAccess — explicit id / username matching > empty senderUsername doesn't accidentally match empty list entry [0.04ms] -(pass) resolveTelegramAccess — explicit id / username matching > blank-string id with username match still allows [0.07ms] -(pass) resolveTelegramAccess — explicit id / username matching > production-shape: bare username (no @) in allowFrom matches bare msg.from.username [0.09ms] -(pass) resolveTelegramAccess — explicit id / username matching > production-shape mismatch: @vansin in allowFrom does NOT match bare vansin payload [0.04ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > empty allowFrom → deny [0.22ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > wildcard allows [0.06ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > specific id allows [0.05ms] -(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > sender not in list → deny [0.05ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > empty allowChats → fail-closed [0.10ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=all → allow [0.32ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=observe → deny [0.07ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat NOT in allowChats → deny (even with policy=all) [0.14ms] +(pass) resolveTelegramAccess — explicit id / username matching > blank-string id with username match still allows [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > production-shape: bare username (no @) in allowFrom matches bare msg.from.username [0.05ms] +(pass) resolveTelegramAccess — explicit id / username matching > production-shape mismatch: @vansin in allowFrom does NOT match bare vansin payload [0.05ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > empty allowFrom → deny [0.25ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > wildcard allows [0.09ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > specific id allows [0.06ms] +(pass) resolveFeishuAccess — DM path mirrors telegram fail-closed > sender not in list → deny [0.06ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > empty allowChats → fail-closed [0.11ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=all → allow [0.07ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat in allowChats + groupPolicy=observe → deny [0.06ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > chat NOT in allowChats → deny (even with policy=all) [0.11ms] (pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > wildcard chats opens any chat (with groupPolicy=all) [0.06ms] -(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > groupPolicy=mention allows (caller decides at message inspect time) [0.05ms] -(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for empty allowFrom [0.13ms] -(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for malformed allowFrom + mentions malformed [0.06ms] -(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null when allowFrom has at least one entry [0.05ms] -(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null for wildcard-allow (channel intentionally open) [0.03ms] +(pass) resolveFeishuAccess — group path (allowChats + groupPolicy) > groupPolicy=mention allows (caller decides at message inspect time) [0.06ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for empty allowFrom [0.15ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns warn string for malformed allowFrom + mentions malformed [0.05ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null when allowFrom has at least one entry [0.12ms] +(pass) buildEmptyAllowlistWarn — boot-time visibility > returns null for wildcard-allow (channel intentionally open) [0.04ms] (pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader stores raw allowFrom verbatim — no normalization at load time [0.13ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is missing [0.08ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is malformed (non-array) [0.07ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader is silent when allowFrom has at least one entry (even if numeric) [0.07ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123] alone (numeric sender id from a misformatted access.json) → loader+resolver fail-closed [0.08ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null] (corrupted access.json) → loader+resolver fail-closed [0.06ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [{}] (object instead of id string) → loader+resolver fail-closed [0.06ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123, '@vansin'] (mixed) → '@vansin' still allowed, numeric '123' rejected [0.08ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null, '*'] (mixed wildcard) → wildcard wins despite garbage entries [0.05ms] -(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > missing access.json entirely (loader gets null) → fail-closed [0.08ms] -(pass) regression — pre-v0.11 fail-open MUST NOT come back > empty array NEVER allows [0.06ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is missing [0.04ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader emits boot-warn when allowFrom is malformed (non-array) [0.05ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > loader is silent when allowFrom has at least one entry (even if numeric) [0.05ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123] alone (numeric sender id from a misformatted access.json) → loader+resolver fail-closed [0.06ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null] (corrupted access.json) → loader+resolver fail-closed [0.05ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [{}] (object instead of id string) → loader+resolver fail-closed [0.04ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [123, '@vansin'] (mixed) → '@vansin' still allowed, numeric '123' rejected [0.10ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > [null, '*'] (mixed wildcard) → wildcard wins despite garbage entries [0.08ms] +(pass) loadTelegramAccess + resolver — wiring regression (CHANGE_REQ on #276) > missing access.json entirely (loader gets null) → fail-closed [0.12ms] +(pass) regression — pre-v0.11 fail-open MUST NOT come back > empty array NEVER allows [0.05ms] (pass) regression — pre-v0.11 fail-open MUST NOT come back > undefined NEVER allows [0.04ms] (pass) regression — pre-v0.11 fail-open MUST NOT come back > null NEVER allows [0.03ms] (pass) regression — pre-v0.11 fail-open MUST NOT come back > object-shape (corrupted) NEVER allows [0.03ms] src/im/feishu/adapter-lifecycle.test.ts: -(pass) FeishuAdapter WS lifecycle > SDK start resolution is not readiness; missing onReady times out fail-closed [22.28ms] -(pass) FeishuAdapter WS lifecycle > onReady is the only initial online authority [1.62ms] -(pass) FeishuAdapter WS lifecycle > initial onError rejects and scrubs credentials [1.63ms] -(pass) FeishuAdapter WS lifecycle > initial onError scrubs arbitrary Lark access-token shapes [1.64ms] -(pass) FeishuAdapter WS lifecycle > spurious reconnect before first ready cannot mark health connected [17.29ms] -[2026-08-13T00:27:07.115Z] [feishu:audit] error from=? conv=? — inbound [redacted] Bearer [redacted] -(pass) FeishuAdapter WS lifecycle > inbound handler errors use the same token scrub before health [4.32ms] -(pass) FeishuAdapter WS lifecycle > reconnecting lowers health and reconnected restores it [3.61ms] -(pass) FeishuAdapter WS lifecycle > terminal error after ready lowers health and notifies worker owner once [1.61ms] -(pass) FeishuAdapter WS lifecycle > stop closes the public SDK client and invalidates late callbacks [1.65ms] -(pass) worker terminal owner logs safely and exits non-zero [0.31ms] +(pass) FeishuAdapter WS lifecycle > SDK start resolution is not readiness; missing onReady times out fail-closed [25.59ms] +(pass) FeishuAdapter WS lifecycle > onReady is the only initial online authority [1.77ms] +(pass) FeishuAdapter WS lifecycle > initial onError rejects and scrubs credentials [1.61ms] +(pass) FeishuAdapter WS lifecycle > initial onError scrubs arbitrary Lark access-token shapes [1.61ms] +(pass) FeishuAdapter WS lifecycle > spurious reconnect before first ready cannot mark health connected [15.82ms] +[2026-08-13T02:13:24.110Z] [feishu:audit] error from=? conv=? — inbound [redacted] Bearer [redacted] +(pass) FeishuAdapter WS lifecycle > inbound handler errors use the same token scrub before health [6.93ms] +(pass) FeishuAdapter WS lifecycle > reconnecting lowers health and reconnected restores it [1.47ms] +(pass) FeishuAdapter WS lifecycle > terminal error after ready lowers health and notifies worker owner once [1.50ms] +(pass) FeishuAdapter WS lifecycle > stop closes the public SDK client and invalidates late callbacks [1.59ms] +(pass) worker terminal owner logs safely and exits non-zero [0.14ms] 438 pass 0 fail 1333 expect() calls -Ran 438 tests across 46 files. [4.13s] +Ran 438 tests across 46 files. [4.66s] executed_files=46 discovered_files=46 [L0b] every agent-network/tests file, dispatched by kind tests_dir_executed=19 tests_dir_discovered=19 tests_dir_failed=0 From 152455a64ad2053b4a37738a803e5512d9e1e06a Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 10:38:13 +0800 Subject: [PATCH 06/11] =?UTF-8?q?ci:=20=E5=85=83=E9=97=A8=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E4=BF=AE=E6=8E=89=E7=8B=AC=E7=AB=8B=E5=AE=A1?= =?UTF-8?q?=E6=8A=93=E5=87=BA=E7=9A=84=E4=B8=89=E6=9D=A1=20P1(=E5=85=B6?= =?UTF-8?q?=E4=B8=AD=E4=B8=80=E6=9D=A1=E6=98=AF=E5=85=83=E9=97=A8=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E7=9A=84=E6=BC=8F=E7=BD=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 独立审(codex)在本 PR 上提了三条 P1,逐条复现后全部成立: 1) **深度不感知 —— 元门自己放行了没人会跑的测试。** 两个 unit runner 扫 `/tests` 用的是 `find … -maxdepth 1`,而本脚本 原来只按前缀判覆盖。复现:把一个测试放到 `agent-network/tests/sub/` 下, 元门报「0 个漏网」rc=0,而 runner 的 find 对它命中 0。 **这正是这道门存在的意义所在,它却在自己身上漏了。** 修法:深度从门里推导(scan_depth),不假定递归;`bun test /` 形式按递归算。 双向验过:子目录文件 → rc=1 且点名;直属文件 → rc=0。 2) **套件豁免不校验套件是否真实存在。** 原来只要路径以 `tests/` 开头就放行,于是 `tests/test999-example/new.test.ts` 这种既没 Dockerfile 也没 run.sh 的目录也能过 —— 豁免变成「只要放对地方 就不用被任何东西跑」。改成要求套件目录里 Dockerfile 和 run.sh 都在。 双向验过:伪套件 → rc=1;补上两个文件 → rc=0。 3) **qa.yml 改动不触发本门。** qa.yml 决定那三个聚合门到底跑不跑,它一改本门的前提就可能塌, 但它不在触发路径里。已加进两处 path 过滤。 NOT COVERED(第 2 条修完仍存在的缺口):校验了「套件是一套门」, 但**没有**校验「该套件已注册进 CI」。test224/test597/test679 就长期 有完整 Dockerfile+run.sh 却没人跑 —— 那是 #803 在解决的问题,不是本门的判据。 --- .github/scripts/check-test-file-coverage.py | 204 ++++++++++++++++++++ .github/workflows/test-file-coverage.yml | 49 +++++ 2 files changed, 253 insertions(+) create mode 100755 .github/scripts/check-test-file-coverage.py create mode 100644 .github/workflows/test-file-coverage.yml diff --git a/.github/scripts/check-test-file-coverage.py b/.github/scripts/check-test-file-coverage.py new file mode 100755 index 000000000..0b37b73f1 --- /dev/null +++ b/.github/scripts/check-test-file-coverage.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""元门:每个 *.test.ts 都必须落在某个聚合门的扫描范围里。 + +## 为什么需要这个 + +2026-08-13 手工扫了一遍,发现三处「有测试、但没有任何 CI job 会跑它」: + server/src 69 个,CI 只点名跑 6 个 + agent-network/src 46 个,0 个被引用(#791 补掉) + agent-network/tests 19 个 + agent-node/tests 6 个,两个门自称 complete 却漏了 + +每一处都是同一个结构:测试在本地是绿的,PR 上看不出异常,改坏了不会有人知道。 +补完之后剩下的问题是 —— **下一个新增的测试文件会不会又静默漏掉?** +靠人再扫一遍不是答案。这个脚本就是答案。 + +## 判据 + +聚合门用 `find -name '*.test.ts'` 覆盖若干个根。任何测试文件: + - 落在某个根下 → 被覆盖 + - 落在 tests/<套件>/ 下 → 属于「套件自带」,单独计数并列出(它们由各自的 + Docker 套件跑,是否进 CI 由套件决定,不在本门的判据里) + - 两者都不是 → **失败**。这是唯一的漏网形态:新包、新目录、或者把测试 + 放在了聚合门扫不到的地方。 + +## 两条防空转 + +1. **根必须真的是门的扫描范围**。COVERED 是一份声明,声明会漂 —— 门被删、 + 改名、或者把范围缩掉,这里就要红,否则本门会对着一份早已不成立的清单发绿。 + 注意这条**不能**用子串检查:第一版写的是 `root not in text`,mutation 当场 + 证伪 —— 把 find 的路径改成 $ROOT/server/nonexistent 之后,'server/src' 仍然 + 出现在注释和 FAIL 文案里,门照样绿。见 declares_scope()。 +2. **分母必须非零**。扫出 0 个测试文件时退出 3,而不是「没有违规,通过」—— + 扫描器范围塌掉和真的没有违规,打印出来是同一片绿色。 +""" + +import re +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] + +# root → 声称覆盖它的门(run.sh 路径)。该门必须把 root 真正声明为扫描范围,见 declares_scope()。 +COVERED = { + "server/src": "tests/test798-server-unit-ci/run.sh", + "agent-network/src": "tests/test745-agent-network-unit-ci/run.sh", + "agent-network/tests": "tests/test745-agent-network-unit-ci/run.sh", + "agent-node/src": "tests/test725-agent-node-unit-ci/run.sh", + "agent-node/tests": "tests/test725-agent-node-unit-ci/run.sh", +} + +# tests/<套件>/ 下的测试文件属于套件自带,单独计数 +SUITE_PREFIX = "tests/" + + + + +def suite_is_real(path: str) -> bool: + """`tests//x.test.ts` 只有在那个套件真的是一套门时才豁免。 + + 独立审(codex P1):原来只要路径以 `tests/` 开头就放行,于是 + `tests/test999-example/new.test.ts` 这种既没有 Dockerfile 也没有 run.sh 的 + 目录也能过 —— 豁免变成了「只要放对地方就不用被任何东西跑」。 + 所以要求套件目录里 Dockerfile 和 run.sh 都在。 + + 注意这条**仍然不保证该套件进了 CI**(它可能像 test224/597/679 那样长期 + 没人注册)。那是另一回事,写在 NOT COVERED 里。 + """ + parts = path.split("/") + if len(parts) < 3: + return False + suite = REPO / parts[0] / parts[1] + return (suite / "Dockerfile").is_file() and (suite / "run.sh").is_file() + + +def scan_depth(gate_text: str, root: str) -> int | None: + """门扫这个根时的深度上限:1 = 只扫直属文件,None = 递归。 + + 这条是独立审(codex P1)抓出来的,而且当场复现:两个 unit runner 扫 + `/tests` 用的是 `find … -maxdepth 1`,而本脚本原来只按前缀判覆盖 —— + 于是 `agent-network/tests/sub/x.test.ts` 被判为「已覆盖」,可 runner 的 + find 对它命中 0。**这道门放行了一个没人会跑的测试**,正是它存在的意义所在。 + + 所以深度必须从门里推导,不能假定。 + """ + m = re.search( + rf'find\s+"\$ROOT/{re.escape(root)}"\s+(?P(?:-maxdepth\s+\d+\s+)?)', + gate_text, + ) + if m: + d = re.search(r'-maxdepth\s+(\d+)', m.group("flags") or "") + return int(d.group(1)) if d else None + return None # `bun test /` 形式:bun 会递归 + + +def declares_scope(gate_text: str, root: str) -> bool: + """门里必须真的把 root 当成扫描范围,而不是只在注释里提到它。 + + 第一版这里写的是 `root not in gate_text` —— 子串检查。mutation 当场证明 + 它是坏的:把 find 的路径从 $ROOT/server/src 改成 $ROOT/server/nonexistent + 之后,'server/src' 仍然出现在注释和 FAIL 文案里,门照样发绿。 + 宽容的断言会把不合规当合规收下。所以只认两种真实的范围声明形式。 + """ + pkg, _, sub = root.partition("/") + patterns = [ + # find "$ROOT/" … -name '*.test.ts' + rf'find\s+"\$ROOT/{re.escape(root)}"', + # cd /workspace/ && bun test / + # 结尾必须锚定:`bun test src/` 才算声明整个目录。不锚的话 + # `bun test src/cli.test.ts` 也会匹配上 —— 范围收窄到单个文件, + # 门却仍然宣称覆盖了整个 src/。第二轮 mutation 就是这么活下来的。 + rf'cd\s+/workspace/{re.escape(pkg)}\s+&&\s+bun test\s+{re.escape(sub)}/(?=[\'"\s]|$)', + ] + return any(re.search(p, gate_text) for p in patterns) + + +def tracked_test_files() -> list[str]: + out = subprocess.run( + ["git", "ls-files", "*.test.ts"], + cwd=REPO, capture_output=True, text=True, check=True, + ).stdout + return sorted(p for p in out.splitlines() if p) + + +def main() -> int: + failures: list[str] = [] + + # 防空转 1:每个声明的根都要在它声称的门里字面出现 + for root, gate in COVERED.items(): + gate_path = REPO / gate + if not gate_path.is_file(): + failures.append(f"门不存在:{gate}(声称覆盖 {root})") + continue + text = gate_path.read_text(encoding="utf-8") + if not declares_scope(text, root): + failures.append( + f"门 {gate} 没有把 '{root}' 声明为扫描范围 —— " + "覆盖声明与门的实际范围已经不一致" + ) + + files = tracked_test_files() + print(f"tracked_test_files={len(files)}") + + # 防空转 2:分母为零说明扫描范围塌了,不是「没有违规」 + if not files: + print("FAIL: 扫到 0 个 *.test.ts —— 扫描范围塌了,不是通过", file=sys.stderr) + return 3 + + by_root: dict[str, int] = {r: 0 for r in COVERED} + suite_files: list[str] = [] + orphans: list[str] = [] + + depths = { + root: scan_depth((REPO / gate).read_text(encoding="utf-8"), root) + if (REPO / gate).is_file() else None + for root, gate in COVERED.items() + } + for f in files: + for root in COVERED: + if not f.startswith(root + "/"): + continue + rest = f[len(root) + 1:] + d = depths[root] + if d is not None and rest.count("/") >= d: + # 落在门扫不到的深度里 —— 加了也不会有人跑,按漏网处理 + continue + by_root[root] += 1 + break + else: + if f.startswith(SUITE_PREFIX) and suite_is_real(f): + suite_files.append(f) + else: + orphans.append(f) + + for root, n in sorted(by_root.items()): + print(f" covered {root:<22} {n}") + print(f" suite-owned (tests//) {len(suite_files)}") + for f in suite_files: + print(f" {f}") + + total = sum(by_root.values()) + len(suite_files) + len(orphans) + if total != len(files): + failures.append(f"计数不闭合:分类合计 {total} != 文件数 {len(files)}") + + if orphans: + failures.append( + "以下测试文件不在任何聚合门的扫描范围里 —— 加了也不会有人跑:\n" + + "\n".join(f" {f}" for f in orphans) + + "\n 要么把它挪进已覆盖的根,要么给它所在的包补一个聚合门" + "(照 tests/test798-server-unit-ci 的形状)。" + ) + + if failures: + print() + for msg in failures: + print(f"FAIL: {msg}", file=sys.stderr) + return 1 + + print(f"\nOK: {len(files)} 个测试文件,{len(files) - len(suite_files)} 个在聚合门范围内," + f"{len(suite_files)} 个套件自带,0 个漏网") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/test-file-coverage.yml b/.github/workflows/test-file-coverage.yml new file mode 100644 index 000000000..5d0358e50 --- /dev/null +++ b/.github/workflows/test-file-coverage.yml @@ -0,0 +1,49 @@ +# 元门:新增的 *.test.ts 不能落在所有聚合门的扫描范围之外。 +# +# 起因是 2026-08-13 手工扫出的三处盲区(server/src 69 个 CI 只跑 6 个、 +# agent-network/src 46 个 0 被引用、两个门自称 complete 却漏了 tests/ 下 25 个)。 +# 那三处都补掉了,但补完剩下的问题是:下一个新增的测试文件会不会又静默漏掉。 +# 靠人再扫一遍不是答案,所以有了这道门。 +# +# 判据和两条防空转见 .github/scripts/check-test-file-coverage.py 的文档串。 +# 用 Python 而不是 yml 里的 bash 循环,同 no-memory-slugs.yml 的理由。 + +name: lint (every test file is covered by a gate) + +on: + pull_request: + paths: + - '**/*.test.ts' + - 'tests/test725-agent-node-unit-ci/**' + - 'tests/test745-agent-network-unit-ci/**' + - 'tests/test798-server-unit-ci/**' + # qa.yml 决定这三个门到底跑不跑 —— 它一改,本门的前提就可能塌(codex P1) + - '.github/workflows/qa.yml' + - '.github/scripts/check-test-file-coverage.py' + - '.github/workflows/test-file-coverage.yml' + push: + branches: [main] + paths: + - '**/*.test.ts' + - 'tests/test725-agent-node-unit-ci/**' + - 'tests/test745-agent-network-unit-ci/**' + - 'tests/test798-server-unit-ci/**' + # qa.yml 决定这三个门到底跑不跑 —— 它一改,本门的前提就可能塌(codex P1) + - '.github/workflows/qa.yml' + - '.github/scripts/check-test-file-coverage.py' + - '.github/workflows/test-file-coverage.yml' + +concurrency: + group: lint-test-coverage-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test-file-coverage: + name: every test file is covered by a gate + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v4 + + - name: Run check-test-file-coverage.py + run: python3 .github/scripts/check-test-file-coverage.py From 9626c98e4d301d88822cbb3031753fd94a946ed8 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 11:21:10 +0800 Subject: [PATCH 07/11] =?UTF-8?q?ci:=20=E5=85=83=E9=97=A8=E8=A6=81?= =?UTF-8?q?=E9=AA=8C=E3=80=8C=E8=BF=99=E9=81=93=E9=97=A8=E7=9C=9F=E7=9A=84?= =?UTF-8?q?=E8=A2=AB=20CI=20=E8=B7=91=E3=80=8D,=E4=B8=8D=E5=8F=AA=E6=98=AF?= =?UTF-8?q?=E3=80=8C=E5=AE=83=E5=AD=98=E5=9C=A8=E4=B8=94=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E4=BA=86=E8=8C=83=E5=9B=B4=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 独立审(codex P1)指出的缺口,我上一版只在 NOT COVERED 里记了没修: qa.yml 一旦删掉/改名某个 job、或不再 build/run 它的 Dockerfile, 本脚本照样发绿 —— 因为它从没看过 qa.yml。 **这正是本门要防的那类问题(有门、没人跑),不能留在自己身上。** 判据要求 qa.yml 里同时出现两件事,单独一条不算: -f tests//Dockerfile 真的构建了它 docker run … <这次 build 打的 tag> 真的跑了那个产物 两条解耦 mutation,各自红在不同原因上(基线绿): F1 删掉 server-unit 的 docker run(build 保留) → rc=1「qa.yml 构建了 anet-test798-server-unit 但没有 docker run 它」 F2 把 test745 的 build -f 路径改名 → rc=1「qa.yml 里没有 build tests/test745-agent-network-unit-ci/Dockerfile」 一道门可能覆盖多个根(test745 覆盖 src 与 tests),接线问题去重后只报一次。 --- .github/scripts/check-test-file-coverage.py | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/scripts/check-test-file-coverage.py b/.github/scripts/check-test-file-coverage.py index 0b37b73f1..6ee42252b 100755 --- a/.github/scripts/check-test-file-coverage.py +++ b/.github/scripts/check-test-file-coverage.py @@ -54,6 +54,38 @@ +WORKFLOW = REPO / ".github" / "workflows" / "qa.yml" + + +def gate_is_wired(gate: str) -> tuple[bool, str]: + """这道门有没有真的被 CI 构建并运行。 + + 原来只验了两件事:门文件存在、门声明了扫描范围。**都不等于它会跑。** + 独立审(codex P1)指出:qa.yml 一旦删掉或改名某个 job、或不再 build/run + 它的 Dockerfile,本脚本照样发绿 —— 因为它从没看过 qa.yml。 + 这正是本门要防的那类问题(有门、没人跑),所以不能留在自己身上。 + + 判据是 qa.yml 里同时出现: + - `-f tests//Dockerfile`(真的构建了它) + - `docker run … <这次 build 打的 tag>`(真的跑了那个产物) + 只比 tag 字符串,不解析 YAML —— 但两条都要中,单独一条不算。 + """ + suite = Path(gate).parent.name + if not WORKFLOW.is_file(): + return False, "qa.yml 不存在" + wf = WORKFLOW.read_text(encoding="utf-8") + build = re.search(rf'-f\s+tests/{re.escape(suite)}/Dockerfile', wf) + if not build: + return False, f"qa.yml 里没有 build tests/{suite}/Dockerfile" + tags = re.findall(rf'-t\s+(\S+)\s+\\?\s*\n?\s*-f\s+tests/{re.escape(suite)}/Dockerfile', wf) + if not tags: + return False, f"qa.yml 里 build tests/{suite} 时没有 -t " + tag = tags[0] + if not re.search(rf'docker run[^\n]*\b{re.escape(tag)}\b', wf): + return False, f"qa.yml 构建了 {tag} 但没有 docker run 它" + return True, tag + + def suite_is_real(path: str) -> bool: """`tests//x.test.ts` 只有在那个套件真的是一套门时才豁免。 @@ -136,6 +168,11 @@ def main() -> int: f"门 {gate} 没有把 '{root}' 声明为扫描范围 —— " "覆盖声明与门的实际范围已经不一致" ) + wired, why = gate_is_wired(gate) + # 一道门可能覆盖多个根(test745 覆盖 src 和 tests),接线问题只报一次 + msg = f"门 {gate} 没有接进 CI:{why}" + if not wired and msg not in failures: + failures.append(msg) files = tracked_test_files() print(f"tracked_test_files={len(files)}") From c06df3c92da15780fd8fd9402c3a63ab65452fd3 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 11:21:10 +0800 Subject: [PATCH 08/11] =?UTF-8?q?docs(tests):=20report-only=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E9=94=9A=E7=82=B9=209626c98e,=E4=B8=83=E6=9D=A1=20?= =?UTF-8?q?mutation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report-test-file-coverage-meta-gate.txt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/tests/report-test-file-coverage-meta-gate.txt diff --git a/docs/tests/report-test-file-coverage-meta-gate.txt b/docs/tests/report-test-file-coverage-meta-gate.txt new file mode 100644 index 000000000..963d4d0cb --- /dev/null +++ b/docs/tests/report-test-file-coverage-meta-gate.txt @@ -0,0 +1,30 @@ +# 元门 check-test-file-coverage.py +source_commit=9626c98e4d301d88822cbb3031753fd94a946ed8 +base(current main)=034f00647d42d38d5086d7fc057eb7824a441791 +stack: 含 #798 与 #800 的源码 + +## 基线 +tracked_test_files=236 + covered agent-network/src 46 + covered agent-network/tests 19 + covered agent-node/src 91 + covered agent-node/tests 6 + covered server/src 69 + suite-owned (tests//) 5 + tests/test224-grok-preview-security/security-gate.test.ts + tests/test597-dashboard-slash-namespace/cli-wire.test.ts + tests/test679-task-trace/wiring.test.ts + tests/test682-uncovered-task-trace/semantics.test.ts + tests/test682-uncovered-task-trace/wiring.test.ts + +OK: 236 个测试文件,231 个在聚合门范围内,5 个套件自带,0 个漏网 +rc=0 + +## mutation(七条,全部双向验过) +A 落在任何根之外的新文件 → rc=1 点名 +B find 范围改成 nonexistent → rc=1「没有把 server/src 声明为扫描范围」 +C bun test src/ 收窄成单文件 → rc=1(加结尾锚定后才红,第一版活下来过) +D 伪套件 tests/test999-example/ → rc=1;补 Dockerfile+run.sh 后 rc=0 +E 子目录 agent-network/tests/sub/ → rc=1;直属文件 rc=0(独立审抓出的,不是我自己发现的) +F1 删掉 server-unit 的 docker run → rc=1「构建了但没有 docker run 它」 +F2 build -f 路径改名 → rc=1「没有 build tests/…/Dockerfile」 From 809faac0cb12dbcdfcf0f701aa152a72a9ee1184 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 17:11:00 +0800 Subject: [PATCH 09/11] =?UTF-8?q?ci:=20=E8=90=BD=E5=AE=9E=20=E2=91=A4?= =?UTF-8?q?=E2=91=A5=20=E4=B8=A4=E6=9D=A1=E5=B7=B2=E6=8E=A5=E5=8F=97?= =?UTF-8?q?=E6=9C=AA=E5=AE=9E=E6=96=BD=E7=9A=84=E6=84=8F=E8=A7=81;?= =?UTF-8?q?=E2=91=A1=20=E9=9C=80=E6=89=80=E6=9C=89=E8=80=85=E5=86=B3?= =?UTF-8?q?=E5=AE=9A,=E5=A6=82=E5=AE=9E=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⑥ SOURCE_COMMIT 只验格式不验字节 原来只验 ^[0-9a-f]{40}$。任何 SHA 都能过,而审查指出提交进来的 report 里那个 SHA 早于本套件自身 —— 那份证据无法从它自称的版本复现。 改成与 test823 相同的做法:构建时把 run.sh 在该 commit 下的 git blob 哈希作为 build-arg 传入,容器内就地重算比对(blob 哈希 = sha1("blob \0"+内容),不需要容器里装 git)。 已验脚本内算法与 git hash-object 结果一致;该机制的端到端红/绿在 #835 上证过两次(传错 blob、blob 对但文件被篡改,都 exit 1)。 ⑤ qa.yml 缺 test601 路径 test798 的镜像 COPY 了 test601 的 race-worker.ts,而 server/src/scheduled-tasks-http.test.ts 会执行它做「两个真 Hub 抢同一 occurrence」。只改那个 worker 的 PR 不该跳过这道门。已在两处 paths 补上。 (这 4 行原本只存在于 #798;若只合本 PR、把 #798 当冗余关掉,它们永远 不会落地 —— 此前已在本 PR 记录过这个坑。) ② server 的 npm install 无 lockfile —— 我没有改,需要所有者决定 实测:server/package.json 有 4 个依赖,4 个全用 caret 范围,且仓里没有 任何 lockfile/shrinkwrap。所以同一个 commit 在不同时间构建确实会解析出 不同依赖图,审查这条成立。 但修法只有一条:提交一份 lockfile。那是仓库级的依赖钉死决策 —— 它影响 每一次 server 构建,不只是这道门;而且生成出来的树我无法在这里验证是否 仍然全绿。这不该由我单方面决定,如实留作待决,不假装已修。 --- .github/workflows/qa.yml | 8 ++++++++ tests/test798-server-unit-ci/Dockerfile | 4 ++++ tests/test798-server-unit-ci/run.sh | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 5c159a600..5d248f33a 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -23,6 +23,10 @@ on: - 'tests/test745-agent-network-unit-ci/**' - 'tests/test746-setup-bun-pin/**' - 'tests/test798-server-unit-ci/**' + # test798 的镜像 COPY 了 test601 的 race-worker.ts,且 + # server/src/scheduled-tasks-http.test.ts 会执行它做「两个真 Hub 抢同一 + # occurrence」—— 只改那个 worker 的 PR 不该跳过这道门。 + - 'tests/test601-hub-scheduled-tasks/**' push: branches: [main] paths: @@ -36,6 +40,10 @@ on: - 'tests/test745-agent-network-unit-ci/**' - 'tests/test746-setup-bun-pin/**' - 'tests/test798-server-unit-ci/**' + # test798 的镜像 COPY 了 test601 的 race-worker.ts,且 + # server/src/scheduled-tasks-http.test.ts 会执行它做「两个真 Hub 抢同一 + # occurrence」—— 只改那个 worker 的 PR 不该跳过这道门。 + - 'tests/test601-hub-scheduled-tasks/**' # Older runs on the same ref get cancelled — saves minutes when a PR is # updated rapidly. main pushes run independently. diff --git a/tests/test798-server-unit-ci/Dockerfile b/tests/test798-server-unit-ci/Dockerfile index 4e91a2a04..a08432406 100644 --- a/tests/test798-server-unit-ci/Dockerfile +++ b/tests/test798-server-unit-ci/Dockerfile @@ -35,7 +35,11 @@ COPY tests/test601-hub-scheduled-tasks ./tests/test601-hub-scheduled-tasks COPY tests/test798-server-unit-ci/run.sh ./tests/test798-server-unit-ci/run.sh ARG SOURCE_COMMIT +ARG RUNSH_BLOB ENV TEST798_SOURCE_COMMIT=$SOURCE_COMMIT +# run.sh 在 SOURCE_COMMIT 下的 git blob 哈希 —— 让容器内能验证「报告里的 SHA +# 确实对应镜像里被测的字节」,而不是只验 SHA 的格式。 +ENV TEST798_RUNSH_BLOB=$RUNSH_BLOB RUN chmod 0755 ./tests/test798-server-unit-ci/run.sh \ && install -d -o node -g node -m 0700 "/run/user/$(id -u node)" \ diff --git a/tests/test798-server-unit-ci/run.sh b/tests/test798-server-unit-ci/run.sh index 33672258b..0e48c36d9 100755 --- a/tests/test798-server-unit-ci/run.sh +++ b/tests/test798-server-unit-ci/run.sh @@ -11,11 +11,30 @@ set -euo pipefail ROOT=/workspace SOURCE_COMMIT=${TEST798_SOURCE_COMMIT:-} +# 🔴 光验格式不够:任何 40 位十六进制都能通过,而报告里那个 SHA 可能根本不含 +# 镜像里被测的文件。审查指出提交进来的 report 就写着一个早于本套件自身的 +# 修订号 —— 那份证据无法从它自称的版本复现。 +# 做法(与 test823 同):构建时把 run.sh 在该 commit 下的 git blob 哈希作为 +# build-arg 传入,这里就地重算镜像内文件的 blob 哈希并比对。 +# blob 哈希 = sha1("blob \0" + 内容),不需要容器里装 git。 [[ "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "FAIL: SOURCE_COMMIT must be one full lowercase Git SHA" >&2 exit 1 } +RUNSH_BLOB=${TEST798_RUNSH_BLOB:-} +[[ "$RUNSH_BLOB" =~ ^[0-9a-f]{40}$ ]] || { + echo "FAIL: TEST798_RUNSH_BLOB 缺失或格式不对 —— 无法把 SOURCE_COMMIT 绑到被测字节" >&2 + exit 1 +} +_self="$ROOT/tests/test798-server-unit-ci/run.sh" +_actual=$( { printf 'blob %d\0' "$(wc -c < "$_self")"; cat "$_self"; } | sha1sum | cut -d' ' -f1 ) +[[ "$_actual" == "$RUNSH_BLOB" ]] || { + echo "FAIL: 镜像里的 run.sh 与 SOURCE_COMMIT=$SOURCE_COMMIT 声称的不是同一份" >&2 + echo " 期望 blob $RUNSH_BLOB,实际 $_actual" >&2 + exit 1 +} + # 🔴 红线:COMMHUB_DB 不设的话默认指向生产库。容器里够不到宿主的库, # 但不能靠"够不到"来保证 —— 显式钉到容器内临时路径,并断言它真的被钉住了。 # 31/69 个 server 测试引用了 sqlite/COMMHUB_DB,这条不是形式主义。 From 7538cf0a6dc0a87ec8fd75265719432d2945e700 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 18:07:16 +0800 Subject: [PATCH 10/11] =?UTF-8?q?ci(test798):=20server=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E9=92=89=E6=AD=BB=20=E2=80=94=E2=80=94=20=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=20lockfile=20=E5=B9=B6=E6=94=B9=E7=94=A8=20npm=20ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查 ② 说的成立:server/package.json 4 个依赖全用 caret 且仓里没有 lockfile, 所以同一个 commit 在不同时间构建会解析出不同依赖图 —— 上游发一个兼容版本 就能让这道门变红或改变被测行为,而仓库一个字节都没动。 我上一版把这条标成"仓库级决策,不该由我单方面做"。那个定性是错的: agent-network/package-lock.json 已提交 docs-site/package-lock.json 已提交 prototype/anet-client-app/package-lock.json 已提交 5 个包里 3 个已经提交 lockfile,.gitignore 的 *.lock 也匹配不到 package-lock.json。提交它是本仓既有做法,server 与 agent-node 只是不一致。 真正卡住的是一次验证跑,不是授权 —— 我把成本问题说成了权限问题。 本次改动: - npm install --package-lock-only 生成 server/package-lock.json(1212 行, 未装 node_modules)。锁到的直接依赖: @modelcontextprotocol/sdk 1.30.0 / bun-types 1.3.14 / hono 4.13.1 / zod 4.4.3 - Dockerfile 改为 COPY package.json + package-lock.json,并把 npm install 换成 npm ci(ci 严格按 lockfile 装,install 会按 caret 取"当下最新兼容版")。 验证:带 lockfile 重建后跑完整套件 test_files=69 executed_files=69 failed_files=0 MUTATION_RED registration-password-floor-weakened rc=1 RESULT: PASS 退出码 0 --- server/package-lock.json | 1212 +++++++++++++++++++++++ tests/test798-server-unit-ci/Dockerfile | 7 +- 2 files changed, 1217 insertions(+), 2 deletions(-) create mode 100644 server/package-lock.json diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 000000000..16ccebe3a --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1212 @@ +{ + "name": "@sleep2agi/commhub-server", + "version": "0.9.0-preview.29", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sleep2agi/commhub-server", + "version": "0.9.0-preview.29", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "bun-types": "^1.3.13", + "hono": "^4.12.25", + "zod": "^4.4.3" + }, + "bin": { + "commhub-server": "bin/commhub.ts" + }, + "engines": { + "bun": ">=1.2.0" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", + "integrity": "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bun-types": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/tests/test798-server-unit-ci/Dockerfile b/tests/test798-server-unit-ci/Dockerfile index a08432406..f0701ac87 100644 --- a/tests/test798-server-unit-ci/Dockerfile +++ b/tests/test798-server-unit-ci/Dockerfile @@ -18,8 +18,11 @@ RUN curl --fail --silent --show-error --location \ && rm -f /tmp/bun-linux-x64.zip WORKDIR /workspace -COPY server/package.json ./server/ -RUN cd server && npm install +COPY server/package.json server/package-lock.json ./server/ +# 🔴 npm ci 而不是 npm install:ci 严格按 lockfile 装,install 会按 caret 解析 +# "当下最新的兼容版本"。后者意味着同一个 commit 在不同时间构建出不同依赖图 —— +# 上游发一个兼容版本就能让这道门变红或改变被测行为,而仓库一个字节都没动。 +RUN cd server && npm ci COPY server ./server # RFC-026 G9 / RFC-028 P1 的漂移门比对 hub 与 daemon 的同名共享源码, From cb2c6aaba68ca0f36eb26f7b16fbdbdb497e5d39 Mon Sep 17 00:00:00 2001 From: vansin Date: Fri, 14 Aug 2026 00:21:46 +0800 Subject: [PATCH 11/11] =?UTF-8?q?ci(test798):=20=E6=8A=8A=20RUNSH=5FBLOB?= =?UTF-8?q?=20=E7=9C=9F=E7=9A=84=E4=BC=A0=E8=BF=9B=E5=8E=BB=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E9=97=A8=E5=9C=A8=E8=A6=81=E6=B1=82=E5=AE=83,workf?= =?UTF-8?q?low=20=E4=BB=8E=E6=B2=A1=E4=BE=9B=E7=BB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 上 `server unit (Docker, non-root)` 稳定红,日志里唯一的失败行: FAIL: TEST798_RUNSH_BLOB 缺失或格式不对 —— 无法把 SOURCE_COMMIT 绑到被测字节 链条断在最后一环: run.sh:25-27 要求 TEST798_RUNSH_BLOB 且校验 ^[0-9a-f]{40}$,否则 fail-closed Dockerfile:41,45 ARG RUNSH_BLOB → ENV TEST798_RUNSH_BLOB qa.yml:79-84 docker build 只传 SOURCE_COMMIT,**没传 RUNSH_BLOB** 于是 ARG 取空、ENV 为空串、正则不过。门本身是对的 —— 它正确拒绝了一次 「说不清自己测了哪份字节」的运行,缺的只是供给那一行。 补法用 git 自己的 blob 哈希,和 run.sh:31 的算法是同一个东西: run.sh 算的是 sha1("blob \0" + 内容),那正是 git 的 blob object id。 本地实测两者一致(在本分支 head 上): git rev-parse HEAD:tests/test798-server-unit-ci/run.sh = 0e48c36d9ef516e60040ace54bc69c16c161e47f { printf 'blob %d\0' "$(wc -c < run.sh)"; cat run.sh; } | sha1sum = 0e48c36d9ef516e60040ace54bc69c16c161e47f 对照 #798:它的 run.sh 里 RUNSH_BLOB 命中 0 次 —— 所以这不是 #798 的回归, 是本 PR 新加的要求没接完线。 🔴 这一条只修 CI 红。独立审查另指出本 PR 仍夹带 #798 的旧版本、需在 #798 之后 rebase —— 那件事不在本提交范围内。 --- .github/workflows/qa.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 5d248f33a..9f27a6231 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -80,6 +80,7 @@ jobs: run: | docker build \ --build-arg SOURCE_COMMIT="$GITHUB_SHA" \ + --build-arg RUNSH_BLOB="$(git rev-parse HEAD:tests/test798-server-unit-ci/run.sh)" \ -t anet-test798-server-unit \ -f tests/test798-server-unit-ci/Dockerfile .