Accéder au contenu principal

Tests

Tests

AppKit fournit un kit de test dans @databricks/appkit/testing qui vous permet de tester un plugin, y compris ses appels d'outils entre plugins et ses réponses en streaming, sans workspace Databricks actif, sans identifiants ni accès réseau. Les tests de plugins restent rapides et s'exécutent en CI, où aucun workspace n'est disponible.

Objectif

Solliciter les véritables chemins d'exécution d'un plugin face à un vrai PluginContext dont seules les extrémités sont simulées. Cela couvre l'enregistrement des routes, la répartition des outils entre plugins, l'exécution au nom de l'utilisateur (on-behalf-of) et les délais d'expiration par appel. Rien du contexte n'est réimplémenté : un test ne peut donc pas s'écarter du comportement en production.

Le kit propose trois points d'entrée ainsi qu'un ensemble d'utilitaires de fixtures :

  • createTestApp({ plugins }) — démarrer une véritable application et l'appeler via de vraies requêtes HTTP. Commencez ici.
  • createTestPluginContext() — construire un vrai PluginContext aux extrémités simulées et l'attacher à un plugin, sans démarrage ni socket.
  • expectStream(...).toEmit(...) — vérifier l'ordre des types d'événements émis par un flux.
  • FixturescreateMockRequest, createMockResponse, createMockWorkspaceClient, mockServiceContext et les constructeurs de réponses SQL.

Le kit s'appuie sur le vi de Vitest pour ses mocks : vitest est donc une dépendance de pair optionnelle. Vous l'avez déjà (puisque vous écrivez des tests Vitest) et le kit utilise votre copie plutôt que d'en embarquer une seconde. Étant optionnelle, elle n'est pas installée dans les applications qui n'importent jamais @databricks/appkit/testing : les installations de production restent exemptes du framework de test. N'importe quelle version de Vitest v3 ou v4 convient.

Tester votre plugin

createTestApp({ plugins }) démarre une véritable application AppKit, avec le câblage Express, les routes et la validation des ressources réels, puis met à votre disposition des méthodes pour l'appeler comme le ferait un client :

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();
  }
});

Aucun workspace, aucun identifiant, aucun réseau. Le harnais de test force un NODE_ENV autre que development, se lie à un port éphémère, installe un faux client workspace et conserve le cache en mémoire, si bien qu'aucun appel ne sort.

Les chemins correspondent à la route montée complète. Le préfixe d'un plugin est /api/ suivi du nom de son manifeste en kebab-case : un plugin nommé mySearch est donc servi sur /api/my-search/….

Quel harnais choisir ?

createTestAppcreateTestPluginContext
Démarre l'applicationOuiNon
Ouvre un socketOui (port éphémère)Non
Middleware Express, gestionnaire d'erreursRéelsNon concernés
Validation des ressources / de l'environnementRéelle et stricteNon concernée
Client workspaceSimulé et injectéÀ simuler vous-même avec mockServiceContext
Nécessite close()OuiNon
RapiditéRapide, mais coût d'un socketLe plus rapide

Utilisez createTestApp pour tester de bout en bout le comportement HTTP d'un plugin. Utilisez createTestPluginContext pour tester unitairement le câblage : enregistrement des routes, répartition des outils, composition des délais d'expiration. Nommez les suites de tests utilisant ces harnais *.integration.test.ts, conformément à la convention existante.

Simuler ce que lit votre plugin

Déclarez les réponses sous forme de chemin pointé — "<service>.<method>" — sur la façade du client workspace d'AppKit :

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

Une valeur de type fonction reçoit les arguments de l'appel, ce qui vous permet de scripter un comportement propre à chaque argument ou de rejeter l'appel pour tester un chemin d'erreur. responses configure le mock intégré : le passer en même temps que votre propre client est donc rejeté plutôt qu'ignoré silencieusement — configurez plutôt les réponses sur ce client. Tout chemin que vous ne déclarez pas renvoie undefined au lieu de planter — voir Simuler les services Databricks pour le compromis que cela implique.

Pour la forme des réponses, appuyez-vous sur les types de services du SDK Databricks. Le kit ne les valide pas : une forme incorrecte échouera donc dans votre plugin, et non dans le faux client.

Avec une seule application ouverte, app.client est exactement l'objet que votre gestionnaire résout au runtime — accessible dans un plugin via getExecutionContext().client — ce qui vous permet d'y vérifier les appels :

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

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

getMock existe parce que les accesseurs de façade sont typés selon le SDK, si bien que expect(app.client.jobs.getRun).toHaveBeenCalled() ne passera pas la vérification de types.

Requêtes

app.get/post/put/patch/delete(path, options?) renvoient une Response native : expectStream s'y compose donc directement, sans passerelle.

  • body — toute valeur qui n'est pas une chaîne est encodée en JSON avec content-type: application/json. Une chaîne est envoyée telle quelle.
  • headers — fusionnés en dernier, ils l'emportent donc sur tout ce qu'a défini le harnais de test.
  • obotrue pour l'utilisateur de test par défaut, ou { userId, token, email }. Même raccourci que createMockRequest({ obo }) : un gestionnaire utilisant asUser(req) résout donc cette identité.
  • signal — transmis à fetch.

Teardown

Le harnais ouvre une socket : chaque démarrage doit donc s'accompagner d'un close(). Cet appel libère la socket, exécute les hooks shutdown() de votre plugin, supprime les singletons d'AppKit et restaure process.env dans son état d'avant le démarrage. Il est idempotent.

Privilégiez await using, qui ferme l'application à la sortie de la portée, même si le test lève une exception :

await using app = await createTestApp({ plugins: [myPlugin()] });
// libéré à la sortie de la portée

try/finally fonctionne également, et c'est la solution à adopter si l'application doit survivre à un bloc :

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

Si vous oubliez de fermer, l'application reste active — socket toujours lié, singletons et process.env non restaurés — et l'appel à createTestApp suivant est donc refusé (une seule application à la fois).

Satisfaire les ressources déclarées

Le harnais exécute le vrai validateur en mode strict : un plugin dont le manifeste requiert une ressource échoue donc au démarrage tant que sa variable d'environnement n'est pas définie. Fournissez-la via env :

// Lève une erreur : MY_WAREHOUSE_ID est requis par le manifeste.
await createTestApp({ plugins: [myPlugin()] });

// Démarre correctement.
await createTestApp({ plugins: [myPlugin()], env: { MY_WAREHOUSE_ID: "w-1" } });

Ainsi, « mon plugin déclare correctement ses ressources » devient une véritable assertion. env est restauré lors du close().

Ce que cela ne vérifie pas

Le harnais valide que les variables d'environnement des ressources requises sont présentes. Il ne valide pas les valeurs de configuration par rapport au config.schema de votre manifeste — aucun validateur au runtime n'existe encore pour cela. Un test qui démarre correctement vous confirme que vos déclarations de ressources et vos variables d'environnement sont bien câblées, mais ne dit rien de la validité de vos valeurs de configuration.

Autres options

  • server: false — pas de socket. La configuration, la validation et l'arrêt du plugin s'exécutent quand même ; les méthodes de requête lèvent une exception si elles sont appelées. Utile lorsque vous voulez seulement vérifier qu'un plugin démarre.
  • client — fournissez votre propre client workspace à la place du client factice intégré. C'est alors à vous de gérer son currentUser.me() : AppKit lit currentUser.id au démarrage et ne peut pas démarrer sans cette valeur.
  • nodeEnv — vaut "test" par défaut. "development" est refusé : le mode développement fait passer le port éphémère du harnais par get-port, qui lève une exception sur le port 0, et il démarre également un véritable serveur Vite tout en assouplissant la validation.
  • cache — en mémoire par défaut. C'est en le remplaçant que le cache pourrait accéder au réseau : n'y touchez donc pas, sauf si c'est justement l'objet du test.

createTestPluginContext()

PluginContext est le médiateur qu'AppKit transmet à chaque plugin : il met les routes en tampon, suit les fournisseurs d'outils et exécute les appels d'outils inter-plugins avec la portée utilisateur et un délai d'expiration. createTestPluginContext() renvoie le contexte réel, avec trois points de contact simulés :

Point de contactMode de simulation
TélémétrieUn fournisseur fictif sans effet — aucun pipeline OpenTelemetry requis.
Fournisseurs d'outilsDes doublures enregistrées via le véritable registerToolProvider, indexées par plugin puis par nom d'outil.
RoutesLes véritables addRoute/addMiddleware sont encapsulés pour consigner ce qu'un plugin enregistre.

Le contexte étant réel, executeTool résout toujours la portée utilisateur via asUser(req) et compose toujours le signal d'abandon à partir de votre délai d'expiration — ces chemins d'exécution sont donc réellement testés.

Enregistrer des réponses d'outils factices

Fournissez des réponses prédéfinies, indexées par nom de plugin, puis par nom d'outil. Une réponse est soit une valeur statique, soit une fonction des arguments de l'appel et du signal d'abandon composé :

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

const mock = createTestPluginContext({
  analytics: {
    // réponse statique
    top_users: [{ user: "alice", events: 42 }],
    // réponse sous forme de fonction — vérifiez les arguments ou simulez un traitement lent/interrompu
    query: (args, signal) => runFakeQuery(args, signal),
  },
});

Rattachement à un plugin

attach() relie le contexte à un plugin comme en production : la méthode initialise un cache en mémoire (si AppKit n'en a pas déjà créé un), puis appelle la méthode attachContext du plugin, qui reconstruit la télémétrie et bascule isReady sur true. Attendez son exécution avant d'exécuter un gestionnaire qui lit this.context, this.cache ou qui dépend de isReady :

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

Instanciez directement la classe du plugin (new MyAgentPlugin(...)). Les fabriques analytics() / agents() que vous passez à createApp renvoient un descripteur que l'application se charge de construire — or, pour un test unitaire, c'est l'instance qui vous intéresse.

Le client du workspace et le stub on-behalf-of sont eux aussi globaux au processus, et non propres à chaque application : ServiceContext ne détient qu'un seul client, et le faux createUserContext est un espion unique. C'est pourquoi createTestApp n'autorise qu'une seule application ouverte à la fois et lève une erreur si vous en démarrez une seconde avant de fermer la première — avec deux applications ouvertes, le client et les responses de la seconde n'atteindraient pas les gestionnaires, et fermer l'une retirerait à l'autre le faux OBO partagé. Vitest isole les fichiers de test dans des workers distincts : la contrainte se limite donc aux applications d'un même fichier. Une conséquence à connaître : un describe qui maintient une application ouverte dans beforeAll ne peut pas contenir un test qui démarre la sienne.

Le cache initialisé par attach() est un singleton global au processus : CacheManager est instancié une fois par processus de test, puis réutilisé. Vitest isole les fichiers de test dans des workers distincts, si bien que les caches ne fuient jamais d'un fichier à l'autre, mais les tests d'un même fichier le partagent. Si un test remplit le cache et qu'un test ultérieur du même fichier ne doit pas le voir, videz-le entre les tests avec resetTestCache() :

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

beforeEach(async () => {
  await resetTestCache(); // sans effet si le cache n'est pas encore initialisé
});

C'est également utile au sein même d'un test : videz le cache pour forcer un échec de cache (miss), puis vérifiez que l'appel suivant génère bien un succès de cache (hit).

Inspecter ce qui s'est passé

L'objet renvoyé expose des vues dynamiques que vous consultez une fois l'action testée exécutée :

await someHandler(req, res);

// Chaque répartition des outils inter-plugin, dans l'ordre.
expect(mock.toolCalls[0]).toMatchObject({
  plugin: "analytics",
  tool: "query",
  asUser: true, // prouve que le chemin « on-behalf-of » a bien été exécuté
});

// Chaque route enregistrée par le plugin (gestionnaires bruts, avant encapsulation).
expect(mock.routes).toContainEqual(
  expect.objectContaining({ method: "post", path: "/invocations" }),
);

// Le fournisseur de télémétrie injecté enregistre les spans propres au contexte —
// c.-à-d. le span que PluginContext.executeTool ouvre autour de chaque appel d'outil inter-plugin.
expect(mock.telemetry.getTracer().startActiveSpan).toHaveBeenCalled();

mock.telemetry est injecté dans le PluginContext : il capture donc les spans ouverts par le contexte (notamment executeTool). Il ne s'agit pas de la télémétrie propre au plugin : attachContext reconstruit this.telemetry à partir du véritable TelemetryManager, si bien que les spans ouverts en interne par un plugin ne se retrouvent pas dans mock.telemetry.

RecordedToolCall.asUser est le champ à vérifier pour les appels inter-plugins : comme le faux asUser applique la même précondition de jeton que le véritable Plugin.asUser, une répartition des outils qui enregistre asUser: true (avec userId renseigné) a réellement résolu la portée utilisateur de l'appelant, tandis qu'une requête dépourvue de x-forwarded-access-token est au contraire rejetée — la distinction OBO que des stubs { executeTool } silencieux ne permettent pas de vérifier. Vérifiez les deux cas de figure : une requête bien formée enregistre le userId attendu, et une requête sans jeton lève une erreur.

Le faux reproduit la précondition de jeton d'asUser, mais pas son marqueur de télémétrie interne en mode développement : avec NODE_ENV=development, le véritable Plugin.asUser ignore l'usurpation d'identité et positionne un indicateur OTel isDevOboFallback(), ce que le faux ne fait pas. Vérifiez l'OBO via les champs asUser/userId enregistrés plutôt que par isDevOboFallback().

expectStream(...)

Les plugins AppKit diffusent des Server-Sent Events. expectStream consomme un flux et vérifie l'ordre des types d'événements qu'il émet. Cette fonction accepte un itérable asynchrone (le run() d'un adaptateur d'agent), un simple tableau d'événements, une Response SSE (ou une promesse la retournant) dont elle analyse le corps, ou encore une createMockResponse() dont elle rejoue les écritures capturées.

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

// Correspondance de sous-séquence ordonnée — les événements intercalés (heartbeats, deltas) sont ignorés.
await expectStream(agent.adapter.run(input)).toEmit("tool_call", "message_delta");

// Correspondance exacte — la structure complète du flux, dans l'ordre, sans rien d'autre.
await expectStream(events).toEmitExactly("warehouse_status", "result");

// Ou collecter sans faire d'assertion.
const types = await expectStream(res).collectTypes();

Vérifier la route de streaming d'un plugin

La plupart des plugins diffusent du SSE depuis un gestionnaire de route (res.write(...)), et non depuis un simple générateur. createMockResponse() capture ces écritures et expectStream les relit directement : exécutez le véritable gestionnaire, puis vérifiez :

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

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

// Le mock a capturé le flux SSE écrit par le gestionnaire ; expectStream l'analyse.
await expectStream(res).toEmit("status", "result");

expectStream(res) et expectStream(res.sseResponse()) sont équivalents ; le second vous renvoie la Response brute si vous en avez besoin. Ne passez pas le corps SSE sous forme de chaîne : une chaîne est un itérable de caractères, et expectStream la rejette donc en vous orientant vers sseResponse() plutôt que d'émettre un « événement » par caractère.

toEmit vérifie que les types attendus apparaissent dans l'ordre, mais tolère d'autres événements avant, entre ou après eux — ce qui est exactement le comportement souhaité pour des flux entrelaçant des événements techniques comme les heartbeats ou les métadonnées. Utilisez toEmitExactly lorsque la forme du flux est entièrement déterminée.

expectStream met en mémoire tampon l'intégralité de la source avant de procéder aux assertions : sans cela, un flux qui ne se termine jamais resterait bloqué jusqu'au délai d'expiration du lanceur de tests lui-même. Passez { timeout } pour échouer rapidement avec une erreur explicite :

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

Fixtures

AppKit possède deux contextes, simulés par des outils différents. PluginContext joue le rôle de médiateur entre les plugins : il gère les routes, la répartition des outils et le cloisonnement par utilisateur ; createTestPluginContext() vous en donne la version réelle, avec des bords simulés. ServiceContext est le plan de données : il résout le client workspace, le service principal et l'ID du warehouse auxquels les plugins accèdent via getWorkspaceClient().

Le kit couvre désormais les deux. createTestApp simule le plan de données à votre place en injectant un client workspace factice au véritable point de jonction ; en dessous, mockServiceContext espionne directement le singleton, et createMockWorkspaceClient construit le client que l'un ou l'autre installe.

Le kit réexporte les fixtures de requête/réponse/contexte qu'AppKit utilise en interne :

  • createMockRequest(overrides?) / createMockResponse() — doublures de requête/réponse Express, y compris les indicateurs de streaming (headersSent, writableEnded). Passez obo: true (ou obo: { userId, token, email }) pour définir les en-têtes d'identité transmis qu'asUser exige, plutôt que de les ajouter à la main. createMockResponse() capture également tout ce qu'un gestionnaire écrit ; passez-le à expectStream (ou appelez sseResponse()) pour vérifier le SSE d'une route de streaming. (Les plugins résolvent le client workspace via getWorkspaceClient(), et non via la requête — utilisez mockServiceContext pour le contrôler.)

  • mockServiceContext(options?) — espionne le singleton ServiceContext afin que le code qui résout le service principal ou un contexte utilisateur reçoive des doublures de test. Appelez-le dans beforeEach, et appelez le restore() renvoyé dans afterEach.

  • useServiceContextMock(options?) — la même chose, en une ligne : l'installation dans beforeEach et la restauration dans afterEach sont enregistrées pour vous. Appelez-le en tête d'un bloc describe (pas à l'intérieur d'un test), et lisez la référence active .current depuis un test :

    describe("my plugin", () => {
      const ctx = useServiceContextMock();
      test("...", async () => {
        await handler(createMockRequest({ obo: true }), res);
        expect(ctx.current.createUserContextSpy).toHaveBeenCalled();
      });
    });
  • createSuccessfulSQLResponse(rows, columns) / createFailedSQLResponse(message) — construisent des réponses d'instruction SQL Warehouse.

  • setupDatabricksEnv(overrides?) — définit DATABRICKS_HOST / DATABRICKS_WAREHOUSE_ID avec des valeurs de test.

  • resetTestCache() — vide le singleton de cache partagé entre (ou pendant) les tests ; sans effet si le cache n'est pas encore initialisé. Le kit emploie les deux termes à dessein : un mock enregistre les appels pour que vous puissiez les vérifier (createMockWorkspaceClient, mockServiceContext), tandis qu'un fake prend la place et se contente de fonctionner (FakeProvider, FakeToolResponse).

  • createTestPlugin(factory, config?) — instancie un plugin à partir de sa fabrique avec la même fusion de configuration que celle appliquée par AppKit. Voir Exemple complet.

  • getListeningPort(server) — attend qu'un serveur termine son attachement et renvoie le port obtenu. createTestApp s'en charge pour vous ; utilisez-le lorsque vous démarrez vous-même un serveur avec port: 0.

Simuler les services Databricks

Toutes les opérations réelles des plugins de base passent par getWorkspaceClient(). createMockWorkspaceClient() simule l'ensemble de cette surface : un plugin qui fait appel à jobs, genie, servingEndpoints ou files devient ainsi testable sans avoir à construire manuellement un client imbriqué :

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, ne lève pas d'erreur

createTestApp en installe un pour vous ; n'y recourez donc directement que lorsque vous pilotez un plugin via createTestPluginContext ou mockServiceContext.

Fonctionnement et comportement attendu :

  • La façade est typée, donc client.jbos provoque une erreur de compilation. AppKit est propriétaire de l'interface : il s'agit d'un ensemble fermé, et non d'une course sans fin derrière le SDK.
  • Chaque service est un proxy qui crée un mock mémoïsé par méthode. client.jobs.getRun === client.jobs.getRun, les assertions d'appel sont donc stables, et toLegacyWorkspaceClient() partage les mêmes fonctions — une seule entrée responses couvre les deux vues.
  • config.host est une véritable chaîne (et non un mock), car AppKit s'en sert pour construire les URL. apiClient.userAgent() est synchrone pour la même raison, et apiClient.request résout {} afin que la déstructuration de son résultat ne lève pas d'erreur.
  • Des valeurs par défaut pertinentes sont intégrées : les instructions SQL réussissent, les warehouses signalent RUNNING et currentUser.me() renvoie un utilisateur de service. Passez defaults: false pour tout scripter vous-même.
Les méthodes non déclarées renvoient undefined

Une méthode non déclarée résout undefined au lieu de lever une erreur. C'est voulu — votre plugin continue de fonctionner même s'il sollicite des services dont le test n'a que faire — mais cela signifie qu'un appel dont vous avez oublié de déclarer la réponse renvoie silencieusement undefined plutôt que d'échouer bruyamment : un test peut donc passer pour de mauvaises raisons.

Passez strict: true pour transformer ce silence en échec : un appel vers un chemin sans réponse déclarée lève une erreur en nommant le chemin, au lieu de résoudre undefined. Les valeurs par défaut prédéfinies comptent toujours comme déclarées, le démarrage d'un harnais fonctionne donc à l'identique.

const app = await createTestApp({ plugins: [myPlugin()], strict: true });
// un gestionnaire qui appelle un chemin non déclaré fait désormais échouer la requête

TypeScript en couvre davantage qu'on ne pourrait le croire : comme chaque accesseur est typé d'après la classe de service du SDK lui-même, un service mal orthographié (client.jbos) tout comme une méthode mal orthographiée (client.jobs.getRunz) provoquent des erreurs de compilation. Ce qui échappe au filet, c'est une méthode réelle sans réponse déclarée — ainsi que tout appel qui contourne les types via un cast.

Autre divergence : les méthodes d'un service sont créées au moment de l'accès ; elles sont donc appelables mais non énumérables. typeof client.jobs.getRun vaut "function", mais 'getRun' in client.jobs vaut false et Object.keys(client.jobs) vaut []. Un code de plugin qui détecte les fonctionnalités avec in ou qui utilise la réflexion sur un service empruntera donc une branche différente de celle qu'il suit en production. C'est délibéré : exposer ces clés amènerait util.inspect à sonder chacune d'elles, créant un mock à chaque sondage, soit exactement la récursion incontrôlée que les pièges par défaut permettent d'éviter.

Par ailleurs, createLakebasePool({ workspaceClient }) construira un pool dont le callback de mot de passe se résout en un mock : le pool existe, mais ne peut pas se connecter. Un test Lakebase exige une véritable base de données ou un faux pool conçu à cet effet, pas ceci.

Exemple complet

Pour un plugin que vous avez écrit, instanciez directement la classe avec new. Les fonctions fabriques analytics() / agents() que vous passez à createApp renvoient un descripteur que l'application se chargera de construire, et non une instance.

Lorsque vous souhaitez obtenir une instance issue de l'une de ces fabriques, utilisez createTestPlugin plutôt que de passer par le descripteur :

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

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

// À ne pas faire — cela ignore DEFAULT_CONFIG et oublie `name` : l'instance
// n'est donc pas configurée comme celle créée en production :
//   const plugin = new (genie({}).plugin)({ spaceId: "s-1" });

createTestPlugin applique la même fusion qu'AppKit à l'enregistrement : DEFAULT_CONFIG, puis votre configuration, puis le name du manifeste. Cela vaut uniquement pour ce cas de test unitaire — createTestApp reçoit des descripteurs et construit lui-même les instances.

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

// Un petit plugin qui enregistre une route et diffuse deux événements en flux.
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",
    );
  });
});

Pour tester un plugin qui déclenche des appels d'outils inter-plugins, enregistrez de faux fournisseurs et effectuez des assertions sur mock.toolCalls — y compris asUser, qui confirme que le chemin « au nom de l'utilisateur » a bien été emprunté :

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

// `obo` définit les en-têtes d'identité transmis dont `asUser` a besoin — sans eux,
// la dispatch serait (à juste titre) rejetée avec « Missing user token ».
const req = createMockRequest({ obo: true });
await plugin.runSomethingThatCallsAnalytics(req);

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

Voir aussi

Databricks Developer Hub

Prêt à lancer votre prochaine application agentique en quelques minutes ?

Lire la documentation