<!-- このファイルは KrileWorks が公開している Claude Code 用スキルです。
     出典: https://krileworks.com/agent-skills
     手元の .claude/skills/ に置くと、エージェントが Apex Stem の API を
     記憶ではなくこの記述に従って書くようになります。 -->

---
name: apex-tools
description: Use this skill when implementing a TriggerHandler or making an HTTP callout testable via DI with ApexTools. Fires on TriggerHandler / extends TriggerHandler / getUpdatedRecordsWithChangedField / getUpdateRecordIdsWithChangedFields / IHttpRequestHandler / HttpRequestHandler / MockHttpRequestHandler / MockResponse / MockResponse.of / attach / sentRequests / sentRequestsAt / countByMethod / getBodyAsBlob / getHeader。トリガーハンドラ基底クラスのフック override、特定フィールド変更レコードの絞り込み、HttpCalloutMock を書かずに HTTP コールアウトを DI でモックする時に参照する。
---

# ApexTools リファレンス

ApexTools は TriggerHandler 基底クラスと HTTP リクエストラッパー (DI 対応) を提供する OSS。
このスキルは API の使い方に集中する。Trigger.cls の固定形・7 イベント宣言・命名規則などの常駐ルールは CLAUDE.md 側に記載済みなので深追いしない。

---

## TriggerHandler 基底クラス

`extends TriggerHandler` し、**必要なフックだけ override** する。全フックは `protected virtual` で、override しないフックは何もしない (空メソッドで埋める必要なし)。起動は trigger ファイルから `(new XxxHandler()).execute()` の 1 行。

### フック一覧

| フック | シグネチャ |
|---|---|
| `beforeInsert` | `(List<SObject> newRecords)` |
| `beforeUpdate` | `(Map<Id, SObject> newMap, Map<Id, SObject> oldMap)` |
| `beforeDelete` | `(Map<Id, SObject> deletedMap)` |
| `afterInsert` | `(Map<Id, SObject> newMap)` |
| `afterUpdate` | `(Map<Id, SObject> newMap, Map<Id, SObject> oldMap)` |
| `afterDelete` | `(Map<Id, SObject> deletedMap)` |
| `afterUndelete` | `(Map<Id, SObject> undeletedMap)` |
| `andFinally` | `()` — **どのコンテキストでも常に最後** に呼ばれる |

`andFinally()` は before/after の種類によらず最後に必ず走らせたい処理 (監査ログ確定、Trace のラッピング等) の置き先。多くの Handler では不要。

### 実装例

```apex
public with sharing class TriggerOppHandler extends TriggerHandler {
  protected override void afterInsert(Map<Id, SObject> newRecordsMap) {
    Set<Id> opportunityIds = newRecordsMap.keySet();
    (new CopyAccountIndustryToOpportunityUsecase(opportunityIds)).invoke();
  }

  protected override void afterUpdate(Map<Id, SObject> newMap, Map<Id, SObject> oldMap) {
    Set<Id> needIds = this.getUpdateRecordIdsWithChangedFields(new List<SObjectField>{
      Opportunity.AccountId,
      Opportunity.StageName
    });
    (new RegenerateCollectionUsecase(needIds)).invoke();
  }
}
```

---

## フィールド変更検知ヘルパー

`afterUpdate` / `beforeUpdate` で「特定フィールドが変更されたレコードだけ」に絞る。継承先 (`this.`) から呼べる。

| メソッド | 戻り値 | 用途 |
|---|---|---|
| `getUpdatedRecordsWithChangedField(SObjectField field)` | `List<SObject>` | 単一フィールド変更レコード |
| `getUpdatedRecordsWithChangedFields(List<SObjectField> fields)` | `List<SObject>` | 複数フィールドのいずれかが変更されたレコード |
| `getUpdateRecordIdsWithChangedField(SObjectField field)` | `Set<Id>` | 上記の Id 版 |
| `getUpdateRecordIdsWithChangedFields(List<SObjectField> fields)` | `Set<Id>` | 上記の Id 版 |

「特定フィールドが変わった時だけ何かする」を 1 行に集約し、Handler を「条件判定 + Usecase 呼び出し」の薄さに保つ。Id だけ必要なら `...RecordIds...` 版で `keySet` 相当を直接得る。

---

## IHttpRequestHandler / HttpRequestHandler / MockHttpRequestHandler (v1.0.0)

HTTP コールアウトを DI 抽象化する 3 つ組。本番は `HttpRequestHandler` (標準 `Http` の薄いラッパー)、テストは `MockHttpRequestHandler` を注入する。**公式 `HttpCalloutMock` を実装せずに済む**。

```apex
public interface IHttpRequestHandler {
  IHttpRequestHandler label(String label);   // 呼び出しサイトの名前付け (本番は no-op)
  void send(HttpRequest request);
  String getBody();
  Blob getBodyAsBlob();                      // バイナリ応答 (画像等)
  Integer getStatusCode();
  String getHeader(String name);             // Content-Type ガード等に
}
```

### 応答は MockResponse で宣言する

```apex
MockResponse.of('GET').respond('{"message":"not found"}', 404)          // String
MockResponse.of('post').respond(new Map<String, Object>{ ... }, 200)    // Map/List は JSON 化。メソッド名は大小どちらでも
MockResponse.of('GET').respond(imageBytes, 200).header('Content-Type', 'image/jpeg')  // Blob + ヘッダ
MockResponse.of('GET').respond(ok, 200).repeat()                        // キュー末尾に置くと以降ずっとこれ
```

**メソッド名はルーティングキーではなく「配信時の契約」**: キューの次の応答が宣言したメソッドと実リクエストが食い違うと、期待/実際/キュー状態入りのエラーで即落ちる (黙って違う応答が配られることはない。消費も記録もされない)。

### 2 モード — 結合強度で使い分ける

**🎯 親指ルール: テストの主張に順序が含まれるなら台本、含まれないなら label。迷ったら label。**

#### 台本モード (label なし) — 順序が仕様である時

コンストラクタのリスト = フローの台本。上から読めば期待するコールアウト列そのもの。順序・メソッドの逸脱は詳細エラー:

```apex
MockHttpRequestHandler mock = new MockHttpRequestHandler(new List<MockResponse>{
  MockResponse.of('GET').respond(notFound, 404),    // 1手目: 存在確認
  MockResponse.of('POST').respond(created, 201),    // 2手目: 作成
  MockResponse.of('GET').respond(found, 200)        // 3手目: 再取得
});
```

リトライは同一メソッドを並べるだけ: `[500, 500, 200]` の3連 POST。「5回リトライ」ならエラー応答を4つ並べる (Mock にカウンタ不要)。

#### label モード — サイト間の順序に縛られたくない時

呼び出しサイトごとに名前付きキュー (MockEloquent の attach/label と同じ操作感)。分岐フロー (GET してから POST か PUT) は**両分岐を attach しておき、どちらが消費されたかで通った分岐を assert**:

```apex
// Usecase 側: this.http.label(LBL_EXISTS).send(req);
MockHttpRequestHandler mock = new MockHttpRequestHandler()
  .attach(LBL_EXISTS, MockResponse.of('GET').respond(notFound, 404))
  .attach(LBL_CREATE, MockResponse.of('POST').respond(created, 201))
  .attach(LBL_UPDATE, MockResponse.of('PUT').respond(updated, 200));   // 通らない分岐も宣言してよい

new KintoneUpsertUsecase(input, mock).invoke();

Assert.areEqual(1, mock.sentRequestsAt(LBL_CREATE).size());   // create 分岐を通った
Assert.areEqual(0, mock.sentRequestsAt(LBL_UPDATE).size());   // update は未消費
```

- attach を使ったら send ごとに `label()` 必須 (1回で消費、MockEloquent と同じ)。ラベル typo は登録済み一覧付きでエラー
- attach 未使用なら `label()` は無視される = **label 付き本番コードを素の台本モックでもテストできる** (lenient。台本モードには沈黙経路が無いので罠にならない)
- 同一 label への attach はキューに追記 (= そのサイトのリトライ系列)

### 検証ヘルパー (Spy)

`countByMethod('POST')` / `requestsTo(endpointPart)` / `sentRequestsAt(label)` / `lastRequest()`。`sentRequests` も @TestVisible。

### 🛡 Content-Type ガード (実事故由来の定石)

HTTP 200 でも Content-Type が想定外なら大抵**エンドポイント間違い** (実例: `/bizCards/{id}/image` のつもりが `/bizCards/{id}` を叩き、JSON を base64 して壊れた画像を画面に流した)。バイナリ取得には必ずガードを:

```apex
this.http.label(LBL_CARD_IMAGE).send(req);
String contentType = this.http.getHeader('Content-Type');
if (this.http.getStatusCode() == 200 && (contentType == null || !contentType.startsWith('image/'))) {
  throw new CalloutException('Expected an image response but got Content-Type=' + contentType);
}
Blob image = this.http.getBodyAsBlob();
```

### Usecase 側 (null-coalescing で本番デフォルト)

```apex
public with sharing class KintoneUpsertUsecase {
  @TestVisible static final String LBL_EXISTS = 'kintoneExists';
  @TestVisible static final String LBL_CREATE = 'kintoneCreate';

  private final IHttpRequestHandler http;
  private Trace t = Trace.of('kintone へレコードを upsert');

  public KintoneUpsertUsecase(Input input) {
    this(input, null);
  }

  @TestVisible
  private KintoneUpsertUsecase(Input input, IHttpRequestHandler http) {
    this.http = http ?? new HttpRequestHandler();
  }
  // invoke() 内: this.http.label(LBL_EXISTS).send(existsReq); ...
}
```

1 本の handler を label で多重化するのが v1.0.0 推奨 (IEloquent の label と同じ考え方)。役割ごとに複数 DI する旧スタイルも引き続き可。

### ⚠️ v1.0.0 の破壊的変更 (タグ以前の main から上げる時)

- 旧コンストラクタ (`Map` / `List<Map>` / `String+Integer`) は削除 → `MockResponse.of(method).respond(body, statusCode)` に書き換え
- `IHttpRequestHandler` に `label` / `getBodyAsBlob` / `getHeader` が追加 → 独自実装クラスはメソッド追加が必要
- 枯渇エラーのメッセージが複数行の診断形式に変更 → 完全一致 assert は contains に緩める

---

## 深掘り (元ドキュメント)

- https://krileworks.com/document/ja/apex-tools-trigger-handler.md — TriggerHandler 基底クラスの 7 フック + フィールド変更検知ヘルパーの詳細
- https://krileworks.com/document/ja/apex-tools-http-request-handler.md — 公式 HttpCalloutMock の 3 つのやりづらさと IHttpRequestHandler / MockHttpRequestHandler による解決 (Before/After 対比、Usecase 統合)
