さくらコードSakura Code
小説/第4話

Novel

第4話 エラーは赤く咲く

Script

Cold Open

シーン1 朝のSlack通知

 五月の終わりの朝、オフィスに着いた桜子は、すぐに様子の違いに気づいた。

 社内チャットの開発チャンネルに、いつもは流れていない、赤い色のメッセージが立て続けに並んでいる。

[GitHub Actions] CI failed on main
test_user_age_is_valid - FAILED
build #482

 桜子はリュックを下ろしながら、画面を覗き込んだ。

 (main、って書いてある)

 昨日、桜子の二件目のプルリクエストが、ようやくmainに入ったばかりだった。タイトルは、fix: tighten age validation in user form。先輩の鈴木が、「桜子さんも次はちょっとだけロジック触ってみよう」と渡してくれた、初めての“一行以上”の修正だった。

 席に座る前に、もう一度、画面の赤い文字を見る。

 胃のあたりが、少し冷たくなった。


Part A

シーン2 赤い失敗ログ

 PCを起動して、最初に開いたのはGitHub Actionsの該当ビルドだった。

 ジョブの内訳が並ぶ。lint は緑、format-check も緑。pytest だけ、赤い × のアイコン。

 クリックして、ログを開く。入社初日の環境構築で遥に言われたとおり、エラーの一番下だけを見ない、と心に決めながら、上から順に読んでいく。

============================= test session starts =============================
collected 47 items

tests/test_user.py::test_user_age_is_valid_18 FAILED                  [  6%]
tests/test_user.py::test_user_age_is_valid_19 PASSED                  [ 10%]
...
================================= FAILURES ===================================
______________________ test_user_age_is_valid_18 ______________________________

    def test_user_age_is_valid_18():
        user = User(name="taro", age=18)
>       assert user.is_valid_for_signup() is True
E       AssertionError: assert False is True

tests/test_user.py:24: AssertionError

 (age=18 で落ちてる)

 桜子は、もう一度、自分が触ったファイルを開いた。user.py。

def is_valid_for_signup(self) -> bool:
    return self.age > 18

 その行を、桜子は昨日、こう書いた。前のコードでは >= 18 だったところを、ちゃんと「未成年は登録不可」にするつもりで、不等号をひとつだけ変えた。

 (変えた覚えは、ある)

 でも、いま見ると、それは「18歳ちょうどの人」を、登録できなくしている。

 桜子の指は、キーボードの上で、止まった。


シーン3 git blameを消したくなる瞬間

 ターミナルを開いて、桜子は手が震えそうになるのを、息を吐いて押さえながら打ち込んだ。

$ git blame src/sakura_app/user.py | head

 isvalidforsignup の行に、自分のメールアドレスが並んだ。コミットハッシュは、昨日の桜子のもの。

 (わたしの)

 桜子の頭の中に、ほんの一瞬だけ、よくない考えがいくつか浮かんだ。

 別ブランチで急いで戻して、こっそりpushすれば、誰も気づかないかもしれない。git push --force という単語を、どこかで読んだ気がする。あるいは、自分のせいじゃなく、別の変更と組み合わさった偶然のせいだ、と説明する道もあるかもしれない。

 桜子は、画面のターミナルに、何も打たないまま、深く息を吸った。

 (だめ。それは、だめだ)

 入社初日の、駅までの坂道。初めてのプルリクで受けた十四件のコメント。今朝の、赤い × のアイコン。それらが、頭の中で順番に並んでいく。

 桜色のキーホルダーが、机の端の鞄の取っ手で、わずかに揺れている。

 桜子は、ターミナルから手を離した。


Part B

シーン4 隠すほうが遅い

 席の少し離れたところで、遥が、別のレビューを開いていた。気づいた遥が、紙コップ片手に、桜子の方へ歩いてきた。

「桜子さん、CI赤いね。原因、教えて」

 声は、いつもどおり静かだった。怒っている感じも、責めている感じも、ない。

 桜子は、口を開きかけて、一度、閉じた。それから、自分のディスプレイに視線を戻して、小さく言った。

「……わたしの、変更です。age > 18 にしてしまって。本当は >= 18 のままが、要件として合ってました」

 言葉にした瞬間、自分の声が震えていることに気づいた。

 遥は、椅子を引いてきて、桜子の隣に座った。

「言いにくかったね」

「はい」

「ほんの少しだけ、消したくなったでしょ」

 桜子は、少しの間、黙ってから、うなずいた。

「ありますよね、あれ。force push とか、書き換えとか。考えるのはいい。考えるところまでで、止めたなら、十分」

「……すみません」

「謝らない。直そう」

 遥はモニターの方に身体を寄せて、画面の赤い箇所を指した。

「ひとつだけ言うね。隠すほうが、遅い」

「遅い、ですか」

「うん。隠すと、原因の特定に、隠した分だけ時間がかかる。直しても、再発する。直したように見えても、別の場所で同じ理屈の不具合が出る」

「……はい」

「だから、ここからは、隠さない。原因を見て、影響範囲を見て、テンプレに沿って報告書を書く。修正PRを出す。レビューを受ける。マージする。これだけ」

 遥の指は、画面の assert False is True の上で、一度だけ止まった。

「赤いやつは、ちゃんと、桜子さんに向かって咲いてる」


シーン5 境界条件という花

 遥と一緒に、テストファイルを開く。

def test_user_age_is_valid_18():
    user = User(name="taro", age=18)
    assert user.is_valid_for_signup() is True

def test_user_age_is_valid_17():
    user = User(name="taro", age=17)
    assert user.is_valid_for_signup() is False

def test_user_age_is_valid_19():
    user = User(name="taro", age=19)
    assert user.is_valid_for_signup() is True

「桜子さん、テストの並び、見て。なに気づく?」

「えっと、十七、十八、十九、と、年齢が連続してます」

「うん。なぜ十八が単独であるのか」

「……境目だから、ですか?」

「正解。十八は、登録できる側に入る、っていう仕様。日本の成人年齢が引き下げられたからね、二〇二二年は」

 遥は、桜子のメモ帳の方を、ちらっと見た。

「こういうのを、境界条件って呼ぶ。仕様の角。十八と十七、十八と十九、ゼロと一、九十九と百。何かが切り替わるところ。バグはだいたい、ここに咲く」

「咲く、ですね」

「うん、咲く。赤く」

 桜子は、自分の修正案を、声に出さずに頭の中で並べた。

def is_valid_for_signup(self) -> bool:
    return self.age >= 18

「直すのは、これでいいですか」

「うん。それで pytest 走らせて、全部緑になるか、見よう」

 桜子はターミナルに戻り、pytest を打った。

======================= 47 passed in 0.91s =======================

 全部、緑だった。

「……通りました」

「うん。じゃ、次は、報告書」


Part C

シーン6 報告テンプレート

 ちょうどそのとき、鈴木が、コーヒーカップ片手に通りかかった。

「桜子さん、CI、戻った?」

「あ、はい、いま、ローカルでは緑になりました」

「お、よかった。じゃ、ついでに、障害報告、書いてみよう」

「障害、報告……」

 桜子は、思わず復唱した。鈴木は、うん、と軽くうなずいて、社内wikiのページを開いて、桜子の画面に共有した。

## 障害報告テンプレート

- タイトル
- 発生日時 / 検知日時
- 影響範囲
- 事象
- 一次原因
- 暫定対応
- 恒久対応
- 再発防止
- 学び

「めちゃくちゃ大きい障害も、めちゃくちゃ小さい障害も、書く形式は同じね。今回は、本番には出てない、CI止まり。だから、影響範囲は『開発環境のみ』って書いていい」

「はい」

「今日のは、まあ、新人にとっては、いい教材になる規模。書いてみて。書いたら、開発チャンネルに貼って、終わり」

 鈴木はコーヒーを一口飲んで、自分の席へ戻っていった。やわらかな声色のまま。それが、かえって、桜子の背中をまっすぐにさせた。

 桜子は、テンプレートを自分のローカルにコピーして、空欄を、ひとつずつ埋めていった。

## 障害報告: User.is_valid_for_signup の境界条件不具合

