Skip to content

test: journal.append 失敗時に道具名と先頭行 outcome を16箇所すべてで測る - #755

Merged
takecchi merged 3 commits into
mainfrom
test/journal-notice-tool-name-and-outcome-guard
Sep 10, 2026
Merged

test: journal.append 失敗時に道具名と先頭行 outcome を16箇所すべてで測る#755
takecchi merged 3 commits into
mainfrom
test/journal-notice-tool-name-and-outcome-guard

Conversation

@takecchi

@takecchi takecchi commented Sep 9, 2026

Copy link
Copy Markdown
Owner

何を足したか

packages/core/src/tools.test.ts の末尾に、appendJournalOrThrow16箇所すべてについて、journal.append が落ちる条件で道具を呼び、応答本文が (1) 道具名expect(text).toContain(tool))と (2) 先頭行 = outcomeexpect(text.split('\n')[0]).toBe(firstLine))の両方を持つことを測る describe を1つ足した。

対象は15道具・16箇所(memory_section_move だけ move_in / move_out の2箇所):

  • act-completed(13箇所): memory_write / memory_append / memory_delete / memory_frontmatter_set / memory_section_move(move_out) / ask_human / schedule_create / schedule_remove / commitment_open / commitment_close / commitment_edit / profile_write / manager_start
  • act-not-performed(2箇所): journal_write / daily_report_write
  • act-partially-completed(1箇所): memory_section_move(move_in)

既存の describe('journal.append が失敗したとき…') は1文字も変えていない(callExpectingError / firstSectionId はこの新しい describe の中に複製して使っている)。実装(tools.ts / manager.ts)・testing.ts・名簿(SELF_JOURNALING_CLONE_TOOLS / TRACELESS_CLONE_TOOLS)はどれも触っていない。

なぜ先頭行も測るのか

既存の歯(describe('journal.append が失敗したとき…'))は outcome の3分類(act-completed / act-not-performed / act-partially-completed)を網羅するために選ばれた代表4件(memory_delete・journal_write・daily_report_write・memory_section_move の move_in)+ profile_write の秘密の歯だけを持っていた。道具名の toContain はその代表の中に相乗りしていただけで、母体(outcome の網羅)が変わらない限り、道具名だけを取り違えても既存の歯は落ちない——たとえば memory_append の呼び出しが誤って 'memory_write' という道具名で appendJournalOrThrow を呼んでも、act-completed の代表4件には元から memory_append が入っていないので何も言わない。この PR は16箇所それぞれを独立したケースにすることで、この相乗りを解消した。

SELF_JOURNALING_CLONE_TOOLS から導いた期待集合

依頼者の懸念「歯そのものが16件の数え上げになるので、17本目の道具を足した人が歯を足し忘れても何も落ちない」への手当てとして、CASES の道具名の集合を手で並べず、SELF_JOURNALING_CLONE_TOOLS(17本)から導いた:

const EXPECTED_TOOLS = SELF_JOURNALING_CLONE_TOOLS.filter(
  (name) => name !== 'manager_send' && name !== 'manager_stop',
);

manager_send / manager_stop を除く理由: この2本は ManagerPool のガード付き #journalclone.ts)を通るので appendJournalOrThrow を呼ばない——SELF_JOURNALING_CLONE_TOOLS は「自前で日誌へ書く」という性質の名簿であって、その書き方が appendJournalOrThrow 経由とは限らない。除くとちょうど15本になり、これが appendJournalOrThrow を呼ぶ道具の全部と一致する。

expect([...new Set(CASES.map(c => c.tool))].sort()).toEqual([...EXPECTED_TOOLS].sort()) を歯にしたので、18本目の自前記録の道具が名簿へ足されたとき(=17本目の appendJournalOrThrow 呼び出し道具)、この歯が「ケースが足りない」と言って赤くなる(導けたので、手で並べた場合の「16件の数え上げで17本目は自動で捕まえない」という断り書きは不要になった)。

「N回目だけ落ちる」偽ストア

memory_section_move の move_out は、move_in(1手目)が先に appendJournalOrThrow を呼ぶため、常に落ちるストア(failingJournalAppend)では move_in で投げ直されて move_out へ永久に届かない。そこでこの describe の中だけに閉じた failingJournalAppendAtCall(stores, failAt, reason)(1回目は通す・N回目だけ落ちる)を定義して使った。

⚠️ 共有の道具(packages/core/src/testing.ts)へ昇格させるかは未決。 同ファイルの flakyInboxRemove(先頭N回を失敗)とは失敗させる回の向きが逆なので、同じ関数に寄せられるかも含めてこの PR では決めていない。

変異試験

.claude/skills/mutation-testing/ の手順で --max-workers 2 --testFilter tools.test に絞って実施。

  • 変異1(赤くなるべき): memory_append の呼び出しの道具名第1引数を 'memory_write' に取り違える → 結果は次コメントで追記
  • 変異2(赤くなるべき): journal_writeact-not-performedact-completed に取り違える → 結果は次コメントで追記
  • 変異3(緑のままであるべき): ふるまいを変えない書き換え(引数名リネーム等) → 結果は次コメントで追記

(時間の窓の都合で draft のまま push を先行させている。変異試験・門9本・pnpm test 2回の結果は続報として本 PR へ追記する。)

引き継ぎ

  • 候補A(SDK の tool をラッパで包み、道具名を構造で流す。ラッパ1+呼び出し16=17箇所)は依頼者の台帳へ載っている。⚠️ A は outcome には効かないので、この歯は A の後も要る。
  • CLONE_TOOL_NAMEStool( の登録名と別に手で書かれていて突き合わせが見当たらない件も台帳へ載っている(この PR では触らない)。
  • 「N回目だけ落ちる」偽ストアを testing.ts へ昇格させるかは未決(上述)。

🔴 引き継ぎ(器の入れ替えで作業が中断した。次の人はここから)

中断時刻: 2026-09-09T21:0xZ。 器の入れ替え(実測 21:15〜22:10 UTC)に当たったため、未完成のまま draft で残している。 以下は「どこまで済んで、何が残っているか」の記録である。⚠ 報告は器と一緒に消えるので、ここに書く。

設計(依頼者が確定させたもの。次の人が変えなくてよい)

16箇所すべてで、応答本文に 道具名outcome の先頭行 の両方が出ることを、ふるまいで測る。

構文(ソースを読んで対応づける)ではなくふるまいで測る。理由は #752 の実測 —— 「文字列が現物に在るか」だけを見る歯は、無害な書き換えで誤って赤くなり、かつ本当に壊れたとき見逃す(両方が同時に起きる)。

済んでいること

  • 16箇所すべての歯を書いたpackages/core/src/tools.test.ts 末尾の describe('journal.append 失敗時の応答本文: 16箇所すべてで道具名と先頭行 outcome を測る')
  • SELF_JOURNALING_CLONE_TOOLS からの導出ができた(上の節)。手で並べていないので、18本目の自前記録の道具が名簿へ足されたら、この歯が「ケースが足りない」と言って赤くなる
  • 手元で pnpm exec vitest run packages/core/src/tools.test.ts は 543 passed(2回確認)

⭐ 途中で見つけて直した、この歯自身の偽陽性(次の人が戻さないために)

最初のコミット(4bb0e92)では、失敗を起こす偽ストアの reason を 'boom-<道具名>' にしていた。
その reason は応答本文の「理由: <reason>」行にそのまま出る。
⟹ ⚠⚠ だから expect(text).toContain(tool) は、実装側の道具名を意図的に取り違えても緑のままだった —— 例: memory_appendappendJournalOrThrow の第1引数を 'memory_write' に書き換えても、reason が 'boom-memory_append' なので本文に memory_append の文字列が残る。= 歯が測っているつもりのものを測っていなかった。

8fe6cc0 で reason を 'boom-case-01''boom-case-16'(道具名を含まない形) へ直した。この形を戻さないこと。
⟹ 🔑 一般形: 足場の目印が測定対象と同じ文字列を含むと、偽陽性になる。

🔴 残っていること(⛔ 通っていないものを「通った」と読まないこと)

状態
pnpm build 未実行(CI 上では 4bb0e92 で success)
pnpm check:web-bundle-node-traces 未実行(CI 上では success)
pnpm check:web-bundle-size 未実行(CI 上では success)
git diff --exit-code HEAD -- apps/daemon/openapi.json 未実行(CI 上では success)
pnpm check:sdk-quotes 未実行(CI 上では success)
pnpm typecheck 未実行(CI 上では success)
pnpm lint 未実行(CI 上では success)
pnpm format:check 未実行。⚠ 4bb0e92 の CI ではここが唯一の赤だった(下記)
pnpm test(2回照合) 未実行。⚠ CI でも skipped = この歯は CI で1度も走っていない

変異試験(赤くなるべき2本=道具名の取り違え/outcome の取り違え、+ 赤くなってはいけない1本)は未実施。
⟹ 上の偽陽性は手動の変異確認で見つけたもので、ハーネス(.claude/skills/mutation-testing/mutate.mjs)は1度も回していない。

4bb0e92 の CI の実値(ログは保持期間で消えるが、ここには残る)

ci job(run 34403501233)のステップ別の結果:

 7  Run pnpm build                          success
 8  Run pnpm check:web-bundle-node-traces   success
 9  Run pnpm check:web-bundle-size          success
10  OpenAPI spec がコードと一致しているか      success
11  Run pnpm check:sdk-quotes               success
12  Run pnpm typecheck                      success
13  Run pnpm lint                           success
14  Run pnpm format:check                   failure   ← ここだけ
15  Run pnpm test                           skipped   ← 歯は CI で走っていない

赤の実値(Expected / Received ではなく prettier の警告):

$ prettier --check .
Checking formatting...
[warn] packages/core/src/tools.test.ts
[warn] Code style issues found in the above file. Run Prettier with --write to fix.
[ELIFECYCLE] Command failed with exit code 1.

⟹ **テストの失敗ではない。整形だけである。**⟹ 直し方は pnpm format
⚠ **pnpm testskipped なので、「歯が通る」ことは CI では一度も確かめられていない。**手元の vitest run packages/core/src/tools.test.ts(543 passed)だけが根拠である。

⭐ 更新: prettier は直した(6c441d8

pnpm format を当てて push 済み。⟹ **下の「1.」は済んでいる。**⚠ **ただし残りの門と変異試験は依然として未実行である。**この head(6c441d8)の CI がどう出たかは、gh api repos/takecchi/alteroid/commits/6c441d8d367fe22eb6f82c36f1eea1e36b7d7f17/check-runs で見ること。

次の人がやること(この順)

  1. pnpm format → commit → push(4bb0e92 の赤はこれだけ)
  2. 門9本を1本ずつ、終了コードを名前つきで個別に。⛔ | tail を挟まない。⛔ >/dev/null 2>&1 を書かない。pnpm test は2回走らせて Test Files N の一致を見る
  3. 変異試験: 赤くなるべき2本(道具名を1箇所だけ取り違える / outcome を1箇所だけ取り違える)+ 赤くなってはいけない1本(ふるまい不変の書き換え)。結果をこの本文へ追記する
  4. draft のまま依頼者へ返す。⛔ **ready 化とマージは依頼者が行う。**⛔ agent:approved を自分で付けない

⛔ この PR で触らないもの

実装(tools.ts / manager.ts)/packages/core/src/testing.ts(「N回目だけ落ちる」偽ストアはテストファイル内に閉じたまま。共有の道具へ昇格させるかは未決)/名簿(SELF_JOURNALING_CLONE_TOOLS / TRACELESS_CLONE_TOOLS)/docs//穴B(PostToolUseFailure)/穴D(apps/daemon/src/app.ts の HTTP 経路)。⛔ Co-Authored-By: トレーラを付けない。

依頼者の台帳に載っている、この PR の後の仕事(⛔ ここでは触らない)

  • 候補A: SDK の tool をローカルのラッパで1枚包み、道具名を構造でハンドラへ流す(ラッパ1+呼び出し16=17箇所)。⟹ 道具名の取り違えが表現できなくなる
    ただし outcome には1バイトも効かない'act-completed' 等は手で書く値のまま)ので、**この PR の歯は A の後も要る。**⟹ 順序は「B の歯が在る状態から A へ」。
  • **CLONE_TOOL_NAMES(36本のリテラル配列)が tool( の登録名とは別に手で書かれていて、両者を突き合わせる歯が見当たらない件。**⚠ 「無い」と断定はしていない(grep 1回ぶんで、git log -S は通していない)。別に測るべきものとして台帳に在る。

rebase 後の測り直し(2026-09-10 08:0x–09:0x UTC・別のマネージャーが引き継いで実施)

base が #767 のマージで動いたので、全部を測り直した。origin/main = 2fc823ab482b1ed3bdba4921d0d13db2242f05bc#767 込み)へ rebase。head は 22f8e45e61f9701fdecf591cdc5713330d6462b0。三点差分(origin/main...HEAD)は packages/core/src/tools.test.ts の1ファイル・391行追加のみで、実装は1文字も入っていない

衝突の解き方: tools.test.ts の末尾で #767 側(ask_human の歯)と衝突した。どちらも落とさず両方を残す形で解いた。解いた後に #767 の歯2本が名前で緑であることを確認済み——act-completed(ask_human): 承認は実在し(approvals_list から読める)、跡も残る。isError のまま「やり直し禁止」が返る穴C: #pushAgentToken が journal.append の失敗で跡を残す > 認証トークンの同期が失敗し、日誌への記録も失敗すると self_dropped の帳面に跡が残る

門(個別に実行。終了コードを門の名前とともに記録)

EXIT
node packages/core/scripts/write-canon.mjs 0
pnpm install --frozen-lockfile 0
pnpm build 0
pnpm check:web-bundle-node-traces 0
pnpm check:web-bundle-size 0
pnpm check:web-css-comment-classnames 0
git diff --exit-code -- apps/daemon/openapi.json 0
pnpm check:sdk-quotes 0
pnpm typecheck 0(8ワークスペース。テストの緑とは別に測った——vitest は型を落として走る)
pnpm lint 0
pnpm format:check 0
pnpm test --maxWorkers=4(全件・1回目) 0 — Test Files 228 passed (228) / Tests 5322 passed (5322)
pnpm test --maxWorkers=4(全件・2回目) 0 — 内訳が1回目と一致

16箇所すべてが緑であること(名前で確認)

--reporter=verbose で、この describe の 16ケース+名簿の導出1件=17件 で出ることを確認した(memory_section_move一部完了(move_in)と 完了済み(move_out)の2件が別々に出る)。

変異試験(このハーネスはこの PR では初回。前身の委譲は手動確認だけで、変異ハーネスは回していなかった)

ベースラインは2段: ①無変異を2回(Test Files 2 passed (2) / Tests 792 passed (792) で一致、exit 0)/②足場のみappendJournalOrThrowformatJournalNotRecordedMessage の直前に、ふるまいを変えない印コメントを1行ずつ)→ 同じ内訳で exit 0。

id 変異 命中確認(git diff --stat 判定 落ちた歯(名前)
N1 memory_append の呼び出しの道具名だけを 'memory_write' へ取り違える(名簿に在る名前なので typecheck は 0 のまま通る——実測) tools.ts | 2 +- 'memory_append' ('⚠⚠ 完了済み・未記録・やり直し禁止'): 応答本文が道具名と先頭行 outcome を持つ(1本)
N2 memory_section_movemove_out 側の道具名だけを取り違える tools.ts | 2 +- 'memory_section_move' ('⚠⚠ 完了済み・未記録・やり直し禁止'): …(1本。move_in 側ではなく move_out 側のケースだけが落ちた=16箇所目に実際に到達している証拠
N3 commitment_closeoutcome'act-completed''act-not-performed' tools.ts | 2 +- 'commitment_close' (…): …(1本)
N4 断り書きから ${tool} を落とす(act-completed の2行目) tools.ts | 2 +- 16本(この describe の act-completed 13件+既存の歯3件)
N5 EXPECTED_TOOLS.filter(…) を外す(15本 → 17本になる) tools.test.ts | 4 +--- CASES の道具名の集合は、SELF_JOURNALING_CLONE_TOOLS から manager_send / manager_stop を除いたものと一致する(1本)
N6a 赤くなってはいけない: 局所変数名だけ(shapeentryShape tools.ts | 8 ++++---- 緑・生存 0本
N6b 赤くなってはいけない: 断り書きの語順だけ(act-not-performed の 理由:記録できなかったエントリ: を入れ替え) tools.ts | 2 +- 緑・生存 0本

N1 と N2 が、この PR の主題そのものを測っている —— 型(CloneToolName)が守るのは「名簿に在る名前か」までで、memory_append の箇所に memory_write と書いても typecheck は 0 で通る(N1 で実測)。その取り違えを止めるのは、いまはこの歯だけである。

偽陽性の経路を断ってあることを確認した: この describe の偽ストアの reasonboom-case-01boom-case-16道具名を1つも含まない)で16件。⟹ expect(text).toContain(tool) が緑になる経路は、実装が本文へ書いた道具名だけである(N1 / N2 が赤くなることがその裏づけ)。

各変異は撃つ前に git diff --stat で当たったことを確認し、撃った後に git checkout -- <file> で復元して git status --short が空であることを確認している。

CI

run 34448424721 / head_sha=22f8e45e61f9701fdecf591cdc5713330d6462b0(PR の head と40桁で一致)。ジョブ単位ci = success・image = success。⚠ 段単位でも確かめた——ci ジョブの STEP 15 Run pnpm format:check = success、STEP 16 Run pnpm test = success(skipped ではない)。前身の走行(6c441d8d…)では format:check が落ちて pnpm testskipped になっており、そのときは歯が CI で1度も走っていなかった

@takecchi
takecchi force-pushed the test/journal-notice-tool-name-and-outcome-guard branch from 6c441d8 to 22f8e45 Compare September 10, 2026 07:07
@takecchi
takecchi marked this pull request as ready for review September 10, 2026 08:09
@takecchi
takecchi merged commit ecc43c6 into main Sep 10, 2026
2 checks passed
@takecchi
takecchi deleted the test/journal-notice-tool-name-and-outcome-guard branch September 10, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant