メインコンテンツに移動

テスト

テスト

AppKit は @databricks/appkit/testing にテストキットを同梱しています。これにより、稼働中の Databricks ワークスペースや資格情報、ネットワークアクセスがなくても plugin をテストでき、plugin をまたいだツール呼び出しやストリーミング応答も検証できます。plugin のテストは高速で、ワークスペースが利用できない CI 環境でも実行できます。

Goal

外側の境界だけをフェイクに置き換え、実際の PluginContext に対して plugin の実コードパスを動かします。これにより、ルート登録、plugin 間のツールディスパッチ、ユーザースコープ (on-behalf-of) 実行、呼び出しごとのタイムアウトまでカバーできます。コンテキストは一切再実装しないため、テストが本番の挙動から乖離することはありません。

このキットには3つのエントリーポイントと、一連のフィクスチャヘルパーが用意されています。

  • createTestApp({ plugins }) — 実際のアプリを起動し、実際の HTTP 経由で呼び出します。まずはここから始めてください。
  • createTestPluginContext() — 境界をフェイクにした実際の PluginContext を構築し、plugin に接続します。起動もソケットも不要です。
  • expectStream(...).toEmit(...) — ストリームが発行するイベント種別とその順序を検証します。
  • フィクスチャcreateMockRequestcreateMockResponsecreateMockWorkspaceClientmockServiceContext、および SQL レスポンスビルダー。

このキットはモックに Vitestvi を使用するため、vitestオプションのピア依存関係です。Vitest でテストを書いているのであればすでに導入済みのはずで、キットは2つ目を同梱せず、お使いのコピーを解決して利用します。オプション扱いなので、@databricks/appkit/testing をインポートしないアプリにはインストールされず、本番環境のインストールにテストフレームワークが混入することはありません。Vitest v3 と v4 のいずれでも動作します。

pluginのテスト

createTestApp({ plugins }) は、実際の Express の組み込み、ルート、リソース検証をそのまま備えた本物の AppKit アプリを起動し、クライアントと同じように呼び出せるメソッドを提供します。

import { createTestApp, expectStream } from "@databricks/appkit/testing";

test("my plugin answers a request", async () => {
  const app = await createTestApp({ plugins: [myPlugin()] });
  try {
    const res = await app.post("/api/my-plugin/thing", { body: { q: 1 }, obo: true });
    expect(res.status).toBe(200);
    await expectStream(res).toEmit("status", "result");
  } finally {
    await app.close();
  }
});

ワークスペースも認証情報もネットワークも不要です。ハーネスは NODE_ENV を開発用以外の値に固定し、一時ポートにバインドし、モックのワークスペースクライアントを組み込み、キャッシュをメモリ上に保持するため、外部への通信は一切発生しません。

パスはマウント先を含む完全なルートです。plugin のプレフィックスは /api/ とマニフェスト名のケバブケース表記を組み合わせたものになるため、mySearch という名前の plugin は /api/my-search/… で提供されます。

どちらのハーネスを使うか

createTestAppcreateTestPluginContext
アプリを起動するはいいいえ
ソケットをバインドするはい (一時ポート)いいえ
Express ミドルウェア、エラーハンドラー本物対象外
リソース/環境変数の検証本物、かつ厳密対象外
ワークスペースクライアントモック化して注入mockServiceContext で自分でモック化
close() が必要かはいいいえ
速度高速だがソケット分のコストがかかる最速

plugin の HTTP 挙動をエンドツーエンドで確認するには createTestApp を使います。ルート登録、ツールのディスパッチ、タイムアウトの合成といった配線部分を単体テストするには createTestPluginContext を使います。ハーネスを使うテストスイートは、既存の慣例に合わせて *.integration.test.ts と命名してください。

pluginが読み取るデータをフェイクする

AppKit のワークスペースクライアントファサード上で、ドット区切りのパス "<service>.<method>" を使ってレスポンスを宣言します:

const app = await createTestApp({
  plugins: [myPlugin()],
  responses: {
    "jobs.getRun": { state: "TERMINATED", result_state: "SUCCESS" },
    "statementExecution.executeStatement": { status: { state: "SUCCEEDED" } },
    "apiClient.request": { results: [] },
  },
});

関数値は呼び出し時の引数を受け取るため、引数ごとに挙動を切り替えたり、reject させてエラー経路をテストしたりできます。responses は組み込みのモックを設定するためのものなので、独自の client と一緒に渡すと、黙って無視されるのではなくエラーになります。その場合は、そのクライアント側でレスポンスを設定してください。宣言しなかったパスはクラッシュせず undefined を返します。そのトレードオフについては Databricks services のモック を参照してください。

レスポンスの形状は、Databricks SDK の service 型に従ってください。キット側では検証しないため、形状が間違っている場合はフェイクではなく plugin 側で失敗します。

アプリを 1 つ開いている状態では、app.client は handler が runtime に解決するオブジェクトそのものです (plugin 内からは getExecutionContext().client で取得できます) 。そのため、これに対して呼び出しをアサートできます:

import { getMock } from "@databricks/appkit/testing";

expect(getMock(app.client, "jobs.getRun")).toHaveBeenCalledWith({ run_id: 42 });

getMock が用意されているのは、ファサードのアクセサが SDK の型で定義されており、expect(app.client.jobs.getRun).toHaveBeenCalled() では型チェックが通らないためです。

リクエスト

app.get/post/put/patch/delete(path, options?) はネイティブの Response を返すため、expectStream をブリッジなしでそのまま組み合わせられます。

  • body — 文字列以外の値は content-type: application/json を付けて JSON エンコードされます。文字列はそのまま送信されます。
  • headers — 最後にマージされるため、ハーネスが設定した値よりも優先されます。
  • obo — 既定のテストユーザーを使う場合は true、または { userId, token, email } を指定します。createMockRequest({ obo }) と同じ省略記法なので、asUser(req) を使う handler はその ID を解決します。
  • signalfetch に転送されます。

Teardown

ハーネスはソケットをバインドするため、起動するたびに close() が必要ですclose() はソケットを解放し、pluginの shutdown() フックを実行し、AppKit のシングルトンを破棄したうえで、process.env を起動前の状態に戻します。この処理は冪等です。

テストが例外をスローした場合でもスコープを抜ける際にアプリをクローズできるため、await using の利用を推奨します:

await using app = await createTestApp({ plugins: [myPlugin()] });
// スコープを抜けるときに解放される

try/finally も使えます。アプリをブロックの外まで存続させる必要がある場合は、この方法を使います。

const app = await createTestApp({ plugins: [myPlugin()] });
try {
  // ...
} finally {
  await app.close();
}

クローズし忘れるとアプリが起動したままになり、ソケットは占有され、シングルトンや process.env も復元されないため、次の createTestApp は拒否されます (アプリは同時に1つまでです) 。

宣言されたリソースを満たす

ハーネスは実際のバリデータを厳格なモードで実行するため、マニフェストでリソースを必須としている plugin は、対応する環境変数が設定されていないと起動に失敗します。次のように env で指定してください:

// 例外: MY_WAREHOUSE_ID はマニフェストで必須です。
await createTestApp({ plugins: [myPlugin()] });

// 起動します。
await createTestApp({ plugins: [myPlugin()], env: { MY_WAREHOUSE_ID: "w-1" } });

これにより、「自分の plugin はリソースを正しく宣言している」という主張が、実際に検証可能なものになります。envclose() 時に復元されます。

チェックされないこと

ハーネスが検証するのは、必須リソースの環境変数が存在することです。マニフェストの config.schema に対して設定を検証することはありません — そのための runtime バリデーターはまだ存在しません。テストが正常に起動したという事実が示すのは、リソース宣言と環境変数が正しく配線されていることだけで、設定値の形式が正しいかどうかについては何も示しません。

