@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.
Files changed (73) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/cache/index.d.ts +10 -0
  4. package/dist/cache/index.d.ts.map +1 -1
  5. package/dist/cache/index.js +13 -0
  6. package/dist/cache/index.js.map +1 -1
  7. package/dist/cli/commands/codemod/on-plugins-ready.js +2 -1
  8. package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
  9. package/dist/cli/commands/lint.js +2 -1
  10. package/dist/cli/commands/lint.js.map +1 -1
  11. package/dist/cli/commands/plugin/add-resource/add-resource.js +1 -1
  12. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  13. package/dist/cli/commands/plugin/create/create.js +1 -1
  14. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  15. package/dist/cli/commands/plugin/create/prompt-resource.js +1 -1
  16. package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
  17. package/dist/cli/commands/plugin/sync/sync.js +2 -1
  18. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  19. package/dist/cli/commands/registry/env-writer.js +4 -1
  20. package/dist/cli/commands/registry/env-writer.js.map +1 -1
  21. package/dist/connectors/sql-warehouse/client.js +13 -1
  22. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  23. package/dist/core/appkit.d.ts +1 -1
  24. package/dist/core/appkit.d.ts.map +1 -1
  25. package/dist/core/appkit.js +26 -5
  26. package/dist/core/appkit.js.map +1 -1
  27. package/dist/core/lifecycle-manager.js +47 -27
  28. package/dist/core/lifecycle-manager.js.map +1 -1
  29. package/dist/evals/judge.d.ts.map +1 -1
  30. package/dist/evals/judge.js +8 -2
  31. package/dist/evals/judge.js.map +1 -1
  32. package/dist/plugins/agents/mlflow.js +6 -1
  33. package/dist/plugins/agents/mlflow.js.map +1 -1
  34. package/dist/telemetry/telemetry-manager.js +17 -0
  35. package/dist/telemetry/telemetry-manager.js.map +1 -1
  36. package/dist/testing/create-test-app.d.ts +111 -0
  37. package/dist/testing/create-test-app.d.ts.map +1 -0
  38. package/dist/testing/create-test-app.js +187 -0
  39. package/dist/testing/create-test-app.js.map +1 -0
  40. package/dist/testing/create-test-plugin.d.ts +17 -0
  41. package/dist/testing/create-test-plugin.d.ts.map +1 -0
  42. package/dist/testing/create-test-plugin.js +22 -0
  43. package/dist/testing/create-test-plugin.js.map +1 -0
  44. package/dist/testing/fixtures.d.ts +51 -35
  45. package/dist/testing/fixtures.d.ts.map +1 -1
  46. package/dist/testing/fixtures.js +129 -54
  47. package/dist/testing/fixtures.js.map +1 -1
  48. package/dist/testing/index.d.ts +8 -3
  49. package/dist/testing/index.js +7 -2
  50. package/dist/testing/mock-workspace-client.d.ts +47 -0
  51. package/dist/testing/mock-workspace-client.d.ts.map +1 -0
  52. package/dist/testing/mock-workspace-client.js +192 -0
  53. package/dist/testing/mock-workspace-client.js.map +1 -0
  54. package/dist/testing/reset-singletons.js +39 -0
  55. package/dist/testing/reset-singletons.js.map +1 -0
  56. package/dist/testing/test-app.d.ts +54 -0
  57. package/dist/testing/test-app.d.ts.map +1 -0
  58. package/dist/testing/test-app.js +55 -0
  59. package/dist/testing/test-app.js.map +1 -0
  60. package/dist/testing/test-cache.d.ts +60 -0
  61. package/dist/testing/test-cache.d.ts.map +1 -0
  62. package/dist/testing/test-cache.js +65 -0
  63. package/dist/testing/test-cache.js.map +1 -0
  64. package/dist/testing/test-plugin-context.d.ts +43 -2
  65. package/dist/testing/test-plugin-context.d.ts.map +1 -1
  66. package/dist/testing/test-plugin-context.js +47 -2
  67. package/dist/testing/test-plugin-context.js.map +1 -1
  68. package/docs/api/appkit/Function.createApp.md +9 -9
  69. package/docs/plugins/agents.md +3 -1
  70. package/docs/plugins/testing.md +440 -14
  71. package/llms.txt +1 -1
  72. package/package.json +11 -3
  73. package/sbom.cdx.json +1 -1
@@ -0,0 +1,187 @@
1
+ import { ServiceContext } from "../context/service-context.js";
2
+ import { InMemoryStorage } from "../cache/storage/memory.js";
3
+ import { AppKit, disposeApp } from "../core/appkit.js";
4
+ import { createMockWorkspaceClient } from "./mock-workspace-client.js";
5
+ import { fakeUserContext, oboHeaders, setupDatabricksEnv } from "./fixtures.js";
6
+ import { dropCoreSingletons } from "./reset-singletons.js";
7
+ import { vi } from "vitest";
8
+
9
+ //#region src/testing/create-test-app.ts
10
+ /**
11
+ * The env snapshot to restore on close, and whether an app currently holds it.
12
+ *
13
+ * One live app at a time (see the guard in `createTestApp`), so a flag suffices.
14
+ */
15
+ let envBaseline;
16
+ let harnessAppLive = false;
17
+ /** Take the baseline on boot. */
18
+ function acquireEnvBaseline() {
19
+ envBaseline = { ...process.env };
20
+ harnessAppLive = true;
21
+ }
22
+ /** Restore the baseline on close. */
23
+ function releaseEnvBaseline() {
24
+ harnessAppLive = false;
25
+ if (!envBaseline) return;
26
+ const baseline = envBaseline;
27
+ envBaseline = void 0;
28
+ for (const key of Object.keys(process.env)) if (!(key in baseline)) delete process.env[key];
29
+ Object.assign(process.env, baseline);
30
+ }
31
+ /**
32
+ * Point `ServiceContext.createUserContext` at the harness's mock so an `obo`
33
+ * request does not construct a real SDK client from `DATABRICKS_HOST`.
34
+ *
35
+ * Mirrors the `createUserContextSpy` in `fixtures.ts`; returns its restore.
36
+ */
37
+ function stubUserContext(client) {
38
+ const spy = vi.spyOn(ServiceContext, "createUserContext").mockImplementation((token, userId, userName, userEmail) => fakeUserContext(client, ServiceContext.get())(token, userId, userName, userEmail));
39
+ return () => spy.mockRestore();
40
+ }
41
+ /**
42
+ * Wait for a server to finish binding and return the port it landed on.
43
+ *
44
+ * Needed with `port: 0`: `start()` returns once `listen()` is invoked, before
45
+ * the bind completes, so `address()` is null until the `listening` event fires.
46
+ * `createTestApp` does this for you — reach for it when hand-rolling a server.
47
+ */
48
+ async function getListeningPort(server) {
49
+ const addr = server.address();
50
+ if (addr && typeof addr === "object" && typeof addr.port === "number") return addr.port;
51
+ await new Promise((resolve, reject) => {
52
+ server.once("listening", () => resolve());
53
+ server.once("error", (err) => reject(err));
54
+ });
55
+ const ready = server.address();
56
+ if (!ready || typeof ready !== "object") throw new Error("Server is listening but address() returned null");
57
+ return ready.port;
58
+ }
59
+ /**
60
+ * Boot a real app — real Express wiring, routes, and resource validation — with
61
+ * no workspace, credentials, or network. `createTestPluginContext` is cheaper
62
+ * when you only need to unit-test wiring.
63
+ *
64
+ * Does **not** validate config values against `manifest.config.schema`; no
65
+ * runtime validator exists for that.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const app = await createTestApp({ plugins: [myPlugin()] });
70
+ * try {
71
+ * const res = await app.post("/api/my-plugin/thing", { body: { q: 1 }, obo: true });
72
+ * await expectStream(res).toEmit("status", "result");
73
+ * } finally {
74
+ * await app.close();
75
+ * }
76
+ * ```
77
+ */
78
+ async function createTestApp(options = {}) {
79
+ const { plugins = [], responses, strict, client: suppliedClient, env = {}, server: serverOption, nodeEnv = "test", cache } = options;
80
+ if (nodeEnv === "development") throw new Error("createTestApp: nodeEnv \"development\" is not supported. Dev mode routes the harness's ephemeral `port: 0` through get-port, which throws a RangeError, and it also boots a real Vite dev server, downgrades resource validation to a warning, and stops filtering dev-only plugins. Pin a port explicitly with your own server plugin if you need dev behaviour.");
81
+ if (harnessAppLive) throw new Error("createTestApp: a harness app is already open. AppKit's workspace client, cache, and on-behalf-of fake are process-wide, so a second app would not receive its own `client`/`responses`, and closing either would un-fake the other's on-behalf-of path. Close the first app before booting another — `await using`, or try/finally.");
82
+ acquireEnvBaseline();
83
+ let app;
84
+ let restoreUserContext;
85
+ const disposeBooted = (a) => a[disposeApp]();
86
+ try {
87
+ process.env.NODE_ENV = nodeEnv;
88
+ process.env.APPKIT_STRICT_VALIDATION = "true";
89
+ setupDatabricksEnv({
90
+ DATABRICKS_WORKSPACE_ID: "test-workspace-id",
91
+ ...env
92
+ });
93
+ dropCoreSingletons();
94
+ if (suppliedClient && (responses !== void 0 || strict !== void 0)) throw new Error("createTestApp: `responses` and `strict` configure the built-in mock client, so they do nothing when you also pass `client`. Drop them and configure your own client instead.");
95
+ const client = suppliedClient ?? createMockWorkspaceClient({
96
+ responses,
97
+ strict
98
+ });
99
+ restoreUserContext = stubUserContext(client);
100
+ const hasServer = plugins.some((p) => p?.name === "server");
101
+ if (serverOption === false && hasServer) throw new Error("createTestApp: `server: false` conflicts with the server plugin in `plugins`. Drop one — omit `server: false` to use your plugin, or remove the plugin to boot without a socket.");
102
+ const bootPlugins = [...plugins];
103
+ if (serverOption !== false && !hasServer) {
104
+ const { server: serverPlugin } = await import("../plugins/server/index.js");
105
+ bootPlugins.push(serverPlugin({
106
+ port: 0,
107
+ host: "127.0.0.1"
108
+ }));
109
+ }
110
+ app = await AppKit._createApp({
111
+ plugins: bootPlugins,
112
+ client,
113
+ cache: cache ?? { storage: new InMemoryStorage({ enabled: true }) },
114
+ disableInternalTelemetry: true,
115
+ installSignalHandlers: false
116
+ });
117
+ const serverExports = app.server;
118
+ const httpServer = serverOption === false ? void 0 : serverExports?.getServer?.();
119
+ const port = httpServer ? await getListeningPort(httpServer) : void 0;
120
+ const baseUrl = port === void 0 ? void 0 : `http://127.0.0.1:${port}`;
121
+ const bootedApp = app;
122
+ let closed;
123
+ /** Memoized, so repeated calls are safe in nested `finally`s. */
124
+ const close = () => {
125
+ closed ??= (async () => {
126
+ try {
127
+ await disposeBooted(bootedApp);
128
+ } finally {
129
+ dropCoreSingletons();
130
+ restoreUserContext?.();
131
+ releaseEnvBaseline();
132
+ }
133
+ })();
134
+ return closed;
135
+ };
136
+ const request = async (method, path, reqOptions = {}) => {
137
+ if (baseUrl === void 0) throw new Error(`createTestApp: no HTTP server was started (server: false), so ${method} ${path} cannot be issued.`);
138
+ const headers = {};
139
+ if (reqOptions.obo) Object.assign(headers, oboHeaders(reqOptions.obo));
140
+ let body;
141
+ if (reqOptions.body !== void 0) if (typeof reqOptions.body === "string") body = reqOptions.body;
142
+ else {
143
+ body = JSON.stringify(reqOptions.body);
144
+ headers["content-type"] = "application/json";
145
+ }
146
+ for (const [name, value] of Object.entries(reqOptions.headers ?? {})) headers[name.toLowerCase()] = value;
147
+ return fetch(new URL(path, baseUrl), {
148
+ method,
149
+ headers,
150
+ body,
151
+ signal: reqOptions.signal
152
+ });
153
+ };
154
+ return {
155
+ plugins: bootedApp,
156
+ client,
157
+ get baseUrl() {
158
+ if (baseUrl === void 0) throw new Error("createTestApp: no HTTP server was started (server: false).");
159
+ return baseUrl;
160
+ },
161
+ get port() {
162
+ if (port === void 0) throw new Error("createTestApp: no HTTP server was started (server: false).");
163
+ return port;
164
+ },
165
+ server: httpServer,
166
+ close,
167
+ [Symbol.asyncDispose]: close,
168
+ get: (path, o) => request("GET", path, o),
169
+ post: (path, o) => request("POST", path, o),
170
+ put: (path, o) => request("PUT", path, o),
171
+ patch: (path, o) => request("PATCH", path, o),
172
+ delete: (path, o) => request("DELETE", path, o)
173
+ };
174
+ } catch (err) {
175
+ if (app) try {
176
+ await disposeBooted(app);
177
+ } catch {}
178
+ dropCoreSingletons();
179
+ restoreUserContext?.();
180
+ releaseEnvBaseline();
181
+ throw err;
182
+ }
183
+ }
184
+
185
+ //#endregion
186
+ export { createTestApp, getListeningPort };
187
+ //# sourceMappingURL=create-test-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-test-app.js","names":[],"sources":["../../src/testing/create-test-app.ts"],"sourcesContent":["/**\n * Boot a real AppKit app with no workspace, credentials, or network, then call it\n * over real HTTP.\n */\n\nimport type { Server } from \"node:http\";\n\nimport type {\n CacheConfig,\n PluginConstructor,\n PluginData,\n PluginMap,\n} from \"shared\";\nimport { vi } from \"vitest\";\n\nimport { InMemoryStorage } from \"../cache/storage/memory\";\nimport { ServiceContext } from \"../context/service-context\";\nimport { AppKit, disposeApp } from \"../core/appkit\";\nimport type { WorkspaceClient } from \"../workspace-client\";\nimport type { OboOption } from \"./fixtures\";\nimport { fakeUserContext, oboHeaders, setupDatabricksEnv } from \"./fixtures\";\nimport type { CreateMockWorkspaceClientOptions } from \"./mock-workspace-client\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\nimport { dropCoreSingletons } from \"./reset-singletons\";\n\n// Loose shapes are intentional here; `noExplicitAny` is off repo-wide (see\n// .oxlintrc.json), so a local alias keeps the intent readable.\ntype Any = any;\n\n/**\n * The env snapshot to restore on close, and whether an app currently holds it.\n *\n * One live app at a time (see the guard in `createTestApp`), so a flag suffices.\n */\nlet envBaseline: NodeJS.ProcessEnv | undefined;\nlet harnessAppLive = false;\n\n/** Take the baseline on boot. */\nfunction acquireEnvBaseline(): void {\n envBaseline = { ...process.env };\n harnessAppLive = true;\n}\n\n/** Restore the baseline on close. */\nfunction releaseEnvBaseline(): void {\n harnessAppLive = false;\n if (!envBaseline) return;\n\n const baseline = envBaseline;\n envBaseline = undefined;\n for (const key of Object.keys(process.env)) {\n if (!(key in baseline)) delete process.env[key];\n }\n Object.assign(process.env, baseline);\n}\n\n/** Plugin descriptors, exactly as `createApp` takes them. */\ntype Plugins = PluginData<PluginConstructor, unknown, string>[];\n\n/** Options for {@link createTestApp}. */\nexport interface CreateTestAppOptions<T extends Plugins> {\n /** The plugins under test, as `createApp` takes them. */\n plugins?: T;\n\n /** Dotted-path responses for the built-in mock. Refused when `client` is set. */\n responses?: CreateMockWorkspaceClientOptions[\"responses\"];\n\n /**\n * Make the built-in mock throw when a path with no declared response is\n * called, rather than resolving `undefined`. Refused when `client` is set —\n * configure it on your own client instead.\n */\n strict?: CreateMockWorkspaceClientOptions[\"strict\"];\n\n /**\n * Replaces the built-in mock. You then own `currentUser.me()` — boot reads\n * `currentUser.id` and fails without it.\n */\n client?: WorkspaceClient;\n\n /** Extra env for the boot, restored on `close()`; satisfies declared resources. */\n env?: Record<string, string>;\n\n /** No socket; setup, validation, and teardown still run, request methods throw. */\n server?: false;\n\n /**\n * Defaults to `\"test\"`. `\"development\"` is refused — it throws a `RangeError`\n * in `get-port` on `port: 0`, boots Vite, and relaxes validation.\n *\n * Beyond refusing `development`, this decides error-response redaction:\n * `errorHandlerMiddleware` returns the real message unless `NODE_ENV` is\n * `production`, where a 5xx becomes `\"Server error\"`. Pass `\"production\"` to\n * assert what a deployed app actually returns to a client.\n */\n nodeEnv?: string;\n\n /** Defaults to in-memory, which is what keeps boot offline. */\n cache?: CacheConfig;\n}\n\n/** Per-request options for the {@link TestApp} HTTP methods. */\nexport interface TestRequestOptions {\n /** A non-string value is JSON-encoded with `content-type: application/json`. */\n body?: unknown;\n /** Merged last, so they win over anything the harness sets. */\n headers?: Record<string, string>;\n /** Same convention as `createMockRequest({ obo })`. */\n obo?: OboOption;\n /** Forwarded to `fetch`. */\n signal?: AbortSignal;\n}\n\n/** A booted test app. */\nexport interface TestApp<T extends Plugins> {\n /**\n * Plugin exports by manifest name. Nested rather than spread because `get` and\n * `delete` are plausible plugin names and would collide with the request methods.\n */\n plugins: PluginMap<T>;\n /** The same object a handler resolves at runtime. */\n client: WorkspaceClient;\n /** e.g. `http://127.0.0.1:54321`. Throws when `server: false`. */\n baseUrl: string;\n /** The bound ephemeral port. Throws when `server: false`. */\n port: number;\n /** The underlying HTTP server, or `undefined` with `server: false`. */\n server?: Server;\n\n /** Release the app and restore env. Idempotent. */\n close(): Promise<void>;\n [Symbol.asyncDispose](): Promise<void>;\n\n get(path: string, options?: TestRequestOptions): Promise<Response>;\n post(path: string, options?: TestRequestOptions): Promise<Response>;\n put(path: string, options?: TestRequestOptions): Promise<Response>;\n patch(path: string, options?: TestRequestOptions): Promise<Response>;\n delete(path: string, options?: TestRequestOptions): Promise<Response>;\n}\n\n/**\n * Point `ServiceContext.createUserContext` at the harness's mock so an `obo`\n * request does not construct a real SDK client from `DATABRICKS_HOST`.\n *\n * Mirrors the `createUserContextSpy` in `fixtures.ts`; returns its restore.\n */\nfunction stubUserContext(client: WorkspaceClient): () => void {\n const spy = vi\n .spyOn(ServiceContext, \"createUserContext\")\n .mockImplementation((token, userId, userName, userEmail) =>\n fakeUserContext(client, ServiceContext.get())(\n token,\n userId,\n userName,\n userEmail,\n ),\n );\n return () => spy.mockRestore();\n}\n\n/**\n * Wait for a server to finish binding and return the port it landed on.\n *\n * Needed with `port: 0`: `start()` returns once `listen()` is invoked, before\n * the bind completes, so `address()` is null until the `listening` event fires.\n * `createTestApp` does this for you — reach for it when hand-rolling a server.\n */\nexport async function getListeningPort(server: Server): Promise<number> {\n const addr = server.address();\n if (addr && typeof addr === \"object\" && typeof addr.port === \"number\") {\n return addr.port;\n }\n await new Promise<void>((resolve, reject) => {\n server.once(\"listening\", () => resolve());\n server.once(\"error\", (err) => reject(err));\n });\n const ready = server.address();\n if (!ready || typeof ready !== \"object\") {\n throw new Error(\"Server is listening but address() returned null\");\n }\n return ready.port;\n}\n\n/**\n * Boot a real app — real Express wiring, routes, and resource validation — with\n * no workspace, credentials, or network. `createTestPluginContext` is cheaper\n * when you only need to unit-test wiring.\n *\n * Does **not** validate config values against `manifest.config.schema`; no\n * runtime validator exists for that.\n *\n * @example\n * ```ts\n * const app = await createTestApp({ plugins: [myPlugin()] });\n * try {\n * const res = await app.post(\"/api/my-plugin/thing\", { body: { q: 1 }, obo: true });\n * await expectStream(res).toEmit(\"status\", \"result\");\n * } finally {\n * await app.close();\n * }\n * ```\n */\nexport async function createTestApp<T extends Plugins>(\n options: CreateTestAppOptions<T> = {},\n): Promise<TestApp<T>> {\n const {\n plugins = [] as unknown as T,\n responses,\n strict,\n client: suppliedClient,\n env = {},\n server: serverOption,\n nodeEnv = \"test\",\n cache,\n } = options;\n\n if (nodeEnv === \"development\") {\n throw new Error(\n 'createTestApp: nodeEnv \"development\" is not supported. Dev mode routes ' +\n \"the harness's ephemeral `port: 0` through get-port, which throws a \" +\n \"RangeError, and it also boots a real Vite dev server, downgrades \" +\n \"resource validation to a warning, and stops filtering dev-only \" +\n \"plugins. Pin a port explicitly with your own server plugin if you \" +\n \"need dev behaviour.\",\n );\n }\n\n // Refused rather than half-supported: AppKit's workspace client, cache and\n // on-behalf-of fake are process-wide, so a second live app cannot own its\n // own. Checked before any mutation, so a refused boot leaves the live app\n // untouched.\n if (harnessAppLive) {\n throw new Error(\n \"createTestApp: a harness app is already open. AppKit's workspace \" +\n \"client, cache, and on-behalf-of fake are process-wide, so a second \" +\n \"app would not receive its own `client`/`responses`, and closing \" +\n \"either would un-fake the other's on-behalf-of path. Close the first \" +\n \"app before booting another — `await using`, or try/finally.\",\n );\n }\n\n // Wholesale rather than a whitelist: plugins read vars we cannot enumerate.\n acquireEnvBaseline();\n\n let app: Awaited<ReturnType<typeof AppKit._createApp>> | undefined;\n let restoreUserContext: (() => void) | undefined;\n\n // Runs the booted plugins' shutdown() hooks and closes the server it started;\n // the harness owns dropping the singletons and restoring env. The `as Any` is\n // the one escape hatch the symbol-keyed teardown forces on the harness.\n const disposeBooted = (a: unknown): Promise<void> => (a as Any)[disposeApp]();\n\n try {\n process.env.NODE_ENV = nodeEnv;\n\n // Redundant while NODE_ENV is pinned, but keeps the throw-on-missing-resource\n // contract if that pin ever changes. No opt-out: the warning path is\n // dev-only, and dev is refused.\n process.env.APPKIT_STRICT_VALIDATION = \"true\";\n\n // The workspace ID short-circuits getWorkspaceId's SCIM probe, which would\n // otherwise show up as an apiClient.request call.\n setupDatabricksEnv({\n DATABRICKS_WORKSPACE_ID: \"test-workspace-id\",\n ...env,\n });\n\n dropCoreSingletons();\n\n // `responses` only seeds the built-in mock, so alongside a caller-supplied\n // client it would silently do nothing. Refuse instead, matching the\n // `server: false` conflict below.\n if (suppliedClient && (responses !== undefined || strict !== undefined)) {\n throw new Error(\n \"createTestApp: `responses` and `strict` configure the built-in mock \" +\n \"client, so they do nothing when you also pass `client`. Drop them \" +\n \"and configure your own client instead.\",\n );\n }\n\n // Boot runs ServiceContext.createContext for real, which reads\n // currentUser.id — the mock's built-in default is what lets it through.\n const client =\n suppliedClient ?? createMockWorkspaceClient({ responses, strict });\n\n // createApp({ client }) installs only the service-principal client. An `obo`\n // request reaches ServiceContext.createUserContext, which builds a *real*\n // client from process.env.DATABRICKS_HOST — so the user-scoped path is faked\n // here too, or \"no network\" is false the moment a handler calls asUser.\n restoreUserContext = stubUserContext(client);\n\n // createApp never auto-adds a server, so without this there is nothing to\n // fetch. Lazily imported: the plugin runs dotenv.config() at module load, so\n // a static import would mutate a consumer's env on import of this kit.\n const hasServer = plugins.some((p) => p?.name === \"server\");\n if (serverOption === false && hasServer) {\n // The plugin would still bind a socket while the handle denied one existed.\n throw new Error(\n \"createTestApp: `server: false` conflicts with the server plugin in \" +\n \"`plugins`. Drop one — omit `server: false` to use your plugin, or \" +\n \"remove the plugin to boot without a socket.\",\n );\n }\n const bootPlugins = [...plugins] as Plugins;\n if (serverOption !== false && !hasServer) {\n const { server: serverPlugin } = await import(\"../plugins/server\");\n bootPlugins.push(serverPlugin({ port: 0, host: \"127.0.0.1\" }));\n }\n\n // Both extras are required to stay offline: without explicit storage the\n // cache builds its own client and probes Lakebase, and without the opt-out\n // TelemetryReporter fires an apiClient.request on boot.\n app = await AppKit._createApp({\n plugins: bootPlugins as Any,\n client,\n cache: cache ?? {\n storage: new InMemoryStorage({ enabled: true } as Any),\n },\n disableInternalTelemetry: true,\n // The harness boots repeatedly in one process and runs its own teardown,\n // so it must not accumulate SIGTERM/SIGINT handlers across boots.\n installSignalHandlers: false,\n });\n\n const serverExports = (app as Any).server;\n const httpServer: Server | undefined =\n serverOption === false ? undefined : serverExports?.getServer?.();\n const port = httpServer ? await getListeningPort(httpServer) : undefined;\n const baseUrl = port === undefined ? undefined : `http://127.0.0.1:${port}`;\n\n const bootedApp = app;\n let closed: Promise<void> | undefined;\n\n /** Memoized, so repeated calls are safe in nested `finally`s. */\n const close = () => {\n closed ??= (async () => {\n try {\n await disposeBooted(bootedApp);\n } finally {\n dropCoreSingletons();\n restoreUserContext?.();\n releaseEnvBaseline();\n }\n })();\n return closed;\n };\n\n const request = async (\n method: string,\n path: string,\n reqOptions: TestRequestOptions = {},\n ): Promise<Response> => {\n if (baseUrl === undefined) {\n throw new Error(\n \"createTestApp: no HTTP server was started (server: false), so \" +\n `${method} ${path} cannot be issued.`,\n );\n }\n\n const headers: Record<string, string> = {};\n if (reqOptions.obo) {\n Object.assign(headers, oboHeaders(reqOptions.obo));\n }\n\n let body: string | undefined;\n if (reqOptions.body !== undefined) {\n if (typeof reqOptions.body === \"string\") {\n body = reqOptions.body;\n } else {\n body = JSON.stringify(reqOptions.body);\n headers[\"content-type\"] = \"application/json\";\n }\n }\n\n // Caller headers last, so an explicit content-type or identity wins.\n // Lowercased first: `Headers` comma-joins case variants instead of\n // replacing, so a mixed-case override would corrupt the value into\n // \"alice, bob\" rather than win.\n for (const [name, value] of Object.entries(reqOptions.headers ?? {})) {\n headers[name.toLowerCase()] = value;\n }\n\n return fetch(new URL(path, baseUrl), {\n method,\n headers,\n body,\n signal: reqOptions.signal,\n });\n };\n\n return {\n plugins: bootedApp as unknown as PluginMap<T>,\n client,\n get baseUrl() {\n if (baseUrl === undefined) {\n throw new Error(\n \"createTestApp: no HTTP server was started (server: false).\",\n );\n }\n return baseUrl;\n },\n get port() {\n if (port === undefined) {\n throw new Error(\n \"createTestApp: no HTTP server was started (server: false).\",\n );\n }\n return port;\n },\n server: httpServer,\n close,\n [Symbol.asyncDispose]: close,\n get: (path, o) => request(\"GET\", path, o),\n post: (path, o) => request(\"POST\", path, o),\n put: (path, o) => request(\"PUT\", path, o),\n patch: (path, o) => request(\"PATCH\", path, o),\n delete: (path, o) => request(\"DELETE\", path, o),\n };\n } catch (err) {\n // Teardown must run from the failure path too, or the boot leaks env\n // mutations and singletons into every later test in the file.\n if (app) {\n try {\n await disposeBooted(app);\n } catch {\n // The boot error is the interesting one; don't let teardown mask it.\n }\n }\n // dispose() no longer drops the singletons, and a failure before boot may\n // have left a partial init — drop unconditionally either way.\n dropCoreSingletons();\n restoreUserContext?.();\n releaseEnvBaseline();\n throw err;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { PluginConstructor, PluginData } from "../shared/src/plugin.js";
2
+ import "../shared/src/index.js";
3
+
4
+ //#region src/testing/create-test-plugin.d.ts
5
+ /**
6
+ * Instantiate a plugin from its `toPlugin()` factory for use with
7
+ * `createTestPluginContext`.
8
+ *
9
+ * Merge order mirrors `AppKit.createAndRegisterPlugin` — `DEFAULT_CONFIG`, then
10
+ * the factory's config, then the manifest `name` — so the instance matches what
11
+ * production builds. Reaching through the descriptor by hand
12
+ * (`new (genie({}).plugin)({})`) skips both.
13
+ */
14
+ declare function createTestPlugin<TClass extends PluginConstructor, TConfig, TName extends string>(factory: (config?: TConfig) => PluginData<TClass, TConfig, TName>, config?: TConfig): InstanceType<TClass>;
15
+ //#endregion
16
+ export { createTestPlugin };
17
+ //# sourceMappingURL=create-test-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-test-plugin.d.ts","names":[],"sources":["../../src/testing/create-test-plugin.ts"],"mappings":";;;;;;;AAWA;;;;;;iBAAgB,gBAAA,gBACC,iBAAA,gCAAA,CAIf,OAAA,GAAU,MAAA,GAAS,OAAA,KAAY,UAAA,CAAW,MAAA,EAAQ,OAAA,EAAS,KAAA,GAC3D,MAAA,GAAS,OAAA,GACR,YAAA,CAAa,MAAA"}
@@ -0,0 +1,22 @@
1
+ //#region src/testing/create-test-plugin.ts
2
+ /**
3
+ * Instantiate a plugin from its `toPlugin()` factory for use with
4
+ * `createTestPluginContext`.
5
+ *
6
+ * Merge order mirrors `AppKit.createAndRegisterPlugin` — `DEFAULT_CONFIG`, then
7
+ * the factory's config, then the manifest `name` — so the instance matches what
8
+ * production builds. Reaching through the descriptor by hand
9
+ * (`new (genie({}).plugin)({})`) skips both.
10
+ */
11
+ function createTestPlugin(factory, config) {
12
+ const { plugin: PluginClass, config: factoryConfig, name } = factory(config);
13
+ return new PluginClass({
14
+ ...PluginClass.DEFAULT_CONFIG ?? {},
15
+ ...factoryConfig ?? {},
16
+ name
17
+ });
18
+ }
19
+
20
+ //#endregion
21
+ export { createTestPlugin };
22
+ //# sourceMappingURL=create-test-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-test-plugin.js","names":[],"sources":["../../src/testing/create-test-plugin.ts"],"sourcesContent":["import type { PluginConstructor, PluginData } from \"shared\";\n\n/**\n * Instantiate a plugin from its `toPlugin()` factory for use with\n * `createTestPluginContext`.\n *\n * Merge order mirrors `AppKit.createAndRegisterPlugin` — `DEFAULT_CONFIG`, then\n * the factory's config, then the manifest `name` — so the instance matches what\n * production builds. Reaching through the descriptor by hand\n * (`new (genie({}).plugin)({})`) skips both.\n */\nexport function createTestPlugin<\n TClass extends PluginConstructor,\n TConfig,\n TName extends string,\n>(\n factory: (config?: TConfig) => PluginData<TClass, TConfig, TName>,\n config?: TConfig,\n): InstanceType<TClass> {\n const { plugin: PluginClass, config: factoryConfig, name } = factory(config);\n\n return new PluginClass({\n ...(PluginClass.DEFAULT_CONFIG ?? {}),\n ...(factoryConfig ?? {}),\n name,\n }) as InstanceType<TClass>;\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAgB,iBAKd,SACA,QACsB;CACtB,MAAM,EAAE,QAAQ,aAAa,QAAQ,eAAe,SAAS,QAAQ,OAAO;AAE5E,QAAO,IAAI,YAAY;EACrB,GAAI,YAAY,kBAAkB,EAAE;EACpC,GAAI,iBAAiB,EAAE;EACvB;EACD,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import { IAppRouter } from "../shared/src/plugin.js";
2
+ import { ApiError } from "../shared/src/workspace-client/errors.js";
2
3
  import "../shared/src/index.js";
4
+ import "../workspace-client/index.js";
3
5
  import { ServiceContext, ServiceContextState } from "../context/service-context.js";
4
6
  import { ITelemetry } from "../telemetry/types.js";
5
7
  import * as vitest from "vitest";
@@ -93,6 +95,32 @@ declare function createMockResponse(): {
93
95
  * reads `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` finds test values.
94
96
  */
95
97
  declare function setupDatabricksEnv(overrides?: Record<string, string>): void;
98
+ /**
99
+ * Sets environment variables for the duration of `fn`, then restores them to
100
+ * their prior state. Each key's prior value (or "was absent") is captured on
101
+ * entry; on exit, the prior value is restored, or the key is deleted only if
102
+ * it was previously unset.
103
+ *
104
+ * Supports both sync and async `fn`. If `fn` returns a thenable, `withEnv`
105
+ * returns that promise and restores in `.finally()`. Otherwise, it restores
106
+ * in a synchronous `finally` and returns the callback's return value.
107
+ * Restoration runs even if `fn` throws. Nested calls restore in LIFO order.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * // Sync: restores synchronously after fn
112
+ * withEnv({ MY_VAR: "test" }, () => {
113
+ * console.log(process.env.MY_VAR); // "test"
114
+ * });
115
+ * console.log(process.env.MY_VAR); // prior value (or undefined)
116
+ *
117
+ * // Async: restores after promise settles
118
+ * await withEnv({ MY_VAR: "test" }, async () => {
119
+ * await fetch(...);
120
+ * });
121
+ * ```
122
+ */
123
+ declare function withEnv<T>(vars: Record<string, string>, fn: () => T | Promise<T>): T | Promise<T>;
96
124
  /**
97
125
  * Clears AppKit's process-wide cache singleton so cached values don't leak
98
126
  * between tests in the same file.
@@ -130,19 +158,6 @@ interface TestContextOptions {
130
158
  /** Workspace ID */
131
159
  workspaceId?: string;
132
160
  }
133
- /**
134
- * Creates a default mock WorkspaceClient for testing (SQL succeeds, warehouse
135
- * RUNNING).
136
- */
137
- declare function createMockWorkspaceClient(): {
138
- statementExecution: {
139
- executeStatement: vitest.Mock<(...args: any[]) => any>;
140
- };
141
- warehouses: {
142
- get: vitest.Mock<(...args: any[]) => any>;
143
- start: vitest.Mock<(...args: any[]) => any>;
144
- };
145
- };
146
161
  /**
147
162
  * Mocks the `ServiceContext` singleton for testing — spies `get`,
148
163
  * `initialize`, `isInitialized`, and `createUserContext` so code that resolves
@@ -230,28 +245,29 @@ declare function createFailedSQLResponse(errorMessage: string): {
230
245
  statement_id: string;
231
246
  };
232
247
  /**
233
- * A WorkspaceClient whose `executeStatement`/`getStatement` are bare `vi.fn()`s
234
- * (no default resolution) so a test can script exactly what SQL returns.
235
- * `warehouses.get` defaults to RUNNING.
248
+ * Creates a genuine `ApiError` instance for testing error paths. Returns a real
249
+ * instance (where `error instanceof ApiError` holds), suitable for testing
250
+ * `instanceof` checks and `.statusCode` / `.errorCode` / `.message` accessors.
251
+ *
252
+ * @param options Error details: `statusCode`, `message`, and `errorCode`. All required.
253
+ * @returns A genuine `ApiError` instance.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const error = createApiError({
258
+ * statusCode: 404,
259
+ * message: "File not found",
260
+ * errorCode: "NOT_FOUND",
261
+ * });
262
+ * expect(error).toBeInstanceOf(ApiError);
263
+ * expect(error.statusCode).toBe(404);
264
+ * ```
236
265
  */
237
- declare function createConfigurableMockWorkspaceClient(): {
238
- client: {
239
- statementExecution: {
240
- executeStatement: vitest.Mock<(...args: any[]) => any>;
241
- getStatement: vitest.Mock<(...args: any[]) => any>;
242
- };
243
- warehouses: {
244
- get: vitest.Mock<(...args: any[]) => any>;
245
- start: vitest.Mock<(...args: any[]) => any>;
246
- };
247
- };
248
- mocks: {
249
- executeStatement: vitest.Mock<(...args: any[]) => any>;
250
- getStatement: vitest.Mock<(...args: any[]) => any>;
251
- warehousesGet: vitest.Mock<(...args: any[]) => any>;
252
- warehousesStart: vitest.Mock<(...args: any[]) => any>;
253
- };
254
- };
266
+ declare function createApiError(options: {
267
+ statusCode: number;
268
+ message: string;
269
+ errorCode: string;
270
+ }): ApiError;
255
271
  //#endregion
256
- export { OboOption, ServiceContextMock, TestContextOptions, createConfigurableMockWorkspaceClient, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createMockWorkspaceClient, createSuccessfulSQLResponse, mockServiceContext, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock };
272
+ export { OboOption, ServiceContextMock, TestContextOptions, createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createSuccessfulSQLResponse, mockServiceContext, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, withEnv };
257
273
  //# sourceMappingURL=fixtures.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fixtures.d.ts","names":[],"sources":["../../src/testing/fixtures.ts"],"mappings":";;;;;;;KAWK,GAAA;;;AAJkE;;;;iBAYvD,mBAAA,CAAA,GAAuB,UAAA;AAAvC;;;;AAAA,iBAqDgB,gBAAA,CAAA;EACd,MAAA,EAAQ,UAAA;EACR,QAAA,EAAU,MAAA,SAAe,GAAA;EACzB,UAAA,GAAa,MAAA,UAAgB,IAAA,aAAiB,GAAA;AAAA;;;;;;;;KAqCpC,SAAA;EAtCA,sDA0CN,MAAA,WAzCJ;EA2CI,KAAA,WA3CyB;EA6CzB,KAAA;AAAA;;AARN;;;;;;;;;AAqCA;;;;;iBAAgB,iBAAA,CAAkB,SAAA,GAAW,GAAA;;;;;;;;;;;;;;;;;iBA+C7B,kBAAA,CAAA;;UAAkB,MAAA,CAAA,IAAA;;QAWC,MAAA,CAAA,IAAA,QAAH,GAAA;;OAKE,MAAA,CAAA,IAAA,QAAH,GAAA,EAAG,KAAA;SAcE,MAAA,CAAA,IAAA,QAAH,GAAA,EAAG,KAAA;aAOI,MAAA,CAAA,IAAA,QAAH,GAAA;;;MAST,MAAA,CAAA,IAAA,QAFhB,GAAA,EAAG,KAAA,UACI,OAAA,MACA,IAAA,EAAM,GAAA;OAWG,MAAA,CAAA,IAAA,QAFhB,GAAA,EAAG,KAAA,UACI,OAAA,MACA,IAAA,EAAM,GAAA;;;;;;;;iBAgBN,QAAA;AAAA;;;;;iBAWH,kBAAA,CAAmB,SAAA,GAAW,MAAA;;;;;;;;;;;;;;;;;;;;iBAyBxB,cAAA,CAAA,GAAkB,OAAA;;;;UAcvB,kBAAA;;EAEf,uBAAA,GAA0B,GAAA;;EAE1B,oBAAA,GAAuB,GAAA;EAtEN;EAwEjB,MAAA;EAzEiB;EA2EjB,aAAA;;EAEA,WAAA;EA5DyB;EA8DzB,WAAA;AAAA;;;;;iBAOc,yBAAA,CAAA;;sBAAyB,MAAA,CAAA,IAAA;;;;;;;;;;;;;;;iBA2CzB,kBAAA,CAAmB,OAAA,GAAS,kBAAA;;;;;;;;;KA6ChC,kBAAA,GAAqB,UAAA,QAAkB,kBAAA;;;;;;;;;;;;;;AA7CnD;;;;;;;;;;;;;iBAyEgB,qBAAA,CAAsB,OAAA,GAAS,kBAAA;EAAA,SACpC,OAAA,EAAS,kBAAA;AAAA;;;;;iBA+BE,qBAAA,GAAA,CACpB,EAAA,QAAU,CAAA,GAAI,OAAA,CAAQ,CAAA,GACtB,OAAA,GAAU,kBAAA,GACT,OAAA,CAAQ,CAAA;;;;;iBAcK,2BAAA,CACd,IAAA,EAAM,GAAA,MACN,OAAA,EAAS,KAAA;EAAQ,IAAA;EAAc,SAAA;AAAA;;;;;;;;;;;;;;;;;;iBAoBjB,uBAAA,CAAwB,YAAA;;;;;;;;;;;;;;iBAiBxB,qCAAA,CAAA;;;wBAAqC,MAAA,CAAA,IAAA"}
1
+ {"version":3,"file":"fixtures.d.ts","names":[],"sources":["../../src/testing/fixtures.ts"],"mappings":";;;;;;;;;KAgBK,GAAA;;;;AAL0C;;;iBAa/B,mBAAA,CAAA,GAAuB,UAAA;;AAAvC;;;iBAqDgB,gBAAA,CAAA;EACd,MAAA,EAAQ,UAAA;EACR,QAAA,EAAU,MAAA,SAAe,GAAA;EACzB,UAAA,GAAa,MAAA,UAAgB,IAAA,aAAiB,GAAA;AAAA;;;;;;;;KAqCpC,SAAA;EAtCV,sDA0CI,MAAA,WA1CqB;EA4CrB,KAAA,WA3CS;EA6CT,KAAA;AAAA;;;;;;;;;;;;;;;;iBAiFU,iBAAA,CAAkB,SAAA,GAAW,GAAA;;;;;;;;;;;;;;;;;iBA+C7B,kBAAA,CAAA;;UAAkB,MAAA,CAAA,IAAA;;QAWC,MAAA,CAAA,IAAA,QAAH,GAAA;;OAKE,MAAA,CAAA,IAAA,QAAH,GAAA,EAAG,KAAA;SAcE,MAAA,CAAA,IAAA,QAAH,GAAA,EAAG,KAAA;aAOI,MAAA,CAAA,IAAA,QAAH,GAAA;;;MAST,MAAA,CAAA,IAAA,QAFhB,GAAA,EAAG,KAAA,UACI,OAAA,MACA,IAAA,EAAM,GAAA;OAWG,MAAA,CAAA,IAAA,QAFhB,GAAA,EAAG,KAAA,UACI,OAAA,MACA,IAAA,EAAM,GAAA;;;;;;;;iBAgBN,QAAA;AAAA;;;;;iBAWH,kBAAA,CAAmB,SAAA,GAAW,MAAA;;;;;;AAA9C;;;;;AAmEA;;;;;;;;;;;;;;;iBAAgB,OAAA,GAAA,CACd,IAAA,EAAM,MAAA,kBACN,EAAA,QAAU,CAAA,GAAI,OAAA,CAAQ,CAAA,IACrB,CAAA,GAAI,OAAA,CAAQ,CAAA;;;;;;;;;AAmDf;;;;;AAcA;;;;;;iBAdsB,cAAA,CAAA,GAAkB,OAAA;;;;UAcvB,kBAAA;EAYf;EAVA,uBAAA,GAA0B,GAAA;EAUf;EARX,oBAAA,GAAuB,GAAA;EAoCS;EAlChC,MAAA;EAkC0C;EAhC1C,aAAA;;EAEA,WAAA;;EAEA,WAAA;AAAA;;;;;;;;;iBA4Bc,kBAAA,CAAmB,OAAA,GAAS,kBAAA;;;;;;;;;KAwChC,kBAAA,GAAqB,UAAA,QAAkB,kBAAA;;;;;;;;;;;AAAnD;;;;;AA4BA;;;;;;;;;;AAgCA;iBAhCgB,qBAAA,CAAsB,OAAA,GAAS,kBAAA;EAAA,SACpC,OAAA,EAAS,kBAAA;AAAA;;;;;iBA+BE,qBAAA,GAAA,CACpB,EAAA,QAAU,CAAA,GAAI,OAAA,CAAQ,CAAA,GACtB,OAAA,GAAU,kBAAA,GACT,OAAA,CAAQ,CAAA;;;;;iBAcK,2BAAA,CACd,IAAA,EAAM,GAAA,MACN,OAAA,EAAS,KAAA;EAAQ,IAAA;EAAc,SAAA;AAAA;;;;;;;;;;;;;;;;;;iBAoBjB,uBAAA,CAAwB,YAAA;;;;;;;;;;AAAxC;;;;;;;;;;;;AA+BA;;;;;;iBAAgB,cAAA,CAAe,OAAA;EAC7B,UAAA;EACA,OAAA;EACA,SAAA;AAAA,IACE,QAAA"}
@@ -1,5 +1,10 @@
1
+ import { ApiError } from "../shared/src/workspace-client/errors.js";
2
+ import { AuthenticationError } from "../errors/authentication.js";
3
+ import "../errors/index.js";
1
4
  import { ServiceContext } from "../context/service-context.js";
2
5
  import { CacheManager } from "../cache/index.js";
6
+ import { createMockWorkspaceClient } from "./mock-workspace-client.js";
7
+ import { createHash } from "node:crypto";
3
8
  import { afterEach, beforeEach, vi } from "vitest";
4
9
 
5
10
  //#region src/testing/fixtures.ts
@@ -68,7 +73,41 @@ function createMockRouter() {
68
73
  getHandler: (method, path) => handlers[`${method.toUpperCase()}:${path}`]
69
74
  };
70
75
  }
71
- /** Build the forwarded identity headers an `obo` option implies. */
76
+ /**
77
+ * The one fake of `ServiceContext.createUserContext` this kit uses, shared by
78
+ * `mockServiceContext` and `createTestApp`.
79
+ *
80
+ * Shared rather than duplicated because the two used to disagree, and neither
81
+ * matched production: a missing token went unrejected and `tokenFingerprint`
82
+ * was absent, which silently disables Lakebase pool rotation — `pool-manager`
83
+ * treats a missing fingerprint as "not stale", so the drain-and-recreate branch
84
+ * could never run under a fake.
85
+ *
86
+ * @internal
87
+ */
88
+ function fakeUserContext(client, ids) {
89
+ return (token, userId, userName, userEmail) => {
90
+ if (!token) throw AuthenticationError.missingToken("user token");
91
+ return {
92
+ client,
93
+ userId,
94
+ userName,
95
+ userEmail,
96
+ tokenFingerprint: createHash("sha256").update(token).digest("hex").slice(0, 16),
97
+ warehouseId: ids.warehouseId,
98
+ workspaceId: ids.workspaceId,
99
+ isUserContext: true
100
+ };
101
+ };
102
+ }
103
+ /**
104
+ * Build the forwarded identity headers an `obo` option implies.
105
+ *
106
+ * Exported so `createTestApp`'s request methods use the same convention as
107
+ * `createMockRequest` rather than a second one.
108
+ *
109
+ * @internal
110
+ */
72
111
  function oboHeaders(obo) {
73
112
  const opts = obo === true ? {} : obo;
74
113
  const headers = {
@@ -180,6 +219,74 @@ function setupDatabricksEnv(overrides = {}) {
180
219
  Object.assign(process.env, overrides);
181
220
  }
182
221
  /**
222
+ * Set environment variables and return a function that restores each key to its
223
+ * prior state — the prior value, or a delete when the key was previously unset.
224
+ * Shared capture/restore behind {@link withEnv} and the
225
+ * {@link createTestPluginContext} options path; not part of the public surface.
226
+ */
227
+ function applyEnv(vars) {
228
+ const prior = /* @__PURE__ */ new Map();
229
+ for (const key of Object.keys(vars)) prior.set(key, process.env[key]);
230
+ Object.assign(process.env, vars);
231
+ return () => {
232
+ for (const [key, value] of prior) if (value === void 0) delete process.env[key];
233
+ else process.env[key] = value;
234
+ };
235
+ }
236
+ /**
237
+ * Run a restore on an error path, suppressing any failure it throws so it
238
+ * cannot replace the caller's original error.
239
+ */
240
+ function restoreQuietly(restore) {
241
+ try {
242
+ restore();
243
+ } catch (restoreError) {}
244
+ }
245
+ /**
246
+ * Sets environment variables for the duration of `fn`, then restores them to
247
+ * their prior state. Each key's prior value (or "was absent") is captured on
248
+ * entry; on exit, the prior value is restored, or the key is deleted only if
249
+ * it was previously unset.
250
+ *
251
+ * Supports both sync and async `fn`. If `fn` returns a thenable, `withEnv`
252
+ * returns that promise and restores in `.finally()`. Otherwise, it restores
253
+ * in a synchronous `finally` and returns the callback's return value.
254
+ * Restoration runs even if `fn` throws. Nested calls restore in LIFO order.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * // Sync: restores synchronously after fn
259
+ * withEnv({ MY_VAR: "test" }, () => {
260
+ * console.log(process.env.MY_VAR); // "test"
261
+ * });
262
+ * console.log(process.env.MY_VAR); // prior value (or undefined)
263
+ *
264
+ * // Async: restores after promise settles
265
+ * await withEnv({ MY_VAR: "test" }, async () => {
266
+ * await fetch(...);
267
+ * });
268
+ * ```
269
+ */
270
+ function withEnv(vars, fn) {
271
+ const restore = applyEnv(vars);
272
+ let result;
273
+ try {
274
+ result = fn();
275
+ } catch (err) {
276
+ restoreQuietly(restore);
277
+ throw err;
278
+ }
279
+ if (result != null && typeof result.then === "function") return result.then((value) => {
280
+ restore();
281
+ return value;
282
+ }, (err) => {
283
+ restoreQuietly(restore);
284
+ throw err;
285
+ });
286
+ restore();
287
+ return result;
288
+ }
289
+ /**
183
290
  * Clears AppKit's process-wide cache singleton so cached values don't leak
184
291
  * between tests in the same file.
185
292
  *
@@ -208,22 +315,6 @@ async function resetTestCache() {
208
315
  await cache.clear();
209
316
  }
210
317
  /**
211
- * Creates a default mock WorkspaceClient for testing (SQL succeeds, warehouse
212
- * RUNNING).
213
- */
214
- function createMockWorkspaceClient() {
215
- return {
216
- statementExecution: { executeStatement: vi.fn().mockResolvedValue({
217
- status: { state: "SUCCEEDED" },
218
- result: { data: [] }
219
- }) },
220
- warehouses: {
221
- get: vi.fn().mockResolvedValue({ state: "RUNNING" }),
222
- start: vi.fn().mockResolvedValue(void 0)
223
- }
224
- };
225
- }
226
- /**
227
318
  * Builds a {@link ServiceContextState} value for testing without touching the
228
319
  * singleton. Internal building block for {@link mockServiceContext}, which
229
320
  * installs the state as spies — that installer is the public entry point.
@@ -249,16 +340,7 @@ function mockServiceContext(options = {}) {
249
340
  const getSpy = vi.spyOn(ServiceContext, "get").mockReturnValue(serviceContext);
250
341
  const initSpy = vi.spyOn(ServiceContext, "initialize").mockResolvedValue(serviceContext);
251
342
  const isInitializedSpy = vi.spyOn(ServiceContext, "isInitialized").mockReturnValue(true);
252
- const createUserContextSpy = vi.spyOn(ServiceContext, "createUserContext").mockImplementation((_token, userId, userName) => {
253
- return {
254
- client: options.userDatabricksClient || createMockWorkspaceClient(),
255
- userId,
256
- userName,
257
- warehouseId: serviceContext.warehouseId,
258
- workspaceId: serviceContext.workspaceId,
259
- isUserContext: true
260
- };
261
- });
343
+ const createUserContextSpy = vi.spyOn(ServiceContext, "createUserContext").mockImplementation(fakeUserContext(options.userDatabricksClient || createMockWorkspaceClient(), serviceContext));
262
344
  return {
263
345
  serviceContext,
264
346
  getSpy,
@@ -351,35 +433,28 @@ function createFailedSQLResponse(errorMessage) {
351
433
  };
352
434
  }
353
435
  /**
354
- * A WorkspaceClient whose `executeStatement`/`getStatement` are bare `vi.fn()`s
355
- * (no default resolution) so a test can script exactly what SQL returns.
356
- * `warehouses.get` defaults to RUNNING.
436
+ * Creates a genuine `ApiError` instance for testing error paths. Returns a real
437
+ * instance (where `error instanceof ApiError` holds), suitable for testing
438
+ * `instanceof` checks and `.statusCode` / `.errorCode` / `.message` accessors.
439
+ *
440
+ * @param options Error details: `statusCode`, `message`, and `errorCode`. All required.
441
+ * @returns A genuine `ApiError` instance.
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * const error = createApiError({
446
+ * statusCode: 404,
447
+ * message: "File not found",
448
+ * errorCode: "NOT_FOUND",
449
+ * });
450
+ * expect(error).toBeInstanceOf(ApiError);
451
+ * expect(error.statusCode).toBe(404);
452
+ * ```
357
453
  */
358
- function createConfigurableMockWorkspaceClient() {
359
- const executeStatement = vi.fn();
360
- const getStatement = vi.fn();
361
- const warehousesGet = vi.fn().mockResolvedValue({ state: "RUNNING" });
362
- const warehousesStart = vi.fn().mockResolvedValue(void 0);
363
- return {
364
- client: {
365
- statementExecution: {
366
- executeStatement,
367
- getStatement
368
- },
369
- warehouses: {
370
- get: warehousesGet,
371
- start: warehousesStart
372
- }
373
- },
374
- mocks: {
375
- executeStatement,
376
- getStatement,
377
- warehousesGet,
378
- warehousesStart
379
- }
380
- };
454
+ function createApiError(options) {
455
+ return new ApiError(options.message, options.errorCode, options.statusCode, void 0, []);
381
456
  }
382
457
 
383
458
  //#endregion
384
- export { createConfigurableMockWorkspaceClient, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createMockWorkspaceClient, createSuccessfulSQLResponse, mockServiceContext, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock };
459
+ export { applyEnv, createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createSuccessfulSQLResponse, fakeUserContext, mockServiceContext, oboHeaders, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, withEnv };
385
460
  //# sourceMappingURL=fixtures.js.map