@databricks/appkit-ui 0.61.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/NOTICE.md +1 -0
- package/dist/cli/commands/codemod/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/index.js.map +1 -1
- package/dist/cli/commands/doctor/report.js.map +1 -1
- package/dist/cli/commands/doctor/resolve-targets.js +4 -6
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
- package/dist/cli/commands/doctor/run.js.map +1 -1
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/client.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js.map +1 -1
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/cli/commands/registry/index.js.map +1 -1
- package/dist/cli/commands/registry/info.js.map +1 -1
- package/dist/cli/commands/registry/list.js.map +1 -1
- package/dist/cli/commands/registry/requirements.js.map +1 -1
- package/dist/cli/commands/registry/server-register.js.map +1 -1
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/js/arrow/arrow-client.d.ts.map +1 -1
- package/dist/js/arrow/arrow-client.js.map +1 -1
- package/dist/react/charts/area/index.d.ts.map +1 -1
- package/dist/react/charts/area/index.js.map +1 -1
- package/dist/react/charts/bar/index.d.ts.map +1 -1
- package/dist/react/charts/bar/index.js.map +1 -1
- package/dist/react/charts/base.d.ts.map +1 -1
- package/dist/react/charts/base.js +1 -1
- package/dist/react/charts/base.js.map +1 -1
- package/dist/react/charts/heatmap/index.d.ts.map +1 -1
- package/dist/react/charts/heatmap/index.js.map +1 -1
- package/dist/react/charts/line/index.d.ts.map +1 -1
- package/dist/react/charts/line/index.js.map +1 -1
- package/dist/react/charts/normalize.d.ts.map +1 -1
- package/dist/react/charts/normalize.js.map +1 -1
- package/dist/react/charts/pie/index.d.ts.map +1 -1
- package/dist/react/charts/pie/index.js.map +1 -1
- package/dist/react/charts/radar/index.d.ts.map +1 -1
- package/dist/react/charts/radar/index.js.map +1 -1
- package/dist/react/charts/scatter/index.d.ts.map +1 -1
- package/dist/react/charts/scatter/index.js.map +1 -1
- package/dist/react/charts/theme.d.ts.map +1 -1
- package/dist/react/charts/theme.js.map +1 -1
- package/dist/react/charts/wrapper.d.ts.map +1 -1
- package/dist/react/charts/wrapper.js.map +1 -1
- package/dist/react/file-browser/directory-list.d.ts.map +1 -1
- package/dist/react/file-browser/directory-list.js.map +1 -1
- package/dist/react/file-browser/file-breadcrumb.d.ts.map +1 -1
- package/dist/react/file-browser/file-breadcrumb.js.map +1 -1
- package/dist/react/file-browser/file-entry.d.ts.map +1 -1
- package/dist/react/file-browser/file-entry.js.map +1 -1
- package/dist/react/file-browser/file-preview-panel.d.ts.map +1 -1
- package/dist/react/file-browser/file-preview-panel.js.map +1 -1
- package/dist/react/file-browser/new-folder-input.d.ts.map +1 -1
- package/dist/react/file-browser/new-folder-input.js.map +1 -1
- package/dist/react/genie/genie-chat-input.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-input.js.map +1 -1
- package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message-list.js.map +1 -1
- package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message.js.map +1 -1
- package/dist/react/genie/genie-query-visualization.d.ts.map +1 -1
- package/dist/react/genie/genie-query-visualization.js.map +1 -1
- package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.js.map +1 -1
- package/dist/react/hooks/types.d.ts.map +1 -1
- package/dist/react/hooks/use-agent-chat.d.ts.map +1 -1
- package/dist/react/hooks/use-agent-chat.js.map +1 -1
- package/dist/react/hooks/use-ai-search-query.d.ts.map +1 -1
- package/dist/react/hooks/use-ai-search-query.js.map +1 -1
- package/dist/react/hooks/use-analytics-query.d.ts.map +1 -1
- package/dist/react/hooks/use-analytics-query.js.map +1 -1
- package/dist/react/hooks/use-analytics-warehouse-status.js.map +1 -1
- package/dist/react/hooks/use-chart-data.d.ts.map +1 -1
- package/dist/react/hooks/use-chart-data.js.map +1 -1
- package/dist/react/hooks/use-metric-view.d.ts.map +1 -1
- package/dist/react/hooks/use-metric-view.js.map +1 -1
- package/dist/react/hooks/use-plugin-config.d.ts.map +1 -1
- package/dist/react/hooks/use-plugin-config.js.map +1 -1
- package/dist/react/hooks/use-serving-invoke.d.ts.map +1 -1
- package/dist/react/hooks/use-serving-invoke.js.map +1 -1
- package/dist/react/hooks/use-serving-stream.d.ts.map +1 -1
- package/dist/react/hooks/use-serving-stream.js.map +1 -1
- package/dist/react/resource-status-indicator.d.ts.map +1 -1
- package/dist/react/resource-status-indicator.js.map +1 -1
- package/dist/react/table/data-table.d.ts.map +1 -1
- package/dist/react/table/data-table.js.map +1 -1
- package/dist/react/table/table-wrapper.js.map +1 -1
- package/dist/react/ui-variants/variants.d.ts.map +1 -1
- package/dist/react/ui-variants/variants.js.map +1 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/styles.css +0 -3
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts.map +1 -1
- package/docs/plugins/testing.md +245 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
AppKit ships a testing kit at `@databricks/appkit/testing` so you can test a plugin — including its cross-plugin tool calls and streaming responses — without a live Databricks workspace, credentials, or network access. That makes plugin tests fast and lets them run in CI, where no workspace is available.
|
|
4
|
+
|
|
5
|
+
## Goal[](#goal "Direct link to Goal")
|
|
6
|
+
|
|
7
|
+
Exercise a plugin's real code paths — route registration, cross-plugin tool dispatch, user-scoped (on-behalf-of) execution, and per-call timeouts — against a real `PluginContext` with only its outer edges faked. Nothing about the context is reimplemented, so a test can't drift from production behavior.
|
|
8
|
+
|
|
9
|
+
The kit has two entry points plus a set of fixture helpers:
|
|
10
|
+
|
|
11
|
+
* **`createTestPluginContext()`** — build a real `PluginContext` with faked edges and attach it to a plugin.
|
|
12
|
+
* **`expectStream(...).toEmit(...)`** — assert the ordered event types a stream emits.
|
|
13
|
+
* **Fixtures** — `createMockRequest`, `createMockResponse`, `mockServiceContext`, and SQL response builders.
|
|
14
|
+
|
|
15
|
+
The kit uses [Vitest](https://vitest.dev)'s `vi` for its mocks, so `vitest` is an **optional peer dependency**: you already have it (you're writing Vitest tests), and the kit resolves to your copy rather than bundling a second one. Because it's optional, it is not installed into apps that never import `@databricks/appkit/testing` — production installs stay free of the test framework. Any Vitest v3 or v4 works.
|
|
16
|
+
|
|
17
|
+
## `createTestPluginContext()`[](#createtestplugincontext "Direct link to createtestplugincontext")
|
|
18
|
+
|
|
19
|
+
`PluginContext` is the mediator AppKit passes to every plugin — it buffers routes, tracks tool providers, and runs cross-plugin tool calls with user scoping and a timeout. `createTestPluginContext()` returns the **real** context with three edges faked:
|
|
20
|
+
|
|
21
|
+
| Edge | How it's faked |
|
|
22
|
+
| -------------- | ----------------------------------------------------------------------------------------- |
|
|
23
|
+
| Telemetry | A no-op mock provider — no OpenTelemetry pipeline needed. |
|
|
24
|
+
| Tool providers | Fakes registered through the real `registerToolProvider`, keyed by plugin then tool name. |
|
|
25
|
+
| Routes | The real `addRoute`/`addMiddleware` are wrapped to record what a plugin registers. |
|
|
26
|
+
|
|
27
|
+
Because the context is real, `executeTool` still resolves the user scope via `asUser(req)` and still composes the abort signal from your timeout — so those paths are genuinely under test.
|
|
28
|
+
|
|
29
|
+
### Registering fake tool responses[](#registering-fake-tool-responses "Direct link to Registering fake tool responses")
|
|
30
|
+
|
|
31
|
+
Pass canned responses keyed by plugin name, then tool name. A response is either a static value or a function of the call arguments and the composed abort signal:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createTestPluginContext } from "@databricks/appkit/testing";
|
|
35
|
+
|
|
36
|
+
const mock = createTestPluginContext({
|
|
37
|
+
analytics: {
|
|
38
|
+
// static response
|
|
39
|
+
top_users: [{ user: "alice", events: 42 }],
|
|
40
|
+
// function response — assert on args, or simulate slow/aborting work
|
|
41
|
+
query: (args, signal) => runFakeQuery(args, signal),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Attaching to a plugin[](#attaching-to-a-plugin "Direct link to Attaching to a plugin")
|
|
48
|
+
|
|
49
|
+
`attach()` wires the context to a plugin the production way: it seeds an in-memory cache (if AppKit hasn't already initialized one), then calls the plugin's `attachContext`, which rebuilds telemetry and flips `isReady` to `true`. Await it before exercising any handler that reads `this.context`, `this.cache`, or gates on `isReady`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const plugin = new MyAgentPlugin({ dir: false });
|
|
53
|
+
await mock.attach(plugin);
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Instantiate the plugin **class** directly (`new MyAgentPlugin(...)`). The `analytics()` / `agents()` factories you pass to `createApp` return a descriptor for the app to construct — for a unit test you want the instance.
|
|
58
|
+
|
|
59
|
+
The cache `attach()` seeds is a process-wide singleton: `CacheManager` is initialized once per test process and reused. Vitest isolates test *files* in separate workers, so caches never leak across files, but tests **within one file** share it. If a test populates the cache and a later test in the same file must not see it, clear it between tests with `resetTestCache()`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { resetTestCache } from "@databricks/appkit/testing";
|
|
63
|
+
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
await resetTestCache(); // no-op if the cache isn't initialized yet
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
It also helps *within* a single test — clear the cache to force a miss, then assert the following call is a hit.
|
|
71
|
+
|
|
72
|
+
### Inspecting what happened[](#inspecting-what-happened "Direct link to Inspecting what happened")
|
|
73
|
+
|
|
74
|
+
The returned object exposes live views you read after the action under test runs:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
await someHandler(req, res);
|
|
78
|
+
|
|
79
|
+
// Every cross-plugin tool dispatch, in order.
|
|
80
|
+
expect(mock.toolCalls[0]).toMatchObject({
|
|
81
|
+
plugin: "analytics",
|
|
82
|
+
tool: "query",
|
|
83
|
+
asUser: true, // proves the on-behalf-of path ran
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Every route the plugin registered (raw handlers, before wrapping).
|
|
87
|
+
expect(mock.routes).toContainEqual(
|
|
88
|
+
expect.objectContaining({ method: "post", path: "/invocations" }),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// The injected telemetry provider records the context's own spans — i.e. the
|
|
92
|
+
// span PluginContext.executeTool opens around each cross-plugin tool call.
|
|
93
|
+
expect(mock.telemetry.getTracer().startActiveSpan).toHaveBeenCalled();
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`mock.telemetry` is injected into the `PluginContext`, so it captures the spans the *context* opens (notably `executeTool`). It is **not** the plugin's own telemetry: `attachContext` rebuilds `this.telemetry` from the real `TelemetryManager`, so spans a plugin opens internally do not land on `mock.telemetry`.
|
|
98
|
+
|
|
99
|
+
`RecordedToolCall.asUser` is the high-value signal for cross-plugin calls: because the fake `asUser` enforces the same token precondition as the real `Plugin.asUser`, a dispatch that records `asUser: true` (with `userId` set) genuinely resolved the caller's user scope, and a request missing `x-forwarded-access-token` **rejects** instead — the OBO distinction that silent `{ executeTool }` stubs cannot verify. Assert both directions: a well-formed request records the expected `userId`, and a token-less one throws.
|
|
100
|
+
|
|
101
|
+
The fake replicates `asUser`'s **token precondition**, not its internal dev-mode telemetry marker: in `NODE_ENV=development` the real `Plugin.asUser` skips impersonation and sets an OTel `isDevOboFallback()` flag, which the fake does not reproduce. Assert OBO through the recorded `asUser`/`userId` fields rather than `isDevOboFallback()`.
|
|
102
|
+
|
|
103
|
+
## `expectStream(...)`[](#expectstream "Direct link to expectstream")
|
|
104
|
+
|
|
105
|
+
AppKit plugins stream Server-Sent Events. `expectStream` consumes a stream and asserts the ordered event types it emits. It accepts an async iterable (an agent adapter's `run()`), a plain array of events, an SSE `Response` (or a promise of one) whose body it parses, or a `createMockResponse()` whose captured writes it replays.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { expectStream } from "@databricks/appkit/testing";
|
|
109
|
+
|
|
110
|
+
// In-order subsequence match — interleaved events (heartbeats, deltas) are ignored.
|
|
111
|
+
await expectStream(agent.adapter.run(input)).toEmit("tool_call", "message_delta");
|
|
112
|
+
|
|
113
|
+
// Exact match — the stream's full shape, in order, with nothing else.
|
|
114
|
+
await expectStream(events).toEmitExactly("warehouse_status", "result");
|
|
115
|
+
|
|
116
|
+
// Or collect without asserting.
|
|
117
|
+
const types = await expectStream(res).collectTypes();
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Asserting a plugin's streaming route[](#asserting-a-plugins-streaming-route "Direct link to Asserting a plugin's streaming route")
|
|
122
|
+
|
|
123
|
+
Most plugins stream SSE from a **route handler** (`res.write(...)`), not a bare generator. `createMockResponse()` captures those writes, and `expectStream` reads them straight back — drive the real handler, then assert:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { createMockRequest, createMockResponse, expectStream } from "@databricks/appkit/testing";
|
|
127
|
+
|
|
128
|
+
const res = createMockResponse();
|
|
129
|
+
await plugin._handleStream(createMockRequest({ obo: true }), res);
|
|
130
|
+
|
|
131
|
+
// The mock captured the SSE the handler wrote; expectStream parses it.
|
|
132
|
+
await expectStream(res).toEmit("status", "result");
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`expectStream(res)` and `expectStream(res.sseResponse())` are equivalent — the latter hands you the raw `Response` if you want it. Do **not** pass the SSE body as a string: a string is an iterable of characters, so `expectStream` rejects it with a pointer to `sseResponse()` rather than emitting one "event" per character.
|
|
137
|
+
|
|
138
|
+
`toEmit` checks that the expected types appear **in order** but tolerates other events before, between, or after them — which is what you want for streams that interleave bookkeeping events like heartbeats or metadata. Use `toEmitExactly` when the stream's shape is fully determined.
|
|
139
|
+
|
|
140
|
+
`expectStream` buffers the whole source before asserting, so a stream that never terminates would otherwise hang until the test runner's own timeout. Pass `{ timeout }` to fail fast with a clear error instead:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await expectStream(handler.stream(req), { timeout: 1000 }).toEmit("result");
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Fixtures[](#fixtures "Direct link to Fixtures")
|
|
148
|
+
|
|
149
|
+
The kit re-exports the request/response/context fixtures AppKit uses internally:
|
|
150
|
+
|
|
151
|
+
* `createMockRequest(overrides?)` / `createMockResponse()` — Express request/response doubles, including the streaming flags (`headersSent`, `writableEnded`). Pass `obo: true` (or `obo: { userId, token, email }`) to set the forwarded identity headers `asUser` requires, instead of hand-adding them. `createMockResponse()` also captures everything a handler writes; pass it to `expectStream` (or call `sseResponse()`) to assert a streaming route's SSE. (Plugins resolve the workspace client through `getWorkspaceClient()`, not the request — use `mockServiceContext` to control it.)
|
|
152
|
+
* `mockServiceContext(options?)` — spy the `ServiceContext` singleton so code that resolves the service principal or a user context gets test doubles. Call in `beforeEach`, and call the returned `restore()` in `afterEach`.
|
|
153
|
+
* `useServiceContextMock(options?)` — the same, in one line: it registers the `beforeEach` install and `afterEach` restore for you. Call it at the top of a `describe` block (not inside a test), and read the live `.current` handle from within a test:
|
|
154
|
+
<!-- -->
|
|
155
|
+
```ts
|
|
156
|
+
describe("my plugin", () => {
|
|
157
|
+
const ctx = useServiceContextMock();
|
|
158
|
+
test("...", async () => {
|
|
159
|
+
await handler(createMockRequest({ obo: true }), res);
|
|
160
|
+
expect(ctx.current.createUserContextSpy).toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
* `createSuccessfulSQLResponse(rows, columns)` / `createFailedSQLResponse(message)` — build SQL Warehouse statement responses.
|
|
166
|
+
* `setupDatabricksEnv(overrides?)` — set `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` to test values.
|
|
167
|
+
* `resetTestCache()` — clear the shared cache singleton between (or within) tests; no-ops if the cache isn't initialized yet.
|
|
168
|
+
|
|
169
|
+
## Full example[](#full-example "Direct link to Full example")
|
|
170
|
+
|
|
171
|
+
Instantiate the plugin **class** directly with `new`. The `analytics()` / `agents()` factory functions you pass to `createApp` return a descriptor for the app to construct — for a unit test you want the instance itself.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { Plugin, type PluginManifest } from "@databricks/appkit";
|
|
175
|
+
import { expectStream, createMockRequest, createTestPluginContext } from "@databricks/appkit/testing";
|
|
176
|
+
import { describe, expect, test } from "vitest";
|
|
177
|
+
|
|
178
|
+
// A small plugin that registers a route and streams two events.
|
|
179
|
+
class GreeterPlugin extends Plugin {
|
|
180
|
+
static manifest = {
|
|
181
|
+
name: "greeter",
|
|
182
|
+
displayName: "Greeter",
|
|
183
|
+
description: "Example plugin",
|
|
184
|
+
resources: { required: [], optional: [] },
|
|
185
|
+
} as PluginManifest<"greeter">;
|
|
186
|
+
|
|
187
|
+
async setup() {
|
|
188
|
+
this.context?.addRoute("get", "/hello", (_req, res) => res.end());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async *greet(name: string) {
|
|
192
|
+
yield { type: "greeting_start", name };
|
|
193
|
+
yield { type: "greeting_end", message: `Hello, ${name}!` };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
describe("greeter plugin", () => {
|
|
198
|
+
test("registers its route through the context", async () => {
|
|
199
|
+
const mock = createTestPluginContext();
|
|
200
|
+
const plugin = new GreeterPlugin({});
|
|
201
|
+
|
|
202
|
+
await mock.attach(plugin);
|
|
203
|
+
await plugin.setup();
|
|
204
|
+
|
|
205
|
+
expect(mock.routes).toContainEqual(
|
|
206
|
+
expect.objectContaining({ method: "get", path: "/hello" }),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("streams events in order", async () => {
|
|
211
|
+
const plugin = new GreeterPlugin({});
|
|
212
|
+
await expectStream(plugin.greet("world")).toEmit(
|
|
213
|
+
"greeting_start",
|
|
214
|
+
"greeting_end",
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
To test a plugin that dispatches cross-plugin tool calls, register fake providers and assert on `mock.toolCalls` — including `asUser`, which confirms the on-behalf-of path ran:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const mock = createTestPluginContext({ analytics: { query: [{ n: 1 }] } });
|
|
225
|
+
const plugin = new MyAgentPlugin({ dir: false });
|
|
226
|
+
await mock.attach(plugin);
|
|
227
|
+
|
|
228
|
+
// `obo` sets the forwarded identity headers `asUser` needs — without them the
|
|
229
|
+
// dispatch would (correctly) reject with "Missing user token".
|
|
230
|
+
const req = createMockRequest({ obo: true });
|
|
231
|
+
await plugin.runSomethingThatCallsAnalytics(req);
|
|
232
|
+
|
|
233
|
+
expect(mock.toolCalls[0]).toMatchObject({
|
|
234
|
+
plugin: "analytics",
|
|
235
|
+
tool: "query",
|
|
236
|
+
asUser: true,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## See also[](#see-also "Direct link to See also")
|
|
242
|
+
|
|
243
|
+
* [Custom plugins](./docs/plugins/custom-plugins.md) — build the plugins you test with this kit.
|
|
244
|
+
* [Execution context](./docs/plugins/execution-context.md) — how `asUser` and the service principal differ at runtime.
|
|
245
|
+
* [Local development](./docs/development/local-development.md) — run your app with hot reload while iterating.
|
package/llms.txt
CHANGED
|
@@ -58,6 +58,7 @@ npx @databricks/appkit docs <query>
|
|
|
58
58
|
- [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
|
|
59
59
|
- [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
|
|
60
60
|
- [Plugin Stability Tiers](./docs/plugins/stability.md): AppKit plugins have a two-tier stability system that communicates API maturity and breaking-change expectations.
|
|
61
|
+
- [Testing](./docs/plugins/testing.md): AppKit ships a testing kit at @databricks/appkit/testing so you can test a plugin — including its cross-plugin tool calls and streaming responses — without a live Databricks workspace, credentials, or network access. That makes plugin tests fast and lets them run in CI, where no workspace is available.
|
|
61
62
|
|
|
62
63
|
## appkit API reference [collapsed]
|
|
63
64
|
|