@databricks/appkit 0.75.1 → 0.76.1
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 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts +10 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +13 -0
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js +2 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/lint.js +2 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +2 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js +4 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +13 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/core/appkit.d.ts +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +26 -5
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js +47 -27
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/evals/judge.d.ts.map +1 -1
- package/dist/evals/judge.js +8 -2
- package/dist/evals/judge.js.map +1 -1
- package/dist/plugins/agents/mlflow.js +6 -1
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +17 -0
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +111 -0
- package/dist/testing/create-test-app.d.ts.map +1 -0
- package/dist/testing/create-test-app.js +187 -0
- package/dist/testing/create-test-app.js.map +1 -0
- package/dist/testing/create-test-plugin.d.ts +17 -0
- package/dist/testing/create-test-plugin.d.ts.map +1 -0
- package/dist/testing/create-test-plugin.js +22 -0
- package/dist/testing/create-test-plugin.js.map +1 -0
- package/dist/testing/fixtures.d.ts +51 -35
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +129 -54
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/index.d.ts +8 -3
- package/dist/testing/index.js +7 -2
- package/dist/testing/mock-workspace-client.d.ts +47 -0
- package/dist/testing/mock-workspace-client.d.ts.map +1 -0
- package/dist/testing/mock-workspace-client.js +192 -0
- package/dist/testing/mock-workspace-client.js.map +1 -0
- package/dist/testing/reset-singletons.js +39 -0
- package/dist/testing/reset-singletons.js.map +1 -0
- package/dist/testing/test-app.d.ts +54 -0
- package/dist/testing/test-app.d.ts.map +1 -0
- package/dist/testing/test-app.js +55 -0
- package/dist/testing/test-app.js.map +1 -0
- package/dist/testing/test-cache.d.ts +60 -0
- package/dist/testing/test-cache.d.ts.map +1 -0
- package/dist/testing/test-cache.js +65 -0
- package/dist/testing/test-cache.js.map +1 -0
- package/dist/testing/test-plugin-context.d.ts +43 -2
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +47 -2
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/docs/api/appkit/Function.createApp.md +9 -9
- package/docs/plugins/agents.md +3 -1
- package/docs/plugins/testing.md +440 -14
- package/llms.txt +1 -1
- package/package.json +11 -3
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CacheManager } from "../cache/index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/testing/test-cache.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The handle {@link useTestCache} returns: a live accessor for the real,
|
|
6
|
+
* in-memory {@link CacheManager} active in the current test.
|
|
7
|
+
*/
|
|
8
|
+
interface TestCacheHandle {
|
|
9
|
+
/**
|
|
10
|
+
* The real (in-memory) cache for the current test. Each test's `beforeEach`
|
|
11
|
+
* seeds and clears the singleton, so reading this always sees a fresh cache —
|
|
12
|
+
* call `generateKey`, `get`, `has`, or `vi.spyOn(handle.current, "getOrExecute")`
|
|
13
|
+
* to assert real caching behaviour.
|
|
14
|
+
*/
|
|
15
|
+
readonly current: CacheManager;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Stand up AppKit's real in-memory cache for a test file and clear it before
|
|
19
|
+
* each test, so a plugin's real caching path runs under test with no mock of
|
|
20
|
+
* the internal `cache` module.
|
|
21
|
+
*
|
|
22
|
+
* Boots the process-wide {@link CacheManager} singleton backed by
|
|
23
|
+
* {@link InMemoryStorage} (idempotent — an already-initialized singleton is
|
|
24
|
+
* reused and the storage argument ignored), then clears it via
|
|
25
|
+
* {@link resetTestCache} in `beforeEach` so each test starts empty. The
|
|
26
|
+
* singleton is left in place: this clears the cache's contents, never the
|
|
27
|
+
* pointer.
|
|
28
|
+
*
|
|
29
|
+
* Because the cache is booted before the test body runs, a plugin constructed
|
|
30
|
+
* in the test — whose constructor reads `CacheManager.getInstanceSync()` —
|
|
31
|
+
* binds `this.cache` to this real cache. So `getOrExecute` genuinely caches and
|
|
32
|
+
* the key is production's real `generateKey`, not a re-implemented fake.
|
|
33
|
+
*
|
|
34
|
+
* Call it at the top of a `describe` block (or module top-level), NOT inside a
|
|
35
|
+
* test: Vitest's `beforeEach`/`afterEach` only register during collection.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* describe("my plugin caches", () => {
|
|
40
|
+
* const testCache = useTestCache();
|
|
41
|
+
*
|
|
42
|
+
* test("second identical request is a cache hit", async () => {
|
|
43
|
+
* const plugin = new MyPlugin(config);
|
|
44
|
+
* // ...drive the same request twice...
|
|
45
|
+
* expect(downstreamMock).toHaveBeenCalledTimes(1);
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* test("metadata does not change the cache key", () => {
|
|
49
|
+
* const key = testCache.current.generateKey(["query", "SELECT 1"], "svc");
|
|
50
|
+
* expect(key).toBe(testCache.current.generateKey(["query", "SELECT 1"], "svc"));
|
|
51
|
+
* });
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @returns `{ current }` — the active real {@link CacheManager} for the test.
|
|
56
|
+
*/
|
|
57
|
+
declare function useTestCache(): TestCacheHandle;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { TestCacheHandle, useTestCache };
|
|
60
|
+
//# sourceMappingURL=test-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-cache.d.ts","names":[],"sources":["../../src/testing/test-cache.ts"],"mappings":";;;;;AAUA;;UAAiB,eAAA;EAON;;AA2CX;;;;EA3CW,SAAA,OAAA,EAAS,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CJ,YAAA,CAAA,GAAgB,eAAA"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { InMemoryStorage } from "../cache/storage/memory.js";
|
|
2
|
+
import "../cache/storage/index.js";
|
|
3
|
+
import { CacheManager } from "../cache/index.js";
|
|
4
|
+
import { resetTestCache } from "./fixtures.js";
|
|
5
|
+
import { afterEach, beforeEach } from "vitest";
|
|
6
|
+
|
|
7
|
+
//#region src/testing/test-cache.ts
|
|
8
|
+
/**
|
|
9
|
+
* Stand up AppKit's real in-memory cache for a test file and clear it before
|
|
10
|
+
* each test, so a plugin's real caching path runs under test with no mock of
|
|
11
|
+
* the internal `cache` module.
|
|
12
|
+
*
|
|
13
|
+
* Boots the process-wide {@link CacheManager} singleton backed by
|
|
14
|
+
* {@link InMemoryStorage} (idempotent — an already-initialized singleton is
|
|
15
|
+
* reused and the storage argument ignored), then clears it via
|
|
16
|
+
* {@link resetTestCache} in `beforeEach` so each test starts empty. The
|
|
17
|
+
* singleton is left in place: this clears the cache's contents, never the
|
|
18
|
+
* pointer.
|
|
19
|
+
*
|
|
20
|
+
* Because the cache is booted before the test body runs, a plugin constructed
|
|
21
|
+
* in the test — whose constructor reads `CacheManager.getInstanceSync()` —
|
|
22
|
+
* binds `this.cache` to this real cache. So `getOrExecute` genuinely caches and
|
|
23
|
+
* the key is production's real `generateKey`, not a re-implemented fake.
|
|
24
|
+
*
|
|
25
|
+
* Call it at the top of a `describe` block (or module top-level), NOT inside a
|
|
26
|
+
* test: Vitest's `beforeEach`/`afterEach` only register during collection.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* describe("my plugin caches", () => {
|
|
31
|
+
* const testCache = useTestCache();
|
|
32
|
+
*
|
|
33
|
+
* test("second identical request is a cache hit", async () => {
|
|
34
|
+
* const plugin = new MyPlugin(config);
|
|
35
|
+
* // ...drive the same request twice...
|
|
36
|
+
* expect(downstreamMock).toHaveBeenCalledTimes(1);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* test("metadata does not change the cache key", () => {
|
|
40
|
+
* const key = testCache.current.generateKey(["query", "SELECT 1"], "svc");
|
|
41
|
+
* expect(key).toBe(testCache.current.generateKey(["query", "SELECT 1"], "svc"));
|
|
42
|
+
* });
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @returns `{ current }` — the active real {@link CacheManager} for the test.
|
|
47
|
+
*/
|
|
48
|
+
function useTestCache() {
|
|
49
|
+
let cache;
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
cache = await CacheManager.getInstance({ storage: new InMemoryStorage({}) });
|
|
52
|
+
await resetTestCache();
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
cache = void 0;
|
|
56
|
+
});
|
|
57
|
+
return { get current() {
|
|
58
|
+
if (!cache) throw new Error("useTestCache: no active cache. Call useTestCache() at the top of a describe block (not inside a test), and read `.current` from within a test.");
|
|
59
|
+
return cache;
|
|
60
|
+
} };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { useTestCache };
|
|
65
|
+
//# sourceMappingURL=test-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-cache.js","names":[],"sources":["../../src/testing/test-cache.ts"],"sourcesContent":["import { afterEach, beforeEach } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport { InMemoryStorage } from \"../cache/storage\";\nimport { resetTestCache } from \"./fixtures\";\n\n/**\n * The handle {@link useTestCache} returns: a live accessor for the real,\n * in-memory {@link CacheManager} active in the current test.\n */\nexport interface TestCacheHandle {\n /**\n * The real (in-memory) cache for the current test. Each test's `beforeEach`\n * seeds and clears the singleton, so reading this always sees a fresh cache —\n * call `generateKey`, `get`, `has`, or `vi.spyOn(handle.current, \"getOrExecute\")`\n * to assert real caching behaviour.\n */\n readonly current: CacheManager;\n}\n\n/**\n * Stand up AppKit's real in-memory cache for a test file and clear it before\n * each test, so a plugin's real caching path runs under test with no mock of\n * the internal `cache` module.\n *\n * Boots the process-wide {@link CacheManager} singleton backed by\n * {@link InMemoryStorage} (idempotent — an already-initialized singleton is\n * reused and the storage argument ignored), then clears it via\n * {@link resetTestCache} in `beforeEach` so each test starts empty. The\n * singleton is left in place: this clears the cache's contents, never the\n * pointer.\n *\n * Because the cache is booted before the test body runs, a plugin constructed\n * in the test — whose constructor reads `CacheManager.getInstanceSync()` —\n * binds `this.cache` to this real cache. So `getOrExecute` genuinely caches and\n * the key is production's real `generateKey`, not a re-implemented fake.\n *\n * Call it at the top of a `describe` block (or module top-level), NOT inside a\n * test: Vitest's `beforeEach`/`afterEach` only register during collection.\n *\n * @example\n * ```ts\n * describe(\"my plugin caches\", () => {\n * const testCache = useTestCache();\n *\n * test(\"second identical request is a cache hit\", async () => {\n * const plugin = new MyPlugin(config);\n * // ...drive the same request twice...\n * expect(downstreamMock).toHaveBeenCalledTimes(1);\n * });\n *\n * test(\"metadata does not change the cache key\", () => {\n * const key = testCache.current.generateKey([\"query\", \"SELECT 1\"], \"svc\");\n * expect(key).toBe(testCache.current.generateKey([\"query\", \"SELECT 1\"], \"svc\"));\n * });\n * });\n * ```\n *\n * @returns `{ current }` — the active real {@link CacheManager} for the test.\n */\nexport function useTestCache(): TestCacheHandle {\n let cache: CacheManager | undefined;\n\n beforeEach(async () => {\n // Idempotent: reuses an existing singleton (ignoring the storage arg) or\n // stands up a fresh in-memory one. Keeps the singleton either way.\n cache = await CacheManager.getInstance({\n storage: new InMemoryStorage({}),\n });\n // Fresh contents per test — clears storage without dropping the singleton.\n await resetTestCache();\n });\n\n afterEach(() => {\n cache = undefined;\n });\n\n return {\n get current(): CacheManager {\n if (!cache) {\n throw new Error(\n \"useTestCache: no active cache. Call useTestCache() at the top of a \" +\n \"describe block (not inside a test), and read `.current` from \" +\n \"within a test.\",\n );\n }\n return cache;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,eAAgC;CAC9C,IAAI;AAEJ,YAAW,YAAY;AAGrB,UAAQ,MAAM,aAAa,YAAY,EACrC,SAAS,IAAI,gBAAgB,EAAE,CAAC,EACjC,CAAC;AAEF,QAAM,gBAAgB;GACtB;AAEF,iBAAgB;AACd,UAAQ;GACR;AAEF,QAAO,EACL,IAAI,UAAwB;AAC1B,MAAI,CAAC,MACH,OAAM,IAAI,MACR,iJAGD;AAEH,SAAO;IAEV"}
|
|
@@ -35,6 +35,29 @@ type FakeToolResponse = FakeToolValue | ((args: unknown, signal?: AbortSignal) =
|
|
|
35
35
|
* name; each inner key becomes a tool that returns the mapped response.
|
|
36
36
|
*/
|
|
37
37
|
type FakeProviders = Record<string, Record<string, FakeToolResponse>>;
|
|
38
|
+
/**
|
|
39
|
+
* Options for {@link createTestPluginContext} when called with a second parameter.
|
|
40
|
+
* When provided, `createTestPluginContext` installs a service context seeded
|
|
41
|
+
* from a mock workspace client, plus optional environment variables.
|
|
42
|
+
*/
|
|
43
|
+
interface TestPluginContextOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Responses keyed by dotted path (`"jobs.getRun"`) for the mocked workspace
|
|
46
|
+
* client. Passed directly to {@link createMockWorkspaceClient}.
|
|
47
|
+
*/
|
|
48
|
+
responses?: Record<string, unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* Environment variables to set for the test. Captured on entry, restored
|
|
51
|
+
* (or deleted if they were unset) on exit via an `afterEach` hook and/or
|
|
52
|
+
* explicit {@link TestPluginContext.restore}.
|
|
53
|
+
*/
|
|
54
|
+
env?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* If `true`, throw when a workspace client path with no declared response is
|
|
57
|
+
* called, instead of resolving `undefined`. Defaults to `false` (never crash).
|
|
58
|
+
*/
|
|
59
|
+
strict?: boolean;
|
|
60
|
+
}
|
|
38
61
|
/** A single dispatch observed by a fake provider. */
|
|
39
62
|
interface RecordedToolCall {
|
|
40
63
|
/** Registered plugin name (the key in {@link FakeProviders}). */
|
|
@@ -124,6 +147,14 @@ interface TestPluginContext {
|
|
|
124
147
|
* gate on `isReady`. Returns the same plugin for chaining.
|
|
125
148
|
*/
|
|
126
149
|
attach<P extends Plugin>(plugin: P): Promise<P>;
|
|
150
|
+
/**
|
|
151
|
+
* Restore the service context and environment variables to their pre-test state.
|
|
152
|
+
* Called automatically via `afterEach` when options were provided to
|
|
153
|
+
* `createTestPluginContext`. Can also be called explicitly for escape hatches
|
|
154
|
+
* (e.g., cleanup inside a test body). Idempotent — safe to call multiple times.
|
|
155
|
+
* Only present if the context was created with options.
|
|
156
|
+
*/
|
|
157
|
+
restore?: () => void;
|
|
127
158
|
}
|
|
128
159
|
/**
|
|
129
160
|
* Build a real {@link PluginContext} with faked edges for testing — no live
|
|
@@ -143,16 +174,26 @@ interface TestPluginContext {
|
|
|
143
174
|
* Nothing about `PluginContext` is reimplemented.
|
|
144
175
|
*
|
|
145
176
|
* @param fakes - Canned tool responses keyed by plugin then tool name.
|
|
177
|
+
* @param options - When provided, installs a mock workspace client seeded from
|
|
178
|
+
* `responses`, mocks the service context, and sets `env`. Omit it for the
|
|
179
|
+
* original behavior.
|
|
146
180
|
*
|
|
147
181
|
* @example
|
|
148
182
|
* ```ts
|
|
183
|
+
* // No options
|
|
149
184
|
* const mock = createTestPluginContext({ analytics: { query: fixtureRows } });
|
|
150
185
|
* await mock.attach(agentsPlugin);
|
|
151
186
|
* // ...exercise a handler that dispatches analytics.query...
|
|
152
187
|
* expect(mock.toolCalls[0]).toMatchObject({ plugin: "analytics", asUser: true });
|
|
188
|
+
*
|
|
189
|
+
* // With options — installs service context + seeded client
|
|
190
|
+
* const mock = createTestPluginContext(
|
|
191
|
+
* {},
|
|
192
|
+
* { responses: { "jobs.getRun": { state: "DONE" } } },
|
|
193
|
+
* );
|
|
153
194
|
* ```
|
|
154
195
|
*/
|
|
155
|
-
declare function createTestPluginContext(fakes?: FakeProviders): TestPluginContext;
|
|
196
|
+
declare function createTestPluginContext(fakes?: FakeProviders, options?: TestPluginContextOptions): TestPluginContext;
|
|
156
197
|
//#endregion
|
|
157
|
-
export { FakeProvider, FakeProviders, FakeToolResponse, RecordedRoute, RecordedToolCall, TestPluginContext, createTestPluginContext };
|
|
198
|
+
export { FakeProvider, FakeProviders, FakeToolResponse, RecordedRoute, RecordedToolCall, TestPluginContext, TestPluginContextOptions, createTestPluginContext };
|
|
158
199
|
//# sourceMappingURL=test-plugin-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-plugin-context.d.ts","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"test-plugin-context.d.ts","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"mappings":";;;;;;;;;;;;;;;AAc+C;;KAW1C,aAAA,GACD,MAAA;;;AAcJ;;;;;KAAY,gBAAA,GACR,aAAA,KACE,IAAA,WAAe,MAAA,GAAS,WAAA;;;;;AAY9B;;;;;;KAAY,aAAA,GAAgB,MAAA,SAAe,MAAA,SAAe,gBAAA;;;;;;UAOzC,wBAAA;EAAA;;;;EAKf,SAAA,GAAY,MAAA;EAAA;;;;;EAMZ,GAAA,GAAM,MAAA;EASS;;;;EAJf,MAAA;AAAA;;UAIe,gBAAA;EAQN;EANT,MAAA;EA0BA;EAxBA,IAAA;EAwBM;EAtBN,IAAA;EA0B4B;EAxB5B,MAAA,GAAS,WAAA;EAiCuB;;;;;;;;AAIlC;;;;;EAvBE,MAAA;EAyBwB;;;;;EAnBxB,MAAA;AAAA;;UAIe,aAAA;EACf,MAAA;EACA,IAAA;EAyCQ;;;;;;EAlCR,QAAA,EAAU,OAAA,CAAQ,cAAA;AAAA;;UAIH,YAAA;EA6C6B;EA3C5C,cAAA,EAAgB,OAAA,CAAQ,OAAA;EAWnB;EATL,KAAA,EAAO,mBAAA;AAAA;;;;;UAOQ,iBAAA;EAqBJ;EAnBX,GAAA,EAAK,aAAA;EAwBL;;;;;;EAjBA,SAAA,EAAW,UAAA;EAyBM;;;;EApBjB,SAAA,EAAW,gBAAA;EA4BX;;;AAwCF;EA/DE,MAAA,EAAQ,aAAA;;EAER,SAAA,EAAW,GAAA,SAAY,YAAA;EA+Db;;;;EA1DV,gBAAA,CAAiB,IAAA,UAAc,KAAA,EAAO,MAAA,SAAe,gBAAA;EAyDrD;;;;;;;EAjDA,MAAA,WAAiB,MAAA,EAAQ,MAAA,EAAQ,CAAA,GAAI,OAAA,CAAQ,CAAA;;;;;;;;EAQ7C,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCc,uBAAA,CACd,KAAA,GAAO,aAAA,EACP,OAAA,GAAU,wBAAA,GACT,iBAAA"}
|
|
@@ -4,7 +4,9 @@ import { InMemoryStorage } from "../cache/storage/memory.js";
|
|
|
4
4
|
import "../cache/storage/index.js";
|
|
5
5
|
import { CacheManager } from "../cache/index.js";
|
|
6
6
|
import { PluginContext, isToolProvider } from "../core/plugin-context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createMockWorkspaceClient } from "./mock-workspace-client.js";
|
|
8
|
+
import { applyEnv, createMockTelemetry, mockServiceContext } from "./fixtures.js";
|
|
9
|
+
import { afterEach, onTestFinished } from "vitest";
|
|
8
10
|
|
|
9
11
|
//#region src/testing/test-plugin-context.ts
|
|
10
12
|
/**
|
|
@@ -25,16 +27,30 @@ import { createMockTelemetry } from "./fixtures.js";
|
|
|
25
27
|
* Nothing about `PluginContext` is reimplemented.
|
|
26
28
|
*
|
|
27
29
|
* @param fakes - Canned tool responses keyed by plugin then tool name.
|
|
30
|
+
* @param options - When provided, installs a mock workspace client seeded from
|
|
31
|
+
* `responses`, mocks the service context, and sets `env`. Omit it for the
|
|
32
|
+
* original behavior.
|
|
28
33
|
*
|
|
29
34
|
* @example
|
|
30
35
|
* ```ts
|
|
36
|
+
* // No options
|
|
31
37
|
* const mock = createTestPluginContext({ analytics: { query: fixtureRows } });
|
|
32
38
|
* await mock.attach(agentsPlugin);
|
|
33
39
|
* // ...exercise a handler that dispatches analytics.query...
|
|
34
40
|
* expect(mock.toolCalls[0]).toMatchObject({ plugin: "analytics", asUser: true });
|
|
41
|
+
*
|
|
42
|
+
* // With options — installs service context + seeded client
|
|
43
|
+
* const mock = createTestPluginContext(
|
|
44
|
+
* {},
|
|
45
|
+
* { responses: { "jobs.getRun": { state: "DONE" } } },
|
|
46
|
+
* );
|
|
35
47
|
* ```
|
|
36
48
|
*/
|
|
37
|
-
function createTestPluginContext(fakes = {}) {
|
|
49
|
+
function createTestPluginContext(fakes = {}, options) {
|
|
50
|
+
if (!options) return createTestPluginContextSync(fakes);
|
|
51
|
+
return createTestPluginContextWithOptions(fakes, options);
|
|
52
|
+
}
|
|
53
|
+
function createTestPluginContextSync(fakes) {
|
|
38
54
|
const telemetry = createMockTelemetry();
|
|
39
55
|
const ctx = new PluginContext({ telemetry });
|
|
40
56
|
const toolCalls = [];
|
|
@@ -125,6 +141,35 @@ function createTestPluginContext(fakes = {}) {
|
|
|
125
141
|
attach
|
|
126
142
|
};
|
|
127
143
|
}
|
|
144
|
+
function createTestPluginContextWithOptions(fakes, options) {
|
|
145
|
+
const { responses = {}, env: envVars = {}, strict = false } = options;
|
|
146
|
+
const serviceContextMock = mockServiceContext({ serviceDatabricksClient: createMockWorkspaceClient({
|
|
147
|
+
responses,
|
|
148
|
+
strict
|
|
149
|
+
}) });
|
|
150
|
+
const restoreEnv = applyEnv(envVars);
|
|
151
|
+
const base = createTestPluginContextSync(fakes);
|
|
152
|
+
let hasRestored = false;
|
|
153
|
+
const restore = () => {
|
|
154
|
+
if (hasRestored) return;
|
|
155
|
+
hasRestored = true;
|
|
156
|
+
restoreEnv();
|
|
157
|
+
serviceContextMock.restore();
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
onTestFinished(() => {
|
|
161
|
+
restore();
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
restore();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
...base,
|
|
170
|
+
restore
|
|
171
|
+
};
|
|
172
|
+
}
|
|
128
173
|
function cacheReady() {
|
|
129
174
|
try {
|
|
130
175
|
CacheManager.getInstanceSync();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-plugin-context.js","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n AgentToolDefinition,\n BasePlugin,\n IAppRequest,\n ToolProvider,\n} from \"shared\";\n\nimport { CacheManager } from \"../cache\";\nimport { InMemoryStorage } from \"../cache/storage\";\nimport { isToolProvider, PluginContext } from \"../core/plugin-context\";\nimport { AuthenticationError } from \"../errors\";\nimport type { Plugin } from \"../plugin\";\nimport type { ITelemetry } from \"../telemetry\";\nimport { createMockTelemetry } from \"./fixtures\";\n\n/**\n * A concrete (non-function) fake tool response — returned as-is. Covers the\n * JSON-serializable shapes a tool call yields (rows, objects, primitives,\n * nullish). A bare `unknown` is intentionally not used here: unioned with the\n * function form below it would collapse to `unknown` and strip contextual\n * types from the callback's parameters.\n */\ntype FakeToolValue =\n | Record<string, unknown>\n | unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * A canned tool response. Either a static {@link FakeToolValue} returned\n * as-is, or a function of the call arguments (and the abort signal\n * `PluginContext.executeTool` composes) so a fake can assert on inputs or\n * simulate slow/aborting work. Returning a promise is supported (the return\n * type is intentionally `unknown`, which also covers `Promise<...>`).\n */\nexport type FakeToolResponse =\n | FakeToolValue\n | ((args: unknown, signal?: AbortSignal) => unknown);\n\n/**\n * Fake connector responses, keyed by plugin name and then tool name:\n *\n * ```ts\n * createTestPluginContext({ analytics: { query: fixtureRows } });\n * ```\n *\n * Each top-level key registers a fake {@link ToolProvider} under that plugin\n * name; each inner key becomes a tool that returns the mapped response.\n */\nexport type FakeProviders = Record<string, Record<string, FakeToolResponse>>;\n\n/** A single dispatch observed by a fake provider. */\nexport interface RecordedToolCall {\n /** Registered plugin name (the key in {@link FakeProviders}). */\n plugin: string;\n /** Tool name passed to `executeAgentTool`. */\n tool: string;\n /** Arguments the tool received. */\n args: unknown;\n /** The abort signal `executeTool` composed (timeout ∘ caller). */\n signal?: AbortSignal;\n /**\n * Whether the dispatch was resolved through the on-behalf-of (`asUser`)\n * path. `PluginContext.executeTool` always calls `provider.asUser(req)`, so\n * for a tool reached through `executeTool` this is `true` — and, because the\n * fake `asUser` enforces the same token precondition as the real\n * {@link Plugin.asUser}, a request with no `x-forwarded-access-token` makes\n * that call **throw** rather than record `asUser: true`. The meaningful\n * assertions are therefore: a well-formed request records `asUser: true`\n * with {@link userId} set, and a token-less request rejects.\n *\n * The fake replicates the token precondition only, not the real dev-mode\n * OTel `isDevOboFallback()` marker — assert OBO here, not via that flag.\n */\n asUser: boolean;\n /**\n * The user the on-behalf-of scope resolved to (from `x-forwarded-user`), or\n * `undefined` for a service-principal call (`asUser: false`). Lets a test\n * assert the tool ran as the expected end user, not just that OBO was used.\n */\n userId?: string;\n}\n\n/** A single route registered through the context's `addRoute`/`addMiddleware`. */\nexport interface RecordedRoute {\n method: string;\n path: string;\n /**\n * The raw handlers as passed to `addRoute` — before `PluginContext` wraps\n * them with `forwardAsyncErrors`. Recorded here so aliasing assertions\n * (\"both routes mount the same handler\") can compare the original\n * references, which the wrapped express-level handlers no longer share.\n */\n handlers: express.RequestHandler[];\n}\n\n/** A fake tool provider registered on a mock context. */\nexport interface FakeProvider {\n /** Every `asUser(req)` the context resolved for this provider. */\n asUserRequests: express.Request[];\n /** Definitions returned from `getAgentTools()`. */\n tools: AgentToolDefinition[];\n}\n\n/**\n * The result of {@link createTestPluginContext}: the real `PluginContext` plus the\n * seams a test needs to drive and inspect it.\n */\nexport interface TestPluginContext {\n /** The real {@link PluginContext}, constructed with mock telemetry. */\n ctx: PluginContext;\n /**\n * The mock telemetry provider injected into the {@link PluginContext}.\n * Captures the spans the *context* opens (notably `executeTool`) — not the\n * plugin's own spans: `attachContext` rebuilds the plugin's `this.telemetry`\n * from the real `TelemetryManager`, so plugin-internal spans do not land here.\n */\n telemetry: ITelemetry;\n /**\n * Tool dispatches observed across all fake providers, in call order. Live —\n * read it after the action under test runs.\n */\n toolCalls: RecordedToolCall[];\n /**\n * Routes registered through the context, in registration order. Live —\n * populated when the plugin calls `addRoute`/`addMiddleware`.\n */\n routes: RecordedRoute[];\n /** Fake providers by plugin name, for direct assertions. */\n providers: Map<string, FakeProvider>;\n /**\n * Register (or replace) a fake tool provider after construction.\n * Same shape as one {@link FakeProviders} entry.\n */\n registerProvider(name: string, tools: Record<string, FakeToolResponse>): void;\n /**\n * Attach this context to a plugin the production way: seed an in-memory\n * cache (if AppKit hasn't already), then call `plugin.attachContext`, which\n * also rebuilds the plugin's telemetry and flips `isReady` to `true`. Await\n * it before exercising handlers that read `this.context`, `this.cache`, or\n * gate on `isReady`. Returns the same plugin for chaining.\n */\n attach<P extends Plugin>(plugin: P): Promise<P>;\n}\n\n/**\n * Build a real {@link PluginContext} with faked edges for testing — no live\n * workspace, no OpenTelemetry pipeline, no network.\n *\n * The context is the *real* class, so route buffering, the tool registry,\n * timeout composition, and the on-behalf-of (`asUser`) path all run for real.\n * Only three edges are faked, matching the seams the class actually has:\n *\n * - **Telemetry** is a mock provider injected into the context (the one\n * injectable production seam); it records the context's own spans, not the\n * plugin's.\n * - **Tool providers** are fakes registered through the existing public\n * `registerToolProvider`; their `asUser`/`executeAgentTool` are recorded.\n * - **Routes** are captured by wrapping the public `addRoute`/`addMiddleware`.\n *\n * Nothing about `PluginContext` is reimplemented.\n *\n * @param fakes - Canned tool responses keyed by plugin then tool name.\n *\n * @example\n * ```ts\n * const mock = createTestPluginContext({ analytics: { query: fixtureRows } });\n * await mock.attach(agentsPlugin);\n * // ...exercise a handler that dispatches analytics.query...\n * expect(mock.toolCalls[0]).toMatchObject({ plugin: \"analytics\", asUser: true });\n * ```\n */\nexport function createTestPluginContext(\n fakes: FakeProviders = {},\n): TestPluginContext {\n const telemetry = createMockTelemetry();\n const ctx = new PluginContext({ telemetry });\n\n const toolCalls: RecordedToolCall[] = [];\n const routes: RecordedRoute[] = [];\n const providers = new Map<string, FakeProvider>();\n\n // Wrap the public route API so raw (pre-wrap) handlers are inspectable while\n // the real buffering/flush path stays intact.\n const realAddRoute = ctx.addRoute.bind(ctx);\n ctx.addRoute = (\n method: string,\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method, path, handlers });\n realAddRoute(method, path, ...handlers);\n };\n const realAddMiddleware = ctx.addMiddleware.bind(ctx);\n ctx.addMiddleware = (\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method: \"use\", path, handlers });\n realAddMiddleware(path, ...handlers);\n };\n\n function registerProvider(\n name: string,\n tools: Record<string, FakeToolResponse>,\n ): void {\n const record: FakeProvider = {\n asUserRequests: [],\n tools: Object.keys(tools).map((toolName) => ({\n name: toolName,\n description: `Fake tool ${name}.${toolName}`,\n parameters: { type: \"object\" },\n })),\n };\n providers.set(name, record);\n\n const resolve = async (\n toolName: string,\n args: unknown,\n signal: AbortSignal | undefined,\n asUser: boolean,\n userId: string | undefined,\n ): Promise<unknown> => {\n toolCalls.push({\n plugin: name,\n tool: toolName,\n args,\n signal,\n asUser,\n userId,\n });\n // `Object.hasOwn`, not `tools[toolName] === undefined`: a tool named\n // \"constructor\"/\"toString\"/etc. would otherwise resolve to an inherited\n // Object.prototype method and be invoked instead of reported missing.\n if (!Object.hasOwn(tools, toolName)) {\n throw new Error(\n `createTestPluginContext: plugin \"${name}\" has no fake tool \"${toolName}\". ` +\n `Available: ${Object.keys(tools).join(\", \") || \"(none)\"}`,\n );\n }\n const response = tools[toolName];\n return typeof response === \"function\"\n ? await (response as (a: unknown, s?: AbortSignal) => unknown)(\n args,\n signal,\n )\n : response;\n };\n\n const base: ToolProvider = {\n getAgentTools: () => record.tools,\n executeAgentTool: (toolName, args, signal) =>\n resolve(toolName, args, signal, false, undefined),\n };\n\n // Mirror the real `Plugin.asUser` token precondition (plugin.ts) so the\n // recorded `asUser` flag reflects genuine user-scope resolution rather than\n // being unconditionally true: a request with no `x-forwarded-access-token`\n // throws `missingToken` (production behavior), except in development where\n // the real code skips impersonation. This is edge-faking of asUser's\n // *contract*, not a reimplementation of `runInUserContext`/`ServiceContext`.\n //\n // Deliberately NOT reproduced: the real dev-mode path sets an OTel\n // `DEV_OBO_FALLBACK_KEY` marker (read by `isDevOboFallback()`). That key is\n // module-private telemetry plumbing; assert OBO via the recorded\n // `asUser`/`userId` fields, not `isDevOboFallback()`.\n const asUser = (req: IAppRequest): ToolProvider => {\n record.asUserRequests.push(req as express.Request);\n const token = (req as express.Request)\n .header?.(\"x-forwarded-access-token\")\n ?.trim();\n const userId = (req as express.Request)\n .header?.(\"x-forwarded-user\")\n ?.trim();\n const isDev = process.env.NODE_ENV === \"development\";\n\n if (!token && !isDev) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n if (token && !userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n return {\n ...base,\n executeAgentTool: (toolName, args, signal) =>\n resolve(toolName, args, signal, true, userId),\n };\n };\n\n // `registerToolProvider` expects the full ToolProviderPlugin shape\n // (BasePlugin & ToolProvider & { asUser }). executeTool only ever calls\n // `asUser` and `executeAgentTool`; the remaining BasePlugin surface is\n // never touched for a registered provider, so a focused fake plus a cast\n // is sufficient and avoids reimplementing a plugin.\n const provider = {\n name,\n setup: async () => {},\n injectRoutes: () => {},\n getEndpoints: () => ({}),\n ...base,\n asUser,\n } as unknown as BasePlugin &\n ToolProvider & {\n asUser: (req: IAppRequest) => ToolProvider;\n };\n\n ctx.registerToolProvider(name, provider);\n }\n\n for (const [name, tools] of Object.entries(fakes)) {\n registerProvider(name, tools);\n }\n\n async function attach<P extends Plugin>(plugin: P): Promise<P> {\n // Seed a real in-memory cache if AppKit hasn't initialized one. Idempotent:\n // getInstance returns any existing singleton (e.g. one a suite already set\n // up) and ignores the storage argument in that case.\n if (!cacheReady()) {\n await CacheManager.getInstance({ storage: new InMemoryStorage({}) });\n }\n plugin.attachContext({ context: ctx });\n\n // Mirror what AppKit core does after attachContext (core/appkit.ts): put\n // the plugin in the registry so `getPlugins()`/`getPluginNames()`/\n // `hasPlugin()` and any sibling-plugin lookup behave as in production. Only\n // register it as a tool provider when it actually is one AND its name does\n // not collide with an injected fake — the fakes are the authored test\n // doubles and must not be overwritten by the plugin under test.\n ctx.registerPlugin(plugin.name, plugin as unknown as BasePlugin);\n if (isToolProvider(plugin) && !providers.has(plugin.name)) {\n ctx.registerToolProvider(\n plugin.name,\n plugin as unknown as Parameters<typeof ctx.registerToolProvider>[1],\n );\n }\n return plugin;\n }\n\n return {\n ctx,\n telemetry,\n toolCalls,\n routes,\n providers,\n registerProvider,\n attach,\n };\n}\n\nfunction cacheReady(): boolean {\n try {\n CacheManager.getInstanceSync();\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"test-plugin-context.js","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n AgentToolDefinition,\n BasePlugin,\n IAppRequest,\n ToolProvider,\n} from \"shared\";\nimport { afterEach, onTestFinished } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport { InMemoryStorage } from \"../cache/storage\";\nimport { isToolProvider, PluginContext } from \"../core/plugin-context\";\nimport { AuthenticationError } from \"../errors\";\nimport type { Plugin } from \"../plugin\";\nimport type { ITelemetry } from \"../telemetry\";\nimport { applyEnv, createMockTelemetry, mockServiceContext } from \"./fixtures\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\n\n/**\n * A concrete (non-function) fake tool response — returned as-is. Covers the\n * JSON-serializable shapes a tool call yields (rows, objects, primitives,\n * nullish). A bare `unknown` is intentionally not used here: unioned with the\n * function form below it would collapse to `unknown` and strip contextual\n * types from the callback's parameters.\n */\ntype FakeToolValue =\n | Record<string, unknown>\n | unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * A canned tool response. Either a static {@link FakeToolValue} returned\n * as-is, or a function of the call arguments (and the abort signal\n * `PluginContext.executeTool` composes) so a fake can assert on inputs or\n * simulate slow/aborting work. Returning a promise is supported (the return\n * type is intentionally `unknown`, which also covers `Promise<...>`).\n */\nexport type FakeToolResponse =\n | FakeToolValue\n | ((args: unknown, signal?: AbortSignal) => unknown);\n\n/**\n * Fake connector responses, keyed by plugin name and then tool name:\n *\n * ```ts\n * createTestPluginContext({ analytics: { query: fixtureRows } });\n * ```\n *\n * Each top-level key registers a fake {@link ToolProvider} under that plugin\n * name; each inner key becomes a tool that returns the mapped response.\n */\nexport type FakeProviders = Record<string, Record<string, FakeToolResponse>>;\n\n/**\n * Options for {@link createTestPluginContext} when called with a second parameter.\n * When provided, `createTestPluginContext` installs a service context seeded\n * from a mock workspace client, plus optional environment variables.\n */\nexport interface TestPluginContextOptions {\n /**\n * Responses keyed by dotted path (`\"jobs.getRun\"`) for the mocked workspace\n * client. Passed directly to {@link createMockWorkspaceClient}.\n */\n responses?: Record<string, unknown>;\n /**\n * Environment variables to set for the test. Captured on entry, restored\n * (or deleted if they were unset) on exit via an `afterEach` hook and/or\n * explicit {@link TestPluginContext.restore}.\n */\n env?: Record<string, string>;\n /**\n * If `true`, throw when a workspace client path with no declared response is\n * called, instead of resolving `undefined`. Defaults to `false` (never crash).\n */\n strict?: boolean;\n}\n\n/** A single dispatch observed by a fake provider. */\nexport interface RecordedToolCall {\n /** Registered plugin name (the key in {@link FakeProviders}). */\n plugin: string;\n /** Tool name passed to `executeAgentTool`. */\n tool: string;\n /** Arguments the tool received. */\n args: unknown;\n /** The abort signal `executeTool` composed (timeout ∘ caller). */\n signal?: AbortSignal;\n /**\n * Whether the dispatch was resolved through the on-behalf-of (`asUser`)\n * path. `PluginContext.executeTool` always calls `provider.asUser(req)`, so\n * for a tool reached through `executeTool` this is `true` — and, because the\n * fake `asUser` enforces the same token precondition as the real\n * {@link Plugin.asUser}, a request with no `x-forwarded-access-token` makes\n * that call **throw** rather than record `asUser: true`. The meaningful\n * assertions are therefore: a well-formed request records `asUser: true`\n * with {@link userId} set, and a token-less request rejects.\n *\n * The fake replicates the token precondition only, not the real dev-mode\n * OTel `isDevOboFallback()` marker — assert OBO here, not via that flag.\n */\n asUser: boolean;\n /**\n * The user the on-behalf-of scope resolved to (from `x-forwarded-user`), or\n * `undefined` for a service-principal call (`asUser: false`). Lets a test\n * assert the tool ran as the expected end user, not just that OBO was used.\n */\n userId?: string;\n}\n\n/** A single route registered through the context's `addRoute`/`addMiddleware`. */\nexport interface RecordedRoute {\n method: string;\n path: string;\n /**\n * The raw handlers as passed to `addRoute` — before `PluginContext` wraps\n * them with `forwardAsyncErrors`. Recorded here so aliasing assertions\n * (\"both routes mount the same handler\") can compare the original\n * references, which the wrapped express-level handlers no longer share.\n */\n handlers: express.RequestHandler[];\n}\n\n/** A fake tool provider registered on a mock context. */\nexport interface FakeProvider {\n /** Every `asUser(req)` the context resolved for this provider. */\n asUserRequests: express.Request[];\n /** Definitions returned from `getAgentTools()`. */\n tools: AgentToolDefinition[];\n}\n\n/**\n * The result of {@link createTestPluginContext}: the real `PluginContext` plus the\n * seams a test needs to drive and inspect it.\n */\nexport interface TestPluginContext {\n /** The real {@link PluginContext}, constructed with mock telemetry. */\n ctx: PluginContext;\n /**\n * The mock telemetry provider injected into the {@link PluginContext}.\n * Captures the spans the *context* opens (notably `executeTool`) — not the\n * plugin's own spans: `attachContext` rebuilds the plugin's `this.telemetry`\n * from the real `TelemetryManager`, so plugin-internal spans do not land here.\n */\n telemetry: ITelemetry;\n /**\n * Tool dispatches observed across all fake providers, in call order. Live —\n * read it after the action under test runs.\n */\n toolCalls: RecordedToolCall[];\n /**\n * Routes registered through the context, in registration order. Live —\n * populated when the plugin calls `addRoute`/`addMiddleware`.\n */\n routes: RecordedRoute[];\n /** Fake providers by plugin name, for direct assertions. */\n providers: Map<string, FakeProvider>;\n /**\n * Register (or replace) a fake tool provider after construction.\n * Same shape as one {@link FakeProviders} entry.\n */\n registerProvider(name: string, tools: Record<string, FakeToolResponse>): void;\n /**\n * Attach this context to a plugin the production way: seed an in-memory\n * cache (if AppKit hasn't already), then call `plugin.attachContext`, which\n * also rebuilds the plugin's telemetry and flips `isReady` to `true`. Await\n * it before exercising handlers that read `this.context`, `this.cache`, or\n * gate on `isReady`. Returns the same plugin for chaining.\n */\n attach<P extends Plugin>(plugin: P): Promise<P>;\n /**\n * Restore the service context and environment variables to their pre-test state.\n * Called automatically via `afterEach` when options were provided to\n * `createTestPluginContext`. Can also be called explicitly for escape hatches\n * (e.g., cleanup inside a test body). Idempotent — safe to call multiple times.\n * Only present if the context was created with options.\n */\n restore?: () => void;\n}\n\n/**\n * Build a real {@link PluginContext} with faked edges for testing — no live\n * workspace, no OpenTelemetry pipeline, no network.\n *\n * The context is the *real* class, so route buffering, the tool registry,\n * timeout composition, and the on-behalf-of (`asUser`) path all run for real.\n * Only three edges are faked, matching the seams the class actually has:\n *\n * - **Telemetry** is a mock provider injected into the context (the one\n * injectable production seam); it records the context's own spans, not the\n * plugin's.\n * - **Tool providers** are fakes registered through the existing public\n * `registerToolProvider`; their `asUser`/`executeAgentTool` are recorded.\n * - **Routes** are captured by wrapping the public `addRoute`/`addMiddleware`.\n *\n * Nothing about `PluginContext` is reimplemented.\n *\n * @param fakes - Canned tool responses keyed by plugin then tool name.\n * @param options - When provided, installs a mock workspace client seeded from\n * `responses`, mocks the service context, and sets `env`. Omit it for the\n * original behavior.\n *\n * @example\n * ```ts\n * // No options\n * const mock = createTestPluginContext({ analytics: { query: fixtureRows } });\n * await mock.attach(agentsPlugin);\n * // ...exercise a handler that dispatches analytics.query...\n * expect(mock.toolCalls[0]).toMatchObject({ plugin: \"analytics\", asUser: true });\n *\n * // With options — installs service context + seeded client\n * const mock = createTestPluginContext(\n * {},\n * { responses: { \"jobs.getRun\": { state: \"DONE\" } } },\n * );\n * ```\n */\nexport function createTestPluginContext(\n fakes: FakeProviders = {},\n options?: TestPluginContextOptions,\n): TestPluginContext {\n // No options: original behavior.\n if (!options) {\n return createTestPluginContextSync(fakes);\n }\n\n // Options provided: install the seeded client, service context, and scoped env.\n return createTestPluginContextWithOptions(fakes, options);\n}\n\nfunction createTestPluginContextSync(fakes: FakeProviders): TestPluginContext {\n const telemetry = createMockTelemetry();\n const ctx = new PluginContext({ telemetry });\n\n const toolCalls: RecordedToolCall[] = [];\n const routes: RecordedRoute[] = [];\n const providers = new Map<string, FakeProvider>();\n\n // Wrap the public route API so raw (pre-wrap) handlers are inspectable while\n // the real buffering/flush path stays intact.\n const realAddRoute = ctx.addRoute.bind(ctx);\n ctx.addRoute = (\n method: string,\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method, path, handlers });\n realAddRoute(method, path, ...handlers);\n };\n const realAddMiddleware = ctx.addMiddleware.bind(ctx);\n ctx.addMiddleware = (\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method: \"use\", path, handlers });\n realAddMiddleware(path, ...handlers);\n };\n\n function registerProvider(\n name: string,\n tools: Record<string, FakeToolResponse>,\n ): void {\n const record: FakeProvider = {\n asUserRequests: [],\n tools: Object.keys(tools).map((toolName) => ({\n name: toolName,\n description: `Fake tool ${name}.${toolName}`,\n parameters: { type: \"object\" },\n })),\n };\n providers.set(name, record);\n\n const resolve = async (\n toolName: string,\n args: unknown,\n signal: AbortSignal | undefined,\n asUser: boolean,\n userId: string | undefined,\n ): Promise<unknown> => {\n toolCalls.push({\n plugin: name,\n tool: toolName,\n args,\n signal,\n asUser,\n userId,\n });\n // `Object.hasOwn`, not `tools[toolName] === undefined`: a tool named\n // \"constructor\"/\"toString\"/etc. would otherwise resolve to an inherited\n // Object.prototype method and be invoked instead of reported missing.\n if (!Object.hasOwn(tools, toolName)) {\n throw new Error(\n `createTestPluginContext: plugin \"${name}\" has no fake tool \"${toolName}\". ` +\n `Available: ${Object.keys(tools).join(\", \") || \"(none)\"}`,\n );\n }\n const response = tools[toolName];\n return typeof response === \"function\"\n ? await (response as (a: unknown, s?: AbortSignal) => unknown)(\n args,\n signal,\n )\n : response;\n };\n\n const base: ToolProvider = {\n getAgentTools: () => record.tools,\n executeAgentTool: (toolName, args, signal) =>\n resolve(toolName, args, signal, false, undefined),\n };\n\n // Mirror the real `Plugin.asUser` token precondition (plugin.ts) so the\n // recorded `asUser` flag reflects genuine user-scope resolution rather than\n // being unconditionally true: a request with no `x-forwarded-access-token`\n // throws `missingToken` (production behavior), except in development where\n // the real code skips impersonation. This is edge-faking of asUser's\n // *contract*, not a reimplementation of `runInUserContext`/`ServiceContext`.\n //\n // Deliberately NOT reproduced: the real dev-mode path sets an OTel\n // `DEV_OBO_FALLBACK_KEY` marker (read by `isDevOboFallback()`). That key is\n // module-private telemetry plumbing; assert OBO via the recorded\n // `asUser`/`userId` fields, not `isDevOboFallback()`.\n const asUser = (req: IAppRequest): ToolProvider => {\n record.asUserRequests.push(req as express.Request);\n const token = (req as express.Request)\n .header?.(\"x-forwarded-access-token\")\n ?.trim();\n const userId = (req as express.Request)\n .header?.(\"x-forwarded-user\")\n ?.trim();\n const isDev = process.env.NODE_ENV === \"development\";\n\n if (!token && !isDev) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n if (token && !userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n return {\n ...base,\n executeAgentTool: (toolName, args, signal) =>\n resolve(toolName, args, signal, true, userId),\n };\n };\n\n // `registerToolProvider` expects the full ToolProviderPlugin shape\n // (BasePlugin & ToolProvider & { asUser }). executeTool only ever calls\n // `asUser` and `executeAgentTool`; the remaining BasePlugin surface is\n // never touched for a registered provider, so a focused fake plus a cast\n // is sufficient and avoids reimplementing a plugin.\n const provider = {\n name,\n setup: async () => {},\n injectRoutes: () => {},\n getEndpoints: () => ({}),\n ...base,\n asUser,\n } as unknown as BasePlugin &\n ToolProvider & {\n asUser: (req: IAppRequest) => ToolProvider;\n };\n\n ctx.registerToolProvider(name, provider);\n }\n\n for (const [name, tools] of Object.entries(fakes)) {\n registerProvider(name, tools);\n }\n\n async function attach<P extends Plugin>(plugin: P): Promise<P> {\n // Seed a real in-memory cache if AppKit hasn't initialized one. Idempotent:\n // getInstance returns any existing singleton (e.g. one a suite already set\n // up) and ignores the storage argument in that case.\n if (!cacheReady()) {\n await CacheManager.getInstance({ storage: new InMemoryStorage({}) });\n }\n plugin.attachContext({ context: ctx });\n\n // Mirror what AppKit core does after attachContext (core/appkit.ts): put\n // the plugin in the registry so `getPlugins()`/`getPluginNames()`/\n // `hasPlugin()` and any sibling-plugin lookup behave as in production. Only\n // register it as a tool provider when it actually is one AND its name does\n // not collide with an injected fake — the fakes are the authored test\n // doubles and must not be overwritten by the plugin under test.\n ctx.registerPlugin(plugin.name, plugin as unknown as BasePlugin);\n if (isToolProvider(plugin) && !providers.has(plugin.name)) {\n ctx.registerToolProvider(\n plugin.name,\n plugin as unknown as Parameters<typeof ctx.registerToolProvider>[1],\n );\n }\n return plugin;\n }\n\n return {\n ctx,\n telemetry,\n toolCalls,\n routes,\n providers,\n registerProvider,\n attach,\n };\n}\n\nfunction createTestPluginContextWithOptions(\n fakes: FakeProviders,\n options: TestPluginContextOptions,\n): TestPluginContext {\n const { responses = {}, env: envVars = {}, strict = false } = options;\n\n // Build a mock workspace client seeded from responses\n const client = createMockWorkspaceClient({\n responses,\n strict,\n });\n\n // Install the mock service context with the seeded client\n const serviceContextMock = mockServiceContext({\n serviceDatabricksClient: client,\n });\n\n // Set env (captured for restore) via the shared helper.\n const restoreEnv = applyEnv(envVars);\n\n // Create the base context (without options this time, since we're handling everything)\n const base = createTestPluginContextSync(fakes);\n\n // Restore function: restores env and service context (idempotent)\n let hasRestored = false;\n const restore = () => {\n if (hasRestored) return;\n hasRestored = true;\n restoreEnv();\n serviceContextMock.restore();\n };\n\n // Auto-restore after the current test. This helper is documented and used\n // from inside a test body, where a runtime-registered `afterEach` does NOT run\n // for that test (Vitest only collects `afterEach` before the test runs) — the\n // reason the old `afterEach` here silently leaked. `onTestFinished` is the hook\n // built for runtime registration and fires after the creating test. If called\n // at collection scope instead, it throws, so fall back to `afterEach` there.\n try {\n onTestFinished(() => {\n restore();\n });\n } catch {\n afterEach(() => {\n restore();\n });\n }\n\n // Return the context with the restore method\n return {\n ...base,\n restore,\n };\n}\n\nfunction cacheReady(): boolean {\n try {\n CacheManager.getInstanceSync();\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -24,15 +24,15 @@ Initializes telemetry, cache, and service context, then registers plugins in pha
|
|
|
24
24
|
|
|
25
25
|
## Parameters[](#parameters "Direct link to Parameters")
|
|
26
26
|
|
|
27
|
-
| Parameter | Type |
|
|
28
|
-
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
-
| `config` | { `cache?`: [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md); `client?`: [`WorkspaceClient`](./docs/api/appkit/Interface.WorkspaceClient.md); `disableInternalTelemetry?`: `boolean`; `onPluginsReady?`: (`appkit`: `PluginMap`<`T`>) => `void` \| `Promise`<`void`>; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md); } |
|
|
30
|
-
| `config.cache?` | [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md) |
|
|
31
|
-
| `config.client?` | [`WorkspaceClient`](./docs/api/appkit/Interface.WorkspaceClient.md) |
|
|
32
|
-
| `config.disableInternalTelemetry?` | `boolean` |
|
|
33
|
-
| `config.onPluginsReady?` | (`appkit`: `PluginMap`<`T`>) => `void` \| `Promise`<`void`> |
|
|
34
|
-
| `config.plugins?` | `T` |
|
|
35
|
-
| `config.telemetry?` | [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md) |
|
|
27
|
+
| Parameter | Type | Description |
|
|
28
|
+
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
29
|
+
| `config` | { `cache?`: [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md); `client?`: [`WorkspaceClient`](./docs/api/appkit/Interface.WorkspaceClient.md); `disableInternalTelemetry?`: `boolean`; `onPluginsReady?`: (`appkit`: `PluginMap`<`T`>) => `void` \| `Promise`<`void`>; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md); } | - |
|
|
30
|
+
| `config.cache?` | [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md) | - |
|
|
31
|
+
| `config.client?` | [`WorkspaceClient`](./docs/api/appkit/Interface.WorkspaceClient.md) | - |
|
|
32
|
+
| `config.disableInternalTelemetry?` | `boolean` | - |
|
|
33
|
+
| `config.onPluginsReady?` | (`appkit`: `PluginMap`<`T`>) => `void` \| `Promise`<`void`> | Runs after plugin setup but **before** the server starts. |
|
|
34
|
+
| `config.plugins?` | `T` | - |
|
|
35
|
+
| `config.telemetry?` | [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md) | - |
|
|
36
36
|
|
|
37
37
|
## Returns[](#returns "Direct link to Returns")
|
|
38
38
|
|
package/docs/plugins/agents.md
CHANGED
|
@@ -34,6 +34,8 @@ await createApp({
|
|
|
34
34
|
|
|
35
35
|
That alone gives you a live HTTP server with `POST /invocations` (and its alias `POST /responses`) wired to a markdown-driven agent. Use `POST /chat` instead when you want the streaming, HITL-capable surface.
|
|
36
36
|
|
|
37
|
+
> **Optional dependencies.** Two opt-in features load their heavy dependencies lazily, so they are declared as optional peer dependencies and are *not* installed with AppKit by default: agent tracing to an MLflow experiment needs `@mlflow/core` (`npm i @mlflow/core`), and the eval judges below need `autoevals` (`npm i autoevals`). Skip them and agents still run — tracing simply stays off and `t.judge.*` reports the missing package.
|
|
38
|
+
|
|
37
39
|
## Level 1: drop a markdown agent package[](#level-1-drop-a-markdown-agent-package "Direct link to Level 1: drop a markdown agent package")
|
|
38
40
|
|
|
39
41
|
Each agent lives in its own folder under `server/agents/` with entry file `agent.md`. A folder is an agent only if it holds an entry file (`agent.md` or `agent.ts`); a folder without one is skipped, so per-agent asset folders sit beside the entry — notably a `skills/` folder holding [Skills](#skills) (on-demand instruction packs the agent loads by name). A shared `server/agents/skills/` folder holds skills available to any agent.
|
|
@@ -760,7 +762,7 @@ Call `t.skip("reason")` to skip an eval, and read `t.reply`, `t.toolCalls`, and
|
|
|
760
762
|
|
|
761
763
|
### LLM-as-judge[](#llm-as-judge "Direct link to LLM-as-judge")
|
|
762
764
|
|
|
763
|
-
`t.judge.*` scores the last reply with an LLM judge (via `autoevals` pointed at a Databricks serving endpoint). Each judge returns a scored assertion (0..1) that **gates by default** — a miss fails the eval. Chain `.atLeast(n)` to set the pass threshold, or `.soft()` to track it only. Judges require a judge model: pass `--judge-model <endpoint>` (or set `APPKIT_JUDGE_MODEL`) plus Databricks auth; without one, `t.judge.*` throws with a clear message.
|
|
765
|
+
`t.judge.*` scores the last reply with an LLM judge (via `autoevals` pointed at a Databricks serving endpoint). Each judge returns a scored assertion (0..1) that **gates by default** — a miss fails the eval. Chain `.atLeast(n)` to set the pass threshold, or `.soft()` to track it only. Judges require a judge model: pass `--judge-model <endpoint>` (or set `APPKIT_JUDGE_MODEL`) plus Databricks auth; without one, `t.judge.*` throws with a clear message. `autoevals` is an optional peer dependency — install it (`npm i autoevals`) to use the judges.
|
|
764
766
|
|
|
765
767
|
```ts
|
|
766
768
|
async test(t) {
|