@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,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
|
-
*
|
|
234
|
-
* (
|
|
235
|
-
* `
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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,
|
|
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":"
|
|
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"}
|
package/dist/testing/fixtures.js
CHANGED
|
@@ -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
|
-
/**
|
|
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((
|
|
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
|
-
*
|
|
355
|
-
* (
|
|
356
|
-
* `
|
|
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
|
|
359
|
-
|
|
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 {
|
|
459
|
+
export { applyEnv, createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createSuccessfulSQLResponse, fakeUserContext, mockServiceContext, oboHeaders, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, withEnv };
|
|
385
460
|
//# sourceMappingURL=fixtures.js.map
|