その他のオプション

  • server: false — ソケットを開きません。plugin の setup、validation、teardown は引き続き実行されますが、リクエストメソッドを呼び出すと例外がスローされます。plugin が起動することだけを確認したい場合に便利です。
  • client — 組み込みのフェイクではなく、独自のワークスペースクライアントを指定します。この場合、currentUser.me() の実装は呼び出し側の責任になります。AppKit は起動時に currentUser.id を読み取るため、これがないと起動できません。
  • nodeEnv — 既定値は "test" です。"development"拒否されます。開発モードではハーネスの一時ポートが get-port を経由するためポート 0 で例外がスローされ、さらに実際の Vite サーバーが起動して validation も緩和されてしまいます。
  • cache — 既定ではインメモリです。これを上書きすると、キャッシュがネットワークにアクセスできる状態になってしまうため、それ自体がテストの目的でない限り変更しないでください。

createTestPluginContext()

PluginContext は、AppKit がすべての plugin に渡す仲介役です。ルートをバッファリングし、ツールプロバイダーを追跡し、ユーザースコープとタイムアウトを適用しながら plugin 間のツール呼び出しを実行します。createTestPluginContext() は、3 つの境界だけをフェイクに差し替えた実物のコンテキストを返します。

境界差し替え方法
テレメトリ何もしないモックプロバイダー — OpenTelemetry pipeline は不要です。
ツールプロバイダー実際の registerToolProvider 経由で登録されたフェイク。plugin 名、次にツール名をキーとします。
ルート実際の addRoute/addMiddleware をラップし、plugin が登録した内容を記録します。

コンテキストが実物であるため、executeTool は引き続き asUser(req) でユーザースコープを解決し、指定したタイムアウトから中断シグナルを組み立てます。つまり、これらの経路も確実にテスト対象となります。

フェイクのツールレスポンスを登録する

plugin 名、続いてツール名をキーとする定型レスポンスを渡します。レスポンスには、静的な値か、呼び出し引数と合成された中断シグナルを受け取る関数のいずれかを指定できます。

import { createTestPluginContext } from "@databricks/appkit/testing";

const mock = createTestPluginContext({
  analytics: {
    // 静的なレスポンス
    top_users: [{ user: "alice", events: 42 }],
    // 関数形式のレスポンス — 引数を検証したり、処理の遅延や中断をシミュレートしたりできる
    query: (args, signal) => runFakeQuery(args, signal),
  },
});

plugin へのアタッチ

attach() は、本番環境と同じ流れでコンテキストを plugin に接続します。まず (AppKit がまだ初期化していない場合は) インメモリキャッシュをシードし、続いて plugin の attachContext を呼び出します。これによりテレメトリが再構築され、isReadytrue に切り替わります。this.contextthis.cache を読み取る handler、あるいは isReady で制御される handler を実行する前に、必ず await してください。

const plugin = new MyAgentPlugin({});
await mock.attach(plugin);

plugin のクラスを直接インスタンス化します (new MyAgentPlugin(...)) 。createApp に渡す analytics() / agents() ファクトリが返すのは、アプリが構築するためのディスクリプタです。ユニットテストで必要なのはインスタンスの方です。

ワークスペースクライアントと on-behalf-of のスタブも、アプリごとではなくプロセス全体で共有されます。ServiceContext が保持するクライアントは 1 つで、createUserContext のフェイクも単一のスパイです。そのため、createTestApp で同時に開けるアプリは 1 つだけで、最初のアプリを閉じる前に 2 つ目を起動すると例外を投げます。2 つ開いた状態では、2 つ目の clientresponses が handler に届かず、どちらかを閉じるともう一方からも共有の OBO フェイクが失われてしまいます。Vitest はテストファイルを別々のワーカーで分離するため、この制約が効くのは 1 つのファイル内のアプリに限られます。知っておくと役立つ点として、beforeAll でアプリを開いたままにしている describe の中に、独自のアプリを起動するテストを含めることはできません。

attach() が初期値を投入するキャッシュはプロセス全体のシングルトンです。CacheManager はテストプロセスごとに 1 度だけ初期化され、以降は再利用されます。Vitest はテストファイルを別々のワーカーで分離するため、キャッシュがファイルをまたいで漏れることはありませんが、同一ファイル内のテスト同士では共有されます。あるテストがキャッシュを埋め、同じファイル内の後続テストがそれを参照してはならない場合は、resetTestCache() でテストごとにクリアしてください。

import { resetTestCache } from "@databricks/appkit/testing";

beforeEach(async () => {
  await resetTestCache(); // キャッシュが未初期化の場合は何もしない
});

これは単一のテストでも役立ちます。キャッシュをクリアして意図的にミスを発生させ、続く呼び出しがヒットになることを検証できます。

何が起きたかを確認する

返されるオブジェクトは、テスト対象のアクションを実行した後に読み取れるライブビューを提供します。

await someHandler(req, res);

// plugin をまたぐツールのディスパッチをすべて順番に記録。
expect(mock.toolCalls[0]).toMatchObject({
  plugin: "analytics",
  tool: "query",
  asUser: true, // on-behalf-of の経路が実行されたことの証明
});

// plugin が登録したすべてのルート(ラップ前の生の handler)。
expect(mock.routes).toContainEqual(
  expect.objectContaining({ method: "post", path: "/invocations" }),
);

// 注入されたテレメトリプロバイダーはコンテキスト自身のスパン、つまり
// PluginContext.executeTool が plugin をまたぐ各ツール呼び出しの前後に開くスパンを記録する。
expect(mock.telemetry.getTracer().startActiveSpan).toHaveBeenCalled();

mock.telemetryPluginContext に注入されるため、コンテキストが開始したスパン (特に executeTool) を記録します。これは plugin 自身のテレメトリではありませんattachContext は実際の TelemetryManager から this.telemetry を再構築するため、plugin が内部で開始したスパンは mock.telemetry には現れません。

plugin をまたぐ呼び出しの検証でアサートすべきフィールドは RecordedToolCall.asUser です。フェイクの asUser は実際の Plugin.asUser と同じトークンの事前条件を強制するため、asUser: true (かつ userId が設定された状態) が記録されたディスパッチは、呼び出し元のユーザースコープを確かに解決できています。一方、x-forwarded-access-token を欠いたリクエストは拒否されます。これは、黙って通してしまう { executeTool } のスタブでは検証できない OBO の違いです。両方向をアサートしてください。適切な形式のリクエストでは期待どおりの userId が記録され、トークンのないリクエストでは例外がスローされます。

フェイクが再現するのは asUserトークンの事前条件であり、内部の開発モード用テレメトリマーカーではありません。NODE_ENV=development では実際の Plugin.asUser は代理実行をスキップして OTel の isDevOboFallback() フラグを設定しますが、フェイクはこれを再現しません。OBO の検証には isDevOboFallback() ではなく、記録された asUser / userId フィールドを使ってアサートしてください。

expectStream(...)

AppKit の plugin は Server-Sent Events をストリーミングします。expectStream はストリームを読み取り、発行されるイベント種別とその順序を検証します。引数には、非同期イテラブル (エージェントアダプターの run()) 、イベントの配列、SSE の Response (またはその Promise。ボディは内部で解析されます) 、あるいは createMockResponse() (キャプチャした書き込みを再生) を渡せます。

import { expectStream } from "@databricks/appkit/testing";

// 順序を保った部分列の一致 — 間に挟まるイベント(ハートビートやデルタ)は無視されます。
await expectStream(agent.adapter.run(input)).toEmit("tool_call", "message_delta");

// 完全一致 — ストリーム全体の構成を順序どおりに、余分なイベントを一切含まずに検証します。
await expectStream(events).toEmitExactly("warehouse_status", "result");

// アサーションせずに収集することもできます。
const types = await expectStream(res).collectTypes();

pluginのストリーミングルートをアサートする

ほとんどのpluginは、単なるジェネレーターではなくルートhandler (res.write(...)) からSSEをストリーミングします。createMockResponse() がその書き込みをキャプチャし、expectStream がそれをそのまま読み取ります。実際のhandlerを実行したうえで、次のようにアサートします:

import { createMockRequest, createMockResponse, expectStream } from "@databricks/appkit/testing";

const res = createMockResponse();
await plugin._handleStream(createMockRequest({ obo: true }), res);

// モックは handler が書き込んだ SSE を保持しており、expectStream がそれを解析する。
await expectStream(res).toEmit("status", "result");

expectStream(res)expectStream(res.sseResponse()) は同等です。後者は、必要であれば生の Response をそのまま受け取れます。SSE のボディを文字列として渡さないでください。文字列は文字のイテラブルであるため、expectStream は 1 文字ごとに「イベント」を発行するのではなく、sseResponse() を案内するエラーを出して拒否します。

toEmit は期待する型が順序どおりに現れることを検証しますが、その前後や間に別のイベントが挟まっていても許容します。ハートビートやメタデータといった管理用イベントが混在するストリームでは、この挙動が望ましいはずです。ストリームの形が完全に確定している場合は toEmitExactly を使用してください。

expectStream はアサーションの前にソース全体をバッファリングするため、終了しないストリームの場合、そのままではテストランナー自身のタイムアウトまで処理が止まってしまいます。{ timeout } を渡せば、明確なエラーとともに早期に失敗させられます:

await expectStream(handler.stream(req), { timeout: 1000 }).toEmit("result");

Fixtures

AppKit には 2 つのコンテキストがあり、それぞれ別のツールで疑似化します。PluginContext は plugin 間の仲介役として、ルート、ツールのディスパッチ、ユーザースコープを扱います。createTestPluginContext() は、この本物を境界部分だけ疑似化した状態で提供します。ServiceContextデータプレーンであり、plugin が getWorkspaceClient() 経由で到達するワークスペースクライアント、サービスプリンシパル、warehouse ID を解決します。

このキットは両方をカバーします。createTestApp は実際の接続点にモックのワークスペースクライアントを注入して、データプレーンを疑似化します。その下位では mockServiceContext がシングルトンを直接スパイし、createMockWorkspaceClient はどちらの経路でもインストールされるクライアントを構築します。

このキットは、AppKit が内部で使用するリクエスト/レスポンス/コンテキストのフィクスチャを再エクスポートします:

  • createMockRequest(overrides?) / createMockResponse() — Express のリクエスト/レスポンスのテストダブル。ストリーミング用フラグ (headersSentwritableEnded) も含みます。obo: true (または obo: { userId, token, email }) を渡せば、asUser が必要とする転送済み ID ヘッダーを手動で追加せずに設定できます。createMockResponse() は handler が書き込んだ内容をすべてキャプチャします。これを expectStream に渡す (または sseResponse() を呼ぶ) ことで、ストリーミングルートの SSE を検証できます。(plugin はワークスペースクライアントをリクエストではなく getWorkspaceClient() 経由で解決します。制御するには mockServiceContext を使用してください。)

  • mockServiceContext(options?)ServiceContext シングルトンをスパイし、サービスプリンシパル やユーザーコンテキストを解決するコードがテストダブルを受け取るようにします。beforeEach で呼び出し、返された restore()afterEach で呼び出してください。

  • useServiceContextMock(options?) — 同じ処理を 1 行で行います。beforeEach でのインストールと afterEach での復元を代わりに登録します。describe ブロックの先頭で呼び出し (テスト内では呼ばないでください)、テスト内から有効な .current ハンドルを読み取ります:

    describe("my plugin", () => {
      const ctx = useServiceContextMock();
      test("...", async () => {
        await handler(createMockRequest({ obo: true }), res);
        expect(ctx.current.createUserContextSpy).toHaveBeenCalled();
      });
    });
  • createSuccessfulSQLResponse(rows, columns) / createFailedSQLResponse(message) — SQL Warehouse のステートメントレスポンスを構築します。

  • setupDatabricksEnv(overrides?)DATABRICKS_HOST / DATABRICKS_WAREHOUSE_ID をテスト用の値に設定します。

  • resetTestCache() — テスト間 (またはテスト内) で共有キャッシュのシングルトンをクリアします。キャッシュがまだ初期化されていない場合は何もしません。 このキットは 2 つの語を意図的に使い分けています。モックは呼び出しを記録して検証できるようにするもの (createMockWorkspaceClientmockServiceContext)、フェイクは代役として単に動作するもの (FakeProviderFakeToolResponse) です。

  • createTestPlugin(factory, config?) — AppKit と同じ設定マージを適用して、ファクトリから plugin をインスタンス化します。完全な例を参照してください。

  • getListeningPort(server) — サーバーのバインド完了を待ち、割り当てられたポートを返します。createTestApp はこれを自動で行うので、port: 0 で自分でサーバーを起動する場合に利用してください。

Databricks servicesのモック化

コアpluginの実処理は、いずれも getWorkspaceClient() を経由します。createMockWorkspaceClient() はこのインターフェース全体をまとめてモック化するため、jobsgenieservingEndpointsfiles を利用するpluginでも、入れ子になったクライアントを手作業で組み立てることなくテストできます。

import { createMockWorkspaceClient, getMock } from "@databricks/appkit/testing";

const client = createMockWorkspaceClient({
  responses: { "jobs.getRun": { state: "TERMINATED" } },
  config: { host: "https://my-test-host.example.com" },
});

await client.jobs.getRun({ run_id: 1 });        // → { state: "TERMINATED" }
await client.genie.getMessage({ id: "m-1" });   // → undefined(例外はスローされない)

createTestApp はこれらのいずれかを自動でインストールするため、直接使うのは createTestPluginContextmockServiceContext 経由で plugin を動かす場合だけにしてください。

仕組みと期待できる動作は次のとおりです。

  • ファサードは型付きなので、client.jbos はコンパイルエラーになります。インターフェースは AppKit が管理しており、SDK を際限なく追いかけるのではなく、閉じた集合になっています。
  • service はプロキシで、メソッドごとにメモ化されたモックを生成します。client.jobs.getRun === client.jobs.getRun が成り立つため呼び出しアサーションは安定し、toLegacyWorkspaceClient() も同じ関数を共有します。つまり responses のエントリ 1 つで両方の views をカバーできます。
  • config.host は (モックではなく) 実際の文字列です。AppKit がこれを元に URL を組み立てるためです。同じ理由で apiClient.userAgent() は同期的であり、apiClient.request{} を解決するため、その結果を分割代入しても例外になりません。
  • 妥当なデフォルトが組み込まれています。SQL ステートメントは成功し、warehouse は RUNNING を報告し、currentUser.me() はサービスユーザーを返します。すべて自分で記述したい場合は defaults: false を渡してください。
宣言されていないメソッドは undefined を返します

宣言されていないメソッドは、例外を投げる代わりに undefined を解決します。これは意図的な仕様で、テストが関知しない services に触れても plugin が動き続けられるようにするためです。ただしその結果、レスポンスの宣言を忘れた呼び出しは、はっきり失敗せずに静かに undefined を返すため、誤った理由でテストが通ってしまうことがあります。

この沈黙を失敗に変えるには strict: true を渡してください。レスポンスが宣言されていないパスへの呼び出しは、undefined を解決する代わりに、そのパス名を示して例外を投げます。組み込みのデフォルトは宣言済みとして扱われるため、ハーネスの起動はそのまま動作します。

const app = await createTestApp({ plugins: [myPlugin()], strict: true });
// 宣言されていないパスを呼び出す handler では、リクエストが失敗するようになる

TypeScript は想像以上に多くのケースをカバーします。各アクセサが SDK 自身のサービスクラスに対して型付けされているため、サービス名のスペルミス (client.jbos) もメソッド名のスペルミス (client.jobs.getRunz) もコンパイルエラーになります。抜け穴となるのは、実在するメソッドでレスポンスが宣言されていないケースと、キャストで型チェックを回避した呼び出しです。

もう一つの相違点として、サービスのメソッドはアクセス時に生成されるため、呼び出しはできても列挙はできませんtypeof client.jobs.getRun"function" ですが、'getRun' in client.jobsfalseObject.keys(client.jobs)[] になります。そのため、in で機能検出を行ったり、サービスをリフレクションで走査したりする plugin コードは、本番環境とは異なる分岐をたどります。これは意図的な設計です。これらのキーを公開すると util.inspect が一つずつ探索し、探索のたびにモックが生成されてしまい、既定のトラップが回避しようとしている無限再帰を招いてしまうためです。

なお、createLakebasePool({ workspaceClient }) は、パスワードコールバックがモックに解決されるプールを構築します。プールは生成されますが、接続はできません。Lakebase のテストには、これではなく実際のデータベースか、用途に合わせて作った偽のプールが必要です。

完全な例

自分で作成した plugin は、new でクラスを直接インスタンス化します。createApp に渡す analytics() / agents() ファクトリ関数が返すのはインスタンスではなく、アプリが構築するためのディスクリプタです。

これらのファクトリからインスタンスを取得したい場合は、ディスクリプタ経由で辿るのではなく、createTestPlugin を使用してください:

import { createTestPlugin } from "@databricks/appkit/testing";

const plugin = createTestPlugin(genie, { spaceId: "s-1" });

// これは NG — DEFAULT_CONFIG が適用されず `name` も抜けるため、本番で生成される
// インスタンスとは設定が異なってしまう:
//   const plugin = new (genie({}).plugin)({ spaceId: "s-1" });

createTestPlugin は、AppKit が登録時に行うのと同じマージを適用します。順序は DEFAULT_CONFIG、次にユーザー指定の設定、最後にマニフェストの name です。これはこのユニットテスト用の経路に限った話で、createTestApp はディスクリプタを受け取り、インスタンスを自身で構築します。

import { Plugin, type PluginManifest } from "@databricks/appkit";
import { expectStream, createMockRequest, createTestPluginContext } from "@databricks/appkit/testing";
import { describe, expect, test } from "vitest";

// ルートを登録し、2つのイベントをストリーミングする小さなplugin。
class GreeterPlugin extends Plugin {
  static manifest = {
    name: "greeter",
    displayName: "Greeter",
    description: "Example plugin",
    resources: { required: [], optional: [] },
  } as PluginManifest<"greeter">;

  async setup() {
    this.context?.addRoute("get", "/hello", (_req, res) => res.end());
  }

  async *greet(name: string) {
    yield { type: "greeting_start", name };
    yield { type: "greeting_end", message: `Hello, ${name}!` };
  }
}

describe("greeter plugin", () => {
  test("registers its route through the context", async () => {
    const mock = createTestPluginContext();
    const plugin = new GreeterPlugin({});

    await mock.attach(plugin);
    await plugin.setup();

    expect(mock.routes).toContainEqual(
      expect.objectContaining({ method: "get", path: "/hello" }),
    );
  });

  test("streams events in order", async () => {
    const plugin = new GreeterPlugin({});
    await expectStream(plugin.greet("world")).toEmit(
      "greeting_start",
      "greeting_end",
    );
  });
});

plugin 間のツール呼び出しをディスパッチする plugin をテストするには、フェイクのプロバイダーを登録し、mock.toolCalls を検証します。ここには、on-behalf-of の経路が実行されたことを確認できる asUser も含まれます:

const mock = createTestPluginContext({ analytics: { query: [{ n: 1 }] } });
const plugin = new MyAgentPlugin({});
await mock.attach(plugin);

// `obo` は `asUser` が必要とする転送用の ID ヘッダーを設定します。これがないと、
// ディスパッチは(正しく)"Missing user token" で拒否されます。
const req = createMockRequest({ obo: true });
await plugin.runSomethingThatCallsAnalytics(req);

expect(mock.toolCalls[0]).toMatchObject({
  plugin: "analytics",
  tool: "query",
  asUser: true,
});

関連項目

  • カスタムplugin — このキットでテストするpluginを作成します。
  • 実行コンテキストasUser とサービスプリンシパルの実行時の違いについて説明します。
  • ローカル開発 — ホットリロードを利用し、反復しながらアプリを実行します。

Databricks Developer Hub

次のエージェント型アプリを数分でリリースする準備はできていますか?

ドキュメントを読む