Testes
Testes
O AppKit inclui um kit de testes em @databricks/appkit/testing para que você possa testar um plugin, incluindo suas chamadas de ferramentas entre plugins e respostas em streaming, sem precisar de um workspace Databricks ativo, credenciais ou acesso à rede. Os testes de plugins continuam rápidos e rodam em CI, onde não há workspace disponível.
Objetivo
Exercitar os caminhos de código reais de um plugin contra um PluginContext real, com apenas as bordas externas simuladas. Isso cobre registro de rotas, despacho de ferramentas entre plugins, execução no escopo do usuário (em nome dele) e timeouts por chamada. Nada do contexto é reimplementado, portanto um teste não tem como divergir do comportamento em produção.
O kit tem três pontos de entrada, além de um conjunto de helpers de fixtures:
createTestApp({ plugins })— inicializa um app real e o chama por HTTP real. Comece por aqui.createTestPluginContext()— cria umPluginContextreal com bordas simuladas e o anexa a um plugin, sem inicialização e sem socket.expectStream(...).toEmit(...)— verifica a ordem dos tipos de evento emitidos por um stream.- Fixtures —
createMockRequest,createMockResponse,createMockWorkspaceClient,mockServiceContexte builders de respostas SQL.
O kit usa o vi do Vitest para seus mocks, então vitest é uma peer dependency opcional: você já a tem (afinal, está escrevendo testes com Vitest), e o kit resolve para a sua cópia em vez de empacotar uma segunda. Por ser opcional, ela não é instalada em apps que nunca importam @databricks/appkit/testing — as instalações de produção ficam livres do framework de testes. Qualquer Vitest v3 ou v4 funciona.
Testando seu plugin
createTestApp({ plugins }) inicia um app AppKit real, com a integração real do Express, rotas e validação de recursos, e então oferece métodos para chamá-lo como um cliente faria:
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();
}
});Sem workspace, sem credenciais, sem rede. O harness fixa um NODE_ENV que não é de desenvolvimento, vincula uma porta efêmera, instala um cliente do workspace falso e mantém o cache em memória, de modo que nada faz chamadas externas.
Os caminhos correspondem à rota montada completa. O prefixo de um plugin é /api/ seguido do nome do manifesto em kebab-case, ou seja, um plugin chamado mySearch é servido em /api/my-search/….
Qual harness usar?
createTestApp | createTestPluginContext | |
|---|---|---|
| Inicializa a aplicação | Sim | Não |
| Vincula um socket | Sim (porta efêmera) | Não |
| Middleware do Express, error handler | Real | Não se aplica |
| Validação de recursos / ambiente | Real e estrita | Não se aplica |
| Cliente do workspace | Simulado e injetado | Simule você mesmo com mockServiceContext |
Precisa de close() | Sim | Não |
| Velocidade | Rápido, mas com o custo de um socket | Mais rápido |
Use createTestApp para testar o comportamento HTTP de um plugin de ponta a ponta. Use createTestPluginContext para testes unitários das conexões internas: registro de rotas, despacho de ferramentas, composição de timeouts. Nomeie as suítes de harness como *.integration.test.ts, seguindo a convenção existente.
Simulando o que seu plugin lê
Declare as respostas pelo caminho em notação de ponto — "<service>.<method>" na fachada do workspace client do AppKit:
const app = await createTestApp({
plugins: [myPlugin()],
responses: {
"jobs.getRun": { state: "TERMINATED", result_state: "SUCCESS" },
"statementExecution.executeStatement": { status: { state: "SUCCEEDED" } },
"apiClient.request": { results: [] },
},
});Um valor de função recebe os argumentos da chamada, permitindo programar o comportamento por argumento ou rejeitar para testar um caminho de erro. responses configura o mock embutido, portanto passá-lo junto com seu próprio client é rejeitado em vez de ignorado silenciosamente — nesse caso, configure as respostas no próprio client. Qualquer caminho que você não declarar resolve undefined em vez de quebrar — veja Simulando services do Databricks para entender a contrapartida dessa escolha.
Para as formas das respostas, siga os tipos de service do SDK do Databricks. O kit não os valida, então uma forma incorreta falha no seu plugin, e não no falso.
Com um app aberto, app.client é exatamente o objeto que seu handler resolve em runtime — acessado dentro de um plugin via getExecutionContext().client — o que permite fazer asserções sobre as chamadas nele:
import { getMock } from "@databricks/appkit/testing";
expect(getMock(app.client, "jobs.getRun")).toHaveBeenCalledWith({ run_id: 42 });getMock existe porque os acessadores da fachada são tipados com base no SDK, ou seja, expect(app.client.jobs.getRun).toHaveBeenCalled() não passa na verificação de tipos.
Requisições
app.get/post/put/patch/delete(path, options?) retornam um Response nativo, então expectStream se integra diretamente, sem necessidade de ponte.
body— um valor que não seja string é codificado em JSON comcontent-type: application/json. Uma string é enviada como está.headers— mesclados por último, portanto prevalecem sobre qualquer valor definido pelo harness.obo—truepara o usuário de teste padrão, ou{ userId, token, email }. Mesma forma abreviada decreateMockRequest({ obo }), de modo que um handler que usaasUser(req)resolve essa identidade.signal— encaminhado para ofetch.
Teardown
O harness vincula um socket, portanto toda inicialização precisa de um close(). Ele libera o socket, executa os hooks shutdown() do seu plugin, descarta os singletons do AppKit e restaura o process.env ao estado anterior à inicialização. É idempotente.
Prefira await using, que fecha a aplicação ao sair do escopo mesmo que o teste lance uma exceção:
await using app = await createTestApp({ plugins: [myPlugin()] });
// liberado ao sair do escopotry/finally também funciona e é o que você precisa se o app tiver que continuar ativo além de um bloco:
const app = await createTestApp({ plugins: [myPlugin()] });
try {
// ...
} finally {
await app.close();
}Se você esquecer de fechar, a aplicação continua ativa — socket vinculado, singletons e process.env não restaurados — e o próximo createTestApp será recusado (apenas uma aplicação por vez).
Atendendo aos recursos declarados
O harness executa o validador real em modo estrito, portanto um plugin cujo manifesto exige um recurso falha na inicialização, a menos que sua variável de ambiente esteja definida. Forneça-a com env:
// Lança erro: MY_WAREHOUSE_ID é exigido pelo manifesto.
await createTestApp({ plugins: [myPlugin()] });
// Inicializa.
await createTestApp({ plugins: [myPlugin()], env: { MY_WAREHOUSE_ID: "w-1" } });Isso torna "meu plugin declara seus recursos corretamente" uma afirmação genuína. O env é restaurado no close().
O harness valida que as variáveis de ambiente dos recursos obrigatórios estão presentes. Ele não valida os valores de configuração em relação ao config.schema do seu manifesto — ainda não existe um validador em runtime para isso. Um teste que inicializa com sucesso indica que suas declarações de recursos e o env estão devidamente conectados; mas não diz nada sobre se os valores de configuração estão bem formados.
Outras opções
server: false— sem socket. A configuração, a validação e o encerramento do plugin continuam sendo executados; os métodos de requisição lançam erro se chamados. Útil quando você só quer verificar se o plugin inicializa.client— forneça seu próprio cliente do workspace em vez do fake embutido. Nesse caso, você passa a ser responsável pelocurrentUser.me(): o AppKit lêcurrentUser.iddurante a inicialização e não consegue iniciar sem ele.nodeEnv— o padrão é"test"."development"é recusado: o modo de desenvolvimento roteia a porta efêmera do harness peloget-port, que lança erro na porta0, e também inicializa um servidor Vite real e flexibiliza a validação.cache— o padrão é em memória. Sobrescrevê-lo é justamente o que permitiria que o cache acessasse a rede, então não mexa nele, a menos que esse seja o objetivo do teste.
createTestPluginContext()
PluginContext é o mediador que o AppKit passa para cada plugin: ele mantém rotas em buffer, rastreia provedores de ferramentas e executa chamadas de ferramentas entre plugins com escopo de usuário e timeout. createTestPluginContext() retorna o contexto real com três pontos simulados:
| Ponto | Como é simulado |
|---|---|
| Telemetria | Um provedor mock sem operação (no-op) — não é necessário nenhum pipeline do OpenTelemetry. |
| Provedores de ferramentas | Objetos falsos registrados pelo registerToolProvider real, indexados por plugin e, em seguida, por nome da ferramenta. |
| Rotas | Os métodos reais addRoute/addMiddleware são encapsulados para registrar o que cada plugin cadastra. |
Como o contexto é real, executeTool continua resolvendo o escopo do usuário via asUser(req) e continua compondo o sinal de cancelamento a partir do seu timeout — ou seja, esses caminhos são de fato testados.
Registrando respostas falsas de ferramentas
Passe respostas predefinidas indexadas pelo nome do plugin e, em seguida, pelo nome da ferramenta. A resposta pode ser um valor estático ou uma função que recebe os argumentos da chamada e o sinal de cancelamento composto:
import { createTestPluginContext } from "@databricks/appkit/testing";
const mock = createTestPluginContext({
analytics: {
// resposta estática
top_users: [{ user: "alice", events: 42 }],
// resposta em forma de função — valide os argumentos ou simule uma execução lenta/abortada
query: (args, signal) => runFakeQuery(args, signal),
},
});Anexar a um plugin
attach() conecta o contexto a um plugin da mesma forma que em produção: ele cria um cache em memória (caso o AppKit ainda não tenha criado um) e, em seguida, chama o attachContext do plugin, que reconstrói a telemetria e altera isReady para true. Aguarde sua conclusão antes de executar qualquer handler que leia this.context, this.cache ou que dependa de isReady:
const plugin = new MyAgentPlugin({});
await mock.attach(plugin);Instancie a classe do plugin diretamente (new MyAgentPlugin(...)). As factories analytics() / agents() que você passa para createApp retornam um descritor para o app construir — em um teste unitário, o que você quer é a instância.
O cliente do workspace e o stub de on-behalf-of também valem para todo o processo, e não por app: o ServiceContext mantém um único cliente, e o falso de createUserContext é um único spy. Por isso, o createTestApp permite apenas um app aberto por vez e lança erro se você inicializar um segundo antes de fechar o primeiro — com dois abertos, o client e as responses do segundo não chegariam aos handlers, e fechar qualquer um deles removeria do outro o falso de OBO compartilhado. O Vitest isola arquivos de teste em workers separados, portanto essa restrição vale apenas para apps dentro de um mesmo arquivo. Uma consequência que vale conhecer: um describe que mantém um app aberto no beforeAll não pode conter um teste que inicialize o seu próprio.
O cache que o attach() popula é um singleton de escopo de processo: o CacheManager é inicializado uma vez por processo de teste e reutilizado. O Vitest isola arquivos de teste em workers separados, portanto os caches nunca vazam entre arquivos, mas os testes dentro de um mesmo arquivo o compartilham. Se um teste popula o cache e um teste posterior no mesmo arquivo não pode enxergá-lo, limpe-o entre os testes com resetTestCache():
import { resetTestCache } from "@databricks/appkit/testing";
beforeEach(async () => {
await resetTestCache(); // não faz nada se o cache ainda não tiver sido inicializado
});Isso também ajuda dentro de um mesmo teste — limpe o cache para forçar um miss e, em seguida, verifique se a chamada seguinte é um hit.
Inspecionando o que aconteceu
O objeto retornado expõe views em tempo real que você consulta após a execução da ação em teste:
await someHandler(req, res);
// Todo despacho de ferramenta entre plugins, em ordem.
expect(mock.toolCalls[0]).toMatchObject({
plugin: "analytics",
tool: "query",
asUser: true, // comprova que o caminho on-behalf-of foi executado
});
// Toda rota registrada pelo plugin (handlers crus, antes do wrapping).
expect(mock.routes).toContainEqual(
expect.objectContaining({ method: "post", path: "/invocations" }),
);
// O provedor de telemetria injetado registra os spans do próprio contexto — ou seja,
// o span que PluginContext.executeTool abre em torno de cada chamada de ferramenta entre plugins.
expect(mock.telemetry.getTracer().startActiveSpan).toHaveBeenCalled();mock.telemetry é injetado no PluginContext, portanto captura os spans que o contexto abre (em especial executeTool). Ele não é a telemetria do próprio plugin: attachContext reconstrói this.telemetry a partir do TelemetryManager real, de modo que os spans abertos internamente por um plugin não chegam ao mock.telemetry.
RecordedToolCall.asUser é o campo a verificar em chamadas entre plugins: como o asUser falso impõe a mesma precondição de token que o Plugin.asUser real, um despacho que registra asUser: true (com userId definido) de fato resolveu o escopo de usuário do chamador, e uma requisição sem x-forwarded-access-token é rejeitada — a distinção de OBO que stubs silenciosos de { executeTool } não conseguem verificar. Verifique os dois casos: uma requisição bem formada registra o userId esperado, e uma sem token lança erro.
O falso replica a precondição de token do asUser, não seu marcador interno de telemetria em modo de desenvolvimento: em NODE_ENV=development, o Plugin.asUser real ignora a personificação e define uma flag OTel isDevOboFallback(), que o falso não reproduz. Verifique o OBO pelos campos asUser/userId registrados, e não por isDevOboFallback().
expectStream(...)
Os plugins do AppKit transmitem Server-Sent Events. O expectStream consome um stream e valida os tipos de eventos emitidos e sua ordem. Ele aceita um iterável assíncrono (o run() de um adaptador de agente), um array simples de eventos, uma Response SSE (ou uma promise que resolva para ela), cujo corpo é analisado, ou um createMockResponse(), cujas escritas capturadas são reproduzidas.
import { expectStream } from "@databricks/appkit/testing";
// Correspondência de subsequência em ordem — eventos intercalados (heartbeats, deltas) são ignorados.
await expectStream(agent.adapter.run(input)).toEmit("tool_call", "message_delta");
// Correspondência exata — o formato completo do stream, em ordem, e nada além disso.
await expectStream(events).toEmitExactly("warehouse_status", "result");
// Ou colete sem fazer asserções.
const types = await expectStream(res).collectTypes();Fazendo asserções sobre a rota de streaming de um plugin
A maioria dos plugins transmite SSE a partir de um handler de rota (res.write(...)), e não de um gerador simples. O createMockResponse() captura essas escritas, e o expectStream as lê diretamente — execute o handler real e faça as asserções:
import { createMockRequest, createMockResponse, expectStream } from "@databricks/appkit/testing";
const res = createMockResponse();
await plugin._handleStream(createMockRequest({ obo: true }), res);
// O mock capturou o SSE escrito pelo handler; expectStream faz o parsing.
await expectStream(res).toEmit("status", "result");expectStream(res) e expectStream(res.sseResponse()) são equivalentes; o segundo devolve o Response bruto, caso você precise dele. Não passe o corpo SSE como string: uma string é um iterável de caracteres, portanto expectStream a rejeita indicando o uso de sseResponse(), em vez de emitir um "evento" por caractere.
toEmit verifica se os tipos esperados aparecem em ordem, mas tolera outros eventos antes, entre ou depois deles — que é justamente o que você quer em streams que intercalam eventos de controle, como heartbeats ou metadados. Use toEmitExactly quando o formato do stream for totalmente determinado.
expectStream armazena toda a origem em buffer antes de fazer as asserções, de modo que um stream que nunca termina ficaria travado até o tempo limite do próprio executor de testes. Passe { timeout } para falhar rapidamente com um erro claro:
await expectStream(handler.stream(req), { timeout: 1000 }).toEmit("result");Fixtures
O AppKit tem dois contextos, e cada um é simulado por uma ferramenta diferente. O PluginContext é o mediador entre plugins, cuidando de rotas, despacho de ferramentas e escopo de usuário; createTestPluginContext() entrega o objeto real, apenas com as bordas simuladas. Já o ServiceContext é o plano de dados: ele resolve o cliente do workspace, o service principal e o ID do warehouse que os plugins acessam por meio de getWorkspaceClient().
O kit agora cobre os dois. O createTestApp simula o plano de dados para você, injetando um cliente do workspace mockado no ponto de junção real; em um nível abaixo, mockServiceContext espiona o singleton diretamente, e createMockWorkspaceClient constrói o cliente que qualquer um dos dois instala.
O kit reexporta as fixtures de requisição/resposta/contexto que o próprio AppKit usa internamente:
createMockRequest(overrides?)/createMockResponse()— dublês de requisição/resposta do Express, incluindo as flags de streaming (headersSent,writableEnded). Passeobo: true(ouobo: { userId, token, email }) para definir os cabeçalhos de identidade encaminhada exigidos porasUser, em vez de adicioná-los na mão. OcreateMockResponse()também captura tudo o que um handler escreve; passe-o paraexpectStream(ou chamesseResponse()) para fazer asserções sobre o SSE de uma rota de streaming. (Os plugins resolvem o cliente do workspace por meio degetWorkspaceClient(), e não pela requisição — usemockServiceContextpara controlá-lo.)mockServiceContext(options?)— espiona o singletonServiceContextpara que o código que resolve o service principal ou um contexto de usuário receba dublês de teste. Chame embeforeEache chame orestore()retornado emafterEach.useServiceContextMock(options?)— o mesmo, em uma linha: ele registra a instalação nobeforeEache a restauração noafterEachpara você. Chame no topo de um blocodescribe(não dentro de um teste) e leia o handle ativo.currentde dentro de um teste:describe("my plugin", () => { const ctx = useServiceContextMock(); test("...", async () => { await handler(createMockRequest({ obo: true }), res); expect(ctx.current.createUserContextSpy).toHaveBeenCalled(); }); });createSuccessfulSQLResponse(rows, columns)/createFailedSQLResponse(message)— constroem respostas de statement do SQL Warehouse.setupDatabricksEnv(overrides?)— defineDATABRICKS_HOST/DATABRICKS_WAREHOUSE_IDcom valores de teste.resetTestCache()— limpa o singleton de cache compartilhado entre (ou dentro de) testes; não faz nada se o cache ainda não tiver sido inicializado. O kit usa as duas palavras de forma deliberada: um mock registra chamadas para que você possa fazer asserções sobre elas (createMockWorkspaceClient,mockServiceContext), enquanto um falso apenas ocupa o lugar e simplesmente funciona (FakeProvider,FakeToolResponse).createTestPlugin(factory, config?)— instancia um plugin a partir de sua factory com a mesma mesclagem de configuração que o AppKit aplica. Veja Exemplo completo.getListeningPort(server)— aguarda o servidor concluir o bind e retorna a porta em que ele ficou. OcreateTestAppfaz isso por você; recorra a ele quando você mesmo iniciar um servidor comport: 0.
Simulando serviços do Databricks
Todo o trabalho real de qualquer plugin principal passa por getWorkspaceClient(). O createMockWorkspaceClient() simula toda essa superfície, de modo que um plugin que usa jobs, genie, servingEndpoints ou files pode ser testado sem precisar montar manualmente um cliente aninhado:
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, não lança errocreateTestApp instala um deles para você, então recorra a ele diretamente apenas quando estiver conduzindo um plugin por meio de createTestPluginContext ou mockServiceContext.
Como funciona e o que esperar:
- A fachada é tipada, então
client.jbosé um erro de compilação. A interface pertence ao AppKit, portanto trata-se de um conjunto fechado, e não de uma corrida sem fim atrás do SDK. - Cada service é um proxy que cria um mock memoizado por método.
client.jobs.getRun === client.jobs.getRun, então as asserções de chamada são estáveis, etoLegacyWorkspaceClient()compartilha as mesmas funções — uma única entrada emresponsescobre as duas views. config.hosté uma string de verdade (não um mock), porque o AppKit constrói URLs a partir dela.apiClient.userAgent()é síncrono pelo mesmo motivo, eapiClient.requestresolve{}para que a desestruturação do resultado não gere exceção.- Padrões sensatos já vêm embutidos: instruções SQL são bem-sucedidas, warehouses reportam
RUNNINGecurrentUser.me()retorna um usuário de serviço. Passedefaults: falsepara definir tudo você mesmo.
Um método não declarado resolve undefined em vez de lançar exceção. É esse justamente o objetivo — seu plugin continua funcionando ao acessar services que não interessam ao teste —, mas isso significa que uma chamada cuja resposta você esqueceu de declarar retorna undefined silenciosamente, em vez de falhar de forma explícita, de modo que um teste pode passar pelo motivo errado.
Passe strict: true para transformar esse silêncio em falha: uma chamada a um caminho sem resposta declarada lança exceção em vez de resolver undefined, informando o nome do caminho. Os padrões pré-definidos continuam contando como declarados, então a inicialização do ambiente de teste funciona sem alterações.
const app = await createTestApp({ plugins: [myPlugin()], strict: true });
// um handler que chama um caminho não declarado agora faz a requisição falharO TypeScript cobre mais disso do que você imagina: como cada acessador é tipado com base na própria classe de serviço do SDK, tanto um serviço com o nome errado (client.jbos) quanto um método com o nome errado (client.jobs.getRunz) geram erros de compilação. A lacuna fica em um método real sem resposta declarada — e em qualquer chamada que burle os tipos com um cast.
Mais uma divergência: os métodos de um serviço são criados no momento do acesso, portanto são chamáveis, mas não enumeráveis. typeof client.jobs.getRun é "function", mas 'getRun' in client.jobs é false e Object.keys(client.jobs) é []. Por isso, código de plugin que faz detecção de recursos com in ou que usa reflexão sobre um serviço seguirá um caminho diferente do que seguiria em produção. Isso é intencional: expor essas chaves faria o util.inspect sondar cada uma delas, criando um mock por sondagem — exatamente a recursão descontrolada que os traps padrão evitam.
Em separado, createLakebasePool({ workspaceClient }) criará um pool cujo callback de senha resolve para um mock: o pool existe, mas não consegue se conectar. Um teste de Lakebase precisa de um banco de dados real ou de um pool falso feito sob medida, não disso.
Exemplo completo
Para um plugin que você mesmo escreveu, instancie a classe diretamente com new. As funções de fábrica analytics() / agents() que você passa para createApp retornam um descritor a ser construído pelo app, não uma instância.
Quando precisar de uma instância vinda de uma dessas fábricas, use createTestPlugin em vez de acessá-la por meio do descritor:
import { createTestPlugin } from "@databricks/appkit/testing";
const plugin = createTestPlugin(genie, { spaceId: "s-1" });
// Assim não — isso ignora o DEFAULT_CONFIG e esquece o `name`, de modo que a
// instância fica configurada de forma diferente da que é criada em produção:
// const plugin = new (genie({}).plugin)({ spaceId: "s-1" });createTestPlugin aplica a mesma mesclagem que o AppKit faz no registro: DEFAULT_CONFIG, depois a sua configuração e, por fim, o name do manifesto. Isso vale apenas para este caminho de teste unitário — createTestApp recebe descritores e constrói as instâncias por conta própria.
import { Plugin, type PluginManifest } from "@databricks/appkit";
import { expectStream, createMockRequest, createTestPluginContext } from "@databricks/appkit/testing";
import { describe, expect, test } from "vitest";
// Um plugin simples que registra uma rota e emite dois eventos em streaming.
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",
);
});
});Para testar um plugin que dispara chamadas de ferramentas entre plugins, registre provedores falsos e faça asserções sobre mock.toolCalls — incluindo asUser, que confirma que o fluxo em nome do usuário foi executado:
const mock = createTestPluginContext({ analytics: { query: [{ n: 1 }] } });
const plugin = new MyAgentPlugin({});
await mock.attach(plugin);
// `obo` define os cabeçalhos de identidade encaminhados que `asUser` exige — sem eles, o
// dispatch rejeitaria (corretamente) com "Missing user token".
const req = createMockRequest({ obo: true });
await plugin.runSomethingThatCallsAnalytics(req);
expect(mock.toolCalls[0]).toMatchObject({
plugin: "analytics",
tool: "query",
asUser: true,
});Veja também
- Plugins personalizados — crie os plugins que você testa com este kit.
- Contexto de execução — como
asUsere o service principal se diferenciam em runtime. - Desenvolvimento local — execute seu app com hot reload durante as iterações.