- 発生日時: 2022/05/24 mainマージ時
- 検知日時: 2022/05/25 09:05 (CI失敗による)
- 影響範囲: 開発環境のみ。本番未反映。ユーザー影響なし
- 事象: pytest の test_user_age_is_valid_18 が FAILED
- 一次原因: 仕様(18歳は登録可)に対し、`self.age > 18` と書いてしまった。
  正しくは `self.age >= 18`
- 暫定対応: 該当行を `>= 18` に戻すPRを作成 (#48)
- 恒久対応: 同PRに、年齢の境界値テスト (17/18/19) のコメントを追記。
  test_user.py には既にあるが、user.py 側にも仕様の意図をコメントする
- 再発防止:
  - 不等号を変更するときは、境界値ケースのテストを書いて(または見て)から触る
  - 仕様(成人年齢)の根拠は user.py の docstring に記載する
- 学び:
  - 境界条件の角でバグは咲く
  - エラーは隠すより共有したほうが早い

 書き終えて、桜子は、画面を一度、上から下までゆっくり読み直した。

 書いてある事実は、自分が起こしたミスのことなのに、文章として並んでみると、不思議と、自分のことを少し外から見られている感じがした。

 遥の方を見る。遥は、桜子の画面を覗いて、ひとつだけうなずいた。

「学びの最後、いいね。あとで、桜子さんの席のディスプレイに貼っておきたい」

「……それは、ちょっと、恥ずかしいです」

「冗談。チャンネルに、貼ろう」

 桜子は、開発チャンネルに、テンプレートをそのまま投稿した。スレッドに、鈴木からは絵文字、遥からは「ありがとう、読みました」のひとことが返ってきた。

 翔太は、しばらくしてから、こう書いた。

@nakamura-shota: 17/18/19 のテスト並び、いいですね。
僕の最近のPRも、不等号触るときは、これで書こうと思います

 桜子は、画面を見ながら、目元が少しだけ熱くなった。


Ending

シーン7 夜の窓と、赤から緑へ

 二十時前。

 桜子の修正PR #48 は、遥の Approved を経て、mainにマージされた。GitHub Actions のページで、ジョブの一覧が、上から下まで、緑色で並んだ。

 桜子は、その画面を、しばらく眺めていた。

 今朝、同じ場所が赤かったこと。assert False is True の文字列。git blame に並んだ自分のメールアドレス。隠したくなった一秒。遥の「隠すほうが遅い」。鈴木のテンプレート。翔太のスレッドの返信。

 全部、まとめて、緑色のチェックマークの裏側に、入っていく。

 桜子は、ふと、tests/testuser.py のページに戻った。

 今朝、自分のミスを、最初に「ちがう」と言ってくれたのは、人ではなく、testuserageisvalid18 という、九行ほどの、誰かが書いた、地味な関数だった。書いた人の名前は、git blame を辿らないと、桜子には、まだ、分からない。

 (あの赤、なかったら、本番に、出てた)

 その想像は、git push --force を一瞬だけ考えた朝の自分よりも、ずっと、こわかった。

 (テストは、わたしを、責めるためにあるんじゃなくて、たぶん、わたしを、守るためにある)

 書きおろしの実感ではなく、書かれていたものに、守られた、という実感だった。

 窓の外では、すっかり葉桜になった街路樹が、少し強い初夏の風に揺れている。

 桜子は、机の引き出しから、メモ帳を取り出した。

2022/05/25 新人日記

- エラーは赤く咲く。境界の角に咲く
- 隠すほうが、遅い
- 障害報告テンプレート(影響範囲 / 暫定対応 / 恒久対応 / 再発防止 / 学び)
- 17 / 18 / 19 を並べて見る
- 赤いテストは、わたしを責めるためじゃなくて、守るためにあった

 ペンを置いて、桜子は、もう一行だけ、書き足した。

- わたしのコミットは、わたしの、もの
- 次に user.py を触るときは、自分でも、境界のテストを、先に書こう

 画面の中の、緑のチェックマークが、夜の窓に映っている自分の顔と重なった。

 明日も、たぶん、どこかで赤く咲く。

 でも、咲いたら、見にいこう。前後を、読みにいこう。隠さず、書いて、貼ろう。

 桜子は、メモ帳を閉じて、退勤の準備を始めた。