test: journal.append 失敗時に道具名と先頭行 outcome を16箇所すべてで測る - #755
Merged
Conversation
takecchi
force-pushed
the
test/journal-notice-tool-name-and-outcome-guard
branch
from
September 10, 2026 07:07
6c441d8 to
22f8e45
Compare
takecchi
marked this pull request as ready for review
September 10, 2026 08:09
takecchi
deleted the
test/journal-notice-tool-name-and-outcome-guard
branch
September 10, 2026 08:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何を足したか
packages/core/src/tools.test.tsの末尾に、appendJournalOrThrowの16箇所すべてについて、journal.appendが落ちる条件で道具を呼び、応答本文が (1) 道具名(expect(text).toContain(tool))と (2) 先頭行 = outcome(expect(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_startact-not-performed(2箇所): journal_write / daily_report_writeact-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本)から導いた:manager_send/manager_stopを除く理由: この2本はManagerPoolのガード付き#journal(clone.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に絞って実施。memory_appendの呼び出しの道具名第1引数を'memory_write'に取り違える → 結果は次コメントで追記journal_writeのact-not-performedをact-completedに取り違える → 結果は次コメントで追記(時間の窓の都合で draft のまま push を先行させている。変異試験・門9本・
pnpm test2回の結果は続報として本 PR へ追記する。)引き継ぎ
toolをラッパで包み、道具名を構造で流す。ラッパ1+呼び出し16=17箇所)は依頼者の台帳へ載っている。outcomeには効かないので、この歯は A の後も要る。CLONE_TOOL_NAMESがtool(の登録名と別に手で書かれていて突き合わせが見当たらない件も台帳へ載っている(この PR では触らない)。testing.tsへ昇格させるかは未決(上述)。🔴 引き継ぎ(器の入れ替えで作業が中断した。次の人はここから)
中断時刻: 2026-09-09T21:0xZ。 器の入れ替え(実測 21:15〜22:10 UTC)に当たったため、未完成のまま draft で残している。 以下は「どこまで済んで、何が残っているか」の記録である。⚠ 報告は器と一緒に消えるので、ここに書く。
設計(依頼者が確定させたもの。次の人が変えなくてよい)
構文(ソースを読んで対応づける)ではなくふるまいで測る。理由は #752 の実測 —— 「文字列が現物に在るか」だけを見る歯は、無害な書き換えで誤って赤くなり、かつ本当に壊れたとき見逃す(両方が同時に起きる)。
済んでいること
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_appendのappendJournalOrThrowの第1引数を'memory_write'に書き換えても、reason が'boom-memory_append'なので本文にmemory_appendの文字列が残る。= 歯が測っているつもりのものを測っていなかった。⟹
8fe6cc0で reason を'boom-case-01'〜'boom-case-16'(道具名を含まない形) へ直した。この形を戻さないこと。⟹ 🔑 一般形: 足場の目印が測定対象と同じ文字列を含むと、偽陽性になる。
🔴 残っていること(⛔ 通っていないものを「通った」と読まないこと)
pnpm build4bb0e92で success)pnpm check:web-bundle-node-tracespnpm check:web-bundle-sizegit diff --exit-code HEAD -- apps/daemon/openapi.jsonpnpm check:sdk-quotespnpm typecheckpnpm lintpnpm format:check4bb0e92の CI ではここが唯一の赤だった(下記)pnpm test(2回照合)skipped= この歯は CI で1度も走っていない変異試験(赤くなるべき2本=道具名の取り違え/outcome の取り違え、+ 赤くなってはいけない1本)は未実施。
⟹ 上の偽陽性は手動の変異確認で見つけたもので、ハーネス(
.claude/skills/mutation-testing/mutate.mjs)は1度も回していない。4bb0e92の CI の実値(ログは保持期間で消えるが、ここには残る)cijob(run 34403501233)のステップ別の結果:赤の実値(
Expected/Receivedではなく prettier の警告):⟹ **テストの失敗ではない。整形だけである。**⟹ 直し方は
pnpm format。⚠ **
pnpm testがskippedなので、「歯が通る」ことは 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で見ること。次の人がやること(この順)
pnpm format→ commit → push(4bb0e92の赤はこれだけ)| tailを挟まない。⛔>/dev/null 2>&1を書かない。pnpm testは2回走らせてTest Files Nの一致を見る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 の後の仕事(⛔ ここでは触らない)
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 の帳面に跡が残る。門(個別に実行。終了コードを門の名前とともに記録)
node packages/core/scripts/write-canon.mjspnpm install --frozen-lockfilepnpm buildpnpm check:web-bundle-node-tracespnpm check:web-bundle-sizepnpm check:web-css-comment-classnamesgit diff --exit-code -- apps/daemon/openapi.jsonpnpm check:sdk-quotespnpm typecheckpnpm lintpnpm format:checkpnpm test --maxWorkers=4(全件・1回目)Test Files 228 passed (228)/Tests 5322 passed (5322)pnpm test --maxWorkers=4(全件・2回目)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)/②足場のみ(appendJournalOrThrowとformatJournalNotRecordedMessageの直前に、ふるまいを変えない印コメントを1行ずつ)→ 同じ内訳で exit 0。git diff --stat)memory_appendの呼び出しの道具名だけを'memory_write'へ取り違える(名簿に在る名前なのでtypecheckは 0 のまま通る——実測)tools.ts | 2 +-'memory_append' ('⚠⚠ 完了済み・未記録・やり直し禁止'): 応答本文が道具名と先頭行 outcome を持つ(1本)memory_section_moveの move_out 側の道具名だけを取り違えるtools.ts | 2 +-'memory_section_move' ('⚠⚠ 完了済み・未記録・やり直し禁止'): …(1本。move_in 側ではなく move_out 側のケースだけが落ちた=16箇所目に実際に到達している証拠)commitment_closeのoutcomeを'act-completed'→'act-not-performed'へtools.ts | 2 +-'commitment_close' (…): …(1本)${tool}を落とす(act-completed の2行目)tools.ts | 2 +-EXPECTED_TOOLSの.filter(…)を外す(15本 → 17本になる)tools.test.ts | 4 +---CASES の道具名の集合は、SELF_JOURNALING_CLONE_TOOLS から manager_send / manager_stop を除いたものと一致する(1本)shape→entryShape)tools.ts | 8 ++++----理由:と記録できなかったエントリ:を入れ替え)tools.ts | 2 +-N1 と N2 が、この PR の主題そのものを測っている —— 型(
CloneToolName)が守るのは「名簿に在る名前か」までで、memory_appendの箇所にmemory_writeと書いてもtypecheckは 0 で通る(N1 で実測)。その取り違えを止めるのは、いまはこの歯だけである。偽陽性の経路を断ってあることを確認した: この describe の偽ストアの
reasonはboom-case-01〜boom-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 testがskippedになっており、そのときは歯が CI で1度も走っていなかった。