@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixtures.js","names":[],"sources":["../../src/testing/fixtures.ts"],"sourcesContent":["import type { Span, SpanOptions } from \"@opentelemetry/api\";\nimport type { IAppRouter } from \"shared\";\nimport { afterEach, beforeEach, vi } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport type { ServiceContextState } from \"../context/service-context\";\nimport { ServiceContext } from \"../context/service-context\";\nimport type { InstrumentConfig, ITelemetry } from \"../telemetry/types\";\n\n// Test fixtures intentionally use loose shapes; `noExplicitAny` is disabled\n// repo-wide (see biome.json), so a local alias keeps the intent readable.\ntype Any = any;\n\n/**\n * Creates a mock telemetry provider for testing. Every span/meter/logger is a\n * `vi.fn()` no-op, so plugins that trace, count, or log run without a live\n * OpenTelemetry pipeline. Passed into {@link createTestPluginContext} as the one\n * injectable production seam.\n */\nexport function createMockTelemetry(): ITelemetry {\n const mockSpan: Span = {\n addLink: vi.fn(),\n addLinks: vi.fn(),\n end: vi.fn(),\n setAttribute: vi.fn(),\n setAttributes: vi.fn(),\n setStatus: vi.fn(),\n recordException: vi.fn(),\n updateName: vi.fn(),\n addEvent: vi.fn(),\n isRecording: vi.fn().mockReturnValue(false),\n spanContext: vi.fn(),\n };\n\n return {\n getTracer: vi.fn().mockReturnValue({\n startActiveSpan: vi.fn().mockImplementation((...args: Any[]) => {\n const fn = args[args.length - 1];\n if (typeof fn === \"function\") {\n return fn(mockSpan);\n }\n return undefined;\n }),\n }),\n getMeter: vi.fn().mockReturnValue({\n createCounter: vi.fn().mockReturnValue({ add: vi.fn() }),\n createHistogram: vi.fn().mockReturnValue({ record: vi.fn() }),\n }),\n getLogger: vi.fn().mockReturnValue({\n emit: vi.fn(),\n }),\n emit: vi.fn(),\n startActiveSpan: vi\n .fn()\n .mockImplementation(\n async (\n _name: string,\n _options: SpanOptions,\n fn: (span: Span) => Promise<Any>,\n _tracerOptions?: InstrumentConfig,\n ) => {\n return await fn(mockSpan);\n },\n ),\n registerInstrumentations: vi.fn(),\n };\n}\n\n/**\n * Creates a mock Express router that captures registered handlers so a test\n * can pull a handler back out by method + path and invoke it directly.\n */\nexport function createMockRouter(): {\n router: IAppRouter;\n handlers: Record<string, Any>;\n getHandler: (method: string, path: string) => Any;\n} {\n const handlers: Record<string, Any> = {};\n\n const mockRouter = {\n get: vi.fn((path: string, handler: Any) => {\n handlers[`GET:${path}`] = handler;\n }),\n post: vi.fn((path: string, handler: Any) => {\n handlers[`POST:${path}`] = handler;\n }),\n put: vi.fn((path: string, handler: Any) => {\n handlers[`PUT:${path}`] = handler;\n }),\n delete: vi.fn((path: string, handler: Any) => {\n handlers[`DELETE:${path}`] = handler;\n }),\n patch: vi.fn((path: string, handler: Any) => {\n handlers[`PATCH:${path}`] = handler;\n }),\n } as unknown as IAppRouter;\n\n return {\n router: mockRouter,\n handlers,\n getHandler: (method: string, path: string) =>\n handlers[`${method.toUpperCase()}:${path}`],\n };\n}\n\n/**\n * On-behalf-of shorthand for {@link createMockRequest}. `true` uses the default\n * test user; an object picks the identity. Sets the forwarded headers the real\n * `Plugin.asUser` reads (`x-forwarded-access-token`, `x-forwarded-user`, and —\n * when given — `x-forwarded-email`), so an OBO test is one flag instead of\n * hand-rolled headers.\n */\nexport type OboOption =\n | boolean\n | {\n /** `x-forwarded-user` — defaults to `\"test-user\"`. */\n userId?: string;\n /** `x-forwarded-access-token` — defaults to `\"test-user-token\"`. */\n token?: string;\n /** `x-forwarded-email` — omitted unless provided. */\n email?: string;\n };\n\n/** Build the forwarded identity headers an `obo` option implies. */\nfunction oboHeaders(obo: Exclude<OboOption, false>): Record<string, string> {\n const opts = obo === true ? {} : obo;\n const headers: Record<string, string> = {\n \"x-forwarded-access-token\": opts.token ?? \"test-user-token\",\n \"x-forwarded-user\": opts.userId ?? \"test-user\",\n };\n if (opts.email) headers[\"x-forwarded-email\"] = opts.email;\n return headers;\n}\n\n/**\n * Creates a mock Express request. Pass `overrides` to set `params`, `query`,\n * `body`, `headers`, etc.\n *\n * For on-behalf-of tests, pass `obo` instead of hand-adding forwarded headers —\n * `createMockRequest({ obo: true })` sets the identity headers the real\n * `asUser` requires. Any explicit `headers` you also pass win over the ones\n * `obo` generates, so you can override a single field.\n *\n * @example\n * ```ts\n * createMockRequest({ obo: true }); // default test user + token\n * createMockRequest({ obo: { userId: \"alice\" } }); // pick the user\n * ```\n */\nexport function createMockRequest(overrides: Any = {}) {\n const { obo, headers: headerOverrides, ...rest } = overrides;\n\n // `obo` seeds the forwarded identity headers; an explicit `headers` override\n // still wins (merged last) so a test can tweak or drop a single field.\n //\n // Keys are lowercased on the way in, the way Node's parser gives them to\n // Express. `header()` looks up lowercased, so storing as-given would file a\n // mixed-case override under a second key and leave the obo one winning.\n const headers: Record<string, unknown> = Object.fromEntries(\n Object.entries({\n ...(obo ? oboHeaders(obo) : {}),\n ...headerOverrides,\n }).map(([name, value]) => [name.toLowerCase(), value]),\n );\n\n const req = {\n params: {},\n query: {},\n body: {},\n header: function (name: string) {\n return this.headers[name.toLowerCase()];\n },\n // `...rest` keeps the original override power over every default above;\n // `headers` is applied last as the one managed field (obo + overrides).\n ...rest,\n headers,\n };\n return req;\n}\n\n/**\n * Creates a mock Express response object. `write`/`send`/`setHeader` flip\n * `headersSent`, `end` flips `writableEnded` and fires any `close` listener —\n * enough for streaming handlers that branch on those flags.\n *\n * Every chunk passed to `write` (and a final chunk to `end`) is captured, so a\n * streaming route's real SSE output can be replayed: pass the response straight\n * to {@link expectStream}, or call `sseResponse()` for a real `Response`.\n *\n * @example Assert what a streaming route emitted\n * ```ts\n * const res = createMockResponse();\n * await plugin._handleStream(req, res);\n * await expectStream(res).toEmit(\"status\", \"result\");\n * ```\n */\nexport function createMockResponse() {\n const eventListeners: Record<string, Array<(...args: Any[]) => void>> = {};\n const chunks: string[] = [];\n\n const res = {\n // Flips to true once headers/body have gone out — mirrors Express so\n // streaming handlers can branch between a JSON error (pre-headers) and\n // aborting the socket (mid-stream).\n headersSent: false,\n status: vi.fn().mockReturnThis(),\n json: vi.fn().mockReturnThis(),\n send: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n sendStatus: vi.fn().mockReturnThis(),\n end: vi.fn(function (this: Any, chunk?: unknown) {\n // Express allows `end(chunk)` and `end(callback)`; capture only a data\n // chunk, never the completion callback.\n if (chunk != null && typeof chunk !== \"function\") {\n chunks.push(String(chunk));\n }\n this.writableEnded = true;\n if (eventListeners.close) {\n for (const handler of eventListeners.close) {\n handler();\n }\n }\n return this;\n }),\n write: vi.fn(function (this: Any, chunk?: unknown) {\n this.headersSent = true;\n if (chunk != null) chunks.push(String(chunk));\n // Return `this` (truthy) rather than a boolean: handlers that gate on\n // backpressure (`if (res.write(buf)) …`) then take the no-wait path.\n return this;\n }),\n setHeader: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n flushHeaders: vi.fn().mockReturnThis(),\n destroy: vi.fn().mockReturnThis(),\n on: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (!eventListeners[event]) {\n eventListeners[event] = [];\n }\n eventListeners[event].push(handler);\n return this;\n }),\n off: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (eventListeners[event]) {\n eventListeners[event] = eventListeners[event].filter(\n (h) => h !== handler,\n );\n }\n return this;\n }),\n writableEnded: false,\n /**\n * The SSE body captured so far, as a real `Response` — the bridge from a\n * `res.write`-based handler into {@link expectStream}. `expectStream`\n * detects this method and calls it for you, so `expectStream(res)` and\n * `expectStream(res.sseResponse())` are equivalent.\n */\n sseResponse(): Response {\n return new Response(chunks.join(\"\"));\n },\n };\n return res;\n}\n\n/**\n * Sets up common environment variables for Databricks testing so code that\n * reads `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` finds test values.\n */\nexport function setupDatabricksEnv(overrides: Record<string, string> = {}) {\n process.env.DATABRICKS_HOST = \"https://test.databricks.com\";\n process.env.DATABRICKS_WAREHOUSE_ID = \"test-warehouse-id\";\n Object.assign(process.env, overrides);\n}\n\n/**\n * Clears AppKit's process-wide cache singleton so cached values don't leak\n * between tests in the same file.\n *\n * The cache `attach()` seeds is shared by every test in a file (Vitest isolates\n * files, not tests within a file). Call this in `beforeEach` when one test's\n * cached value must not be seen by the next, or mid-test to force a cache miss\n * before asserting a subsequent hit.\n *\n * No-ops when the cache has not been initialized yet, so it is safe to call\n * before any `attach()`.\n *\n * @example\n * ```ts\n * beforeEach(async () => {\n * await resetTestCache();\n * });\n * ```\n */\nexport async function resetTestCache(): Promise<void> {\n let cache: ReturnType<typeof CacheManager.getInstanceSync>;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Not initialized yet — nothing to clear.\n return;\n }\n await cache.clear();\n}\n\n/**\n * Context options for running tests with mocked service/user context\n */\nexport interface TestContextOptions {\n /** Mock WorkspaceClient for service principal operations */\n serviceDatabricksClient?: Any;\n /** Mock WorkspaceClient for user operations */\n userDatabricksClient?: Any;\n /** User ID for user context */\n userId?: string;\n /** Service user ID */\n serviceUserId?: string;\n /** Warehouse ID */\n warehouseId?: string;\n /** Workspace ID */\n workspaceId?: string;\n}\n\n/**\n * Creates a default mock WorkspaceClient for testing (SQL succeeds, warehouse\n * RUNNING).\n */\nexport function createMockWorkspaceClient() {\n return {\n statementExecution: {\n executeStatement: vi.fn().mockResolvedValue({\n status: { state: \"SUCCEEDED\" },\n result: { data: [] },\n }),\n },\n // Analytics route now calls `warehouses.get` before issuing SQL to\n // ensure the warehouse is RUNNING. Default to RUNNING so existing\n // tests that only care about SQL behaviour aren't affected.\n warehouses: {\n get: vi.fn().mockResolvedValue({ state: \"RUNNING\" }),\n start: vi.fn().mockResolvedValue(undefined),\n },\n };\n}\n\n/**\n * Builds a {@link ServiceContextState} value for testing without touching the\n * singleton. Internal building block for {@link mockServiceContext}, which\n * installs the state as spies — that installer is the public entry point.\n */\nfunction buildServiceContextState(\n options: TestContextOptions = {},\n): ServiceContextState {\n return {\n client: (options.serviceDatabricksClient ||\n createMockWorkspaceClient()) as Any,\n serviceUserId: options.serviceUserId || \"test-service-user\",\n warehouseId: Promise.resolve(options.warehouseId || \"test-warehouse-id\"),\n workspaceId: Promise.resolve(options.workspaceId || \"test-workspace-id\"),\n };\n}\n\n/**\n * Mocks the `ServiceContext` singleton for testing — spies `get`,\n * `initialize`, `isInitialized`, and `createUserContext` so code that resolves\n * the service principal or an on-behalf-of user context gets test doubles.\n * Call in `beforeEach`; call the returned `restore()` in `afterEach`.\n *\n * @returns The mock context plus the spies and a `restore()` helper.\n */\nexport function mockServiceContext(options: TestContextOptions = {}) {\n const serviceContext = buildServiceContextState(options);\n\n const getSpy = vi\n .spyOn(ServiceContext, \"get\")\n .mockReturnValue(serviceContext);\n\n const initSpy = vi\n .spyOn(ServiceContext, \"initialize\")\n .mockResolvedValue(serviceContext);\n\n const isInitializedSpy = vi\n .spyOn(ServiceContext, \"isInitialized\")\n .mockReturnValue(true);\n\n const createUserContextSpy = vi\n .spyOn(ServiceContext, \"createUserContext\")\n .mockImplementation((_token: string, userId: string, userName?: string) => {\n return {\n client: (options.userDatabricksClient ||\n createMockWorkspaceClient()) as Any,\n userId,\n userName,\n warehouseId: serviceContext.warehouseId,\n workspaceId: serviceContext.workspaceId,\n isUserContext: true,\n };\n });\n\n return {\n serviceContext,\n getSpy,\n initSpy,\n isInitializedSpy,\n createUserContextSpy,\n restore: () => {\n getSpy.mockRestore();\n initSpy.mockRestore();\n isInitializedSpy.mockRestore();\n createUserContextSpy.mockRestore();\n },\n };\n}\n\n/** The handle {@link mockServiceContext} returns (spies + `restore`). */\nexport type ServiceContextMock = ReturnType<typeof mockServiceContext>;\n\n/**\n * Registers a fresh {@link mockServiceContext} before each test and restores it\n * after — the whole `beforeEach`/`afterEach` dance in one line.\n *\n * Call it at the top of a `describe` block (or module top-level), NOT inside a\n * test: Vitest's `beforeEach`/`afterEach` only register during collection, so a\n * call from within a test body registers nothing for that test.\n *\n * Returns a **live** accessor, not the handle: each `beforeEach` builds fresh\n * spies, so reading `.current` inside a test always sees that test's mock. A\n * handle captured once would go stale after the first hook runs.\n *\n * @example\n * ```ts\n * describe(\"my plugin\", () => {\n * const ctx = useServiceContextMock({ warehouseId: \"wh-1\" });\n *\n * test(\"resolves the warehouse\", async () => {\n * await myHandler(req, res);\n * expect(ctx.current.getSpy).toHaveBeenCalled();\n * });\n * });\n * ```\n *\n * @returns `{ current }` — the active {@link ServiceContextMock} for the test.\n */\nexport function useServiceContextMock(options: TestContextOptions = {}): {\n readonly current: ServiceContextMock;\n} {\n let handle: ServiceContextMock | undefined;\n\n beforeEach(() => {\n handle = mockServiceContext(options);\n });\n\n afterEach(() => {\n handle?.restore();\n handle = undefined;\n });\n\n return {\n get current(): ServiceContextMock {\n if (!handle) {\n throw new Error(\n \"useServiceContextMock: no active mock. Call useServiceContextMock() \" +\n \"at the top of a describe block (not inside a test), and read \" +\n \"`.current` from within a test.\",\n );\n }\n return handle;\n },\n };\n}\n\n/**\n * Runs a test function within a mocked service context: installs the mock,\n * runs `fn`, and restores the singleton afterward.\n */\nexport async function runWithRequestContext<T>(\n fn: () => T | Promise<T>,\n context?: TestContextOptions,\n): Promise<T> {\n const mocks = mockServiceContext(context);\n\n try {\n return await fn();\n } finally {\n mocks.restore();\n }\n}\n\n/**\n * Builds a SUCCEEDED SQL statement response with a synthetic statement id,\n * `data_array` rows, and a manifest schema derived from `columns`.\n */\nexport function createSuccessfulSQLResponse(\n data: Any[][],\n columns: Array<{ name: string; type_name?: string }>,\n) {\n return {\n status: { state: \"SUCCEEDED\" },\n statement_id: `stmt-${Date.now()}`,\n result: {\n data_array: data,\n },\n manifest: {\n schema: {\n columns: columns.map((col) => ({\n name: col.name,\n type_name: col.type_name ?? \"STRING\",\n })),\n },\n },\n };\n}\n\n/** Builds a FAILED SQL statement response carrying `errorMessage`. */\nexport function createFailedSQLResponse(errorMessage: string) {\n return {\n status: {\n state: \"FAILED\",\n error: {\n message: errorMessage,\n },\n },\n statement_id: `stmt-${Date.now()}`,\n };\n}\n\n/**\n * A WorkspaceClient whose `executeStatement`/`getStatement` are bare `vi.fn()`s\n * (no default resolution) so a test can script exactly what SQL returns.\n * `warehouses.get` defaults to RUNNING.\n */\nexport function createConfigurableMockWorkspaceClient() {\n const executeStatement = vi.fn();\n const getStatement = vi.fn();\n // Analytics route now calls `warehouses.get` before issuing SQL; default to\n // RUNNING so callers that don't care about warehouse readiness don't have\n // to wire it up.\n const warehousesGet = vi.fn().mockResolvedValue({ state: \"RUNNING\" });\n const warehousesStart = vi.fn().mockResolvedValue(undefined);\n\n const client = {\n statementExecution: {\n executeStatement,\n getStatement,\n },\n warehouses: {\n get: warehousesGet,\n start: warehousesStart,\n },\n };\n\n return {\n client,\n mocks: {\n executeStatement,\n getStatement,\n warehousesGet,\n warehousesStart,\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fixtures.js","names":[],"sources":["../../src/testing/fixtures.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type { Span, SpanOptions } from \"@opentelemetry/api\";\nimport type { IAppRouter } from \"shared\";\nimport { afterEach, beforeEach, vi } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport type { ServiceContextState } from \"../context/service-context\";\nimport { ServiceContext } from \"../context/service-context\";\nimport { AuthenticationError } from \"../errors\";\nimport type { InstrumentConfig, ITelemetry } from \"../telemetry/types\";\nimport { ApiError } from \"../workspace-client\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\n\n// Test fixtures intentionally use loose shapes; `noExplicitAny` is disabled\n// repo-wide (see .oxlintrc.json), so a local alias keeps the intent readable.\ntype Any = any;\n\n/**\n * Creates a mock telemetry provider for testing. Every span/meter/logger is a\n * `vi.fn()` no-op, so plugins that trace, count, or log run without a live\n * OpenTelemetry pipeline. Passed into {@link createTestPluginContext} as the one\n * injectable production seam.\n */\nexport function createMockTelemetry(): ITelemetry {\n const mockSpan: Span = {\n addLink: vi.fn(),\n addLinks: vi.fn(),\n end: vi.fn(),\n setAttribute: vi.fn(),\n setAttributes: vi.fn(),\n setStatus: vi.fn(),\n recordException: vi.fn(),\n updateName: vi.fn(),\n addEvent: vi.fn(),\n isRecording: vi.fn().mockReturnValue(false),\n spanContext: vi.fn(),\n };\n\n return {\n getTracer: vi.fn().mockReturnValue({\n startActiveSpan: vi.fn().mockImplementation((...args: Any[]) => {\n const fn = args[args.length - 1];\n if (typeof fn === \"function\") {\n return fn(mockSpan);\n }\n return undefined;\n }),\n }),\n getMeter: vi.fn().mockReturnValue({\n createCounter: vi.fn().mockReturnValue({ add: vi.fn() }),\n createHistogram: vi.fn().mockReturnValue({ record: vi.fn() }),\n }),\n getLogger: vi.fn().mockReturnValue({\n emit: vi.fn(),\n }),\n emit: vi.fn(),\n startActiveSpan: vi\n .fn()\n .mockImplementation(\n async (\n _name: string,\n _options: SpanOptions,\n fn: (span: Span) => Promise<Any>,\n _tracerOptions?: InstrumentConfig,\n ) => {\n return await fn(mockSpan);\n },\n ),\n registerInstrumentations: vi.fn(),\n };\n}\n\n/**\n * Creates a mock Express router that captures registered handlers so a test\n * can pull a handler back out by method + path and invoke it directly.\n */\nexport function createMockRouter(): {\n router: IAppRouter;\n handlers: Record<string, Any>;\n getHandler: (method: string, path: string) => Any;\n} {\n const handlers: Record<string, Any> = {};\n\n const mockRouter = {\n get: vi.fn((path: string, handler: Any) => {\n handlers[`GET:${path}`] = handler;\n }),\n post: vi.fn((path: string, handler: Any) => {\n handlers[`POST:${path}`] = handler;\n }),\n put: vi.fn((path: string, handler: Any) => {\n handlers[`PUT:${path}`] = handler;\n }),\n delete: vi.fn((path: string, handler: Any) => {\n handlers[`DELETE:${path}`] = handler;\n }),\n patch: vi.fn((path: string, handler: Any) => {\n handlers[`PATCH:${path}`] = handler;\n }),\n } as unknown as IAppRouter;\n\n return {\n router: mockRouter,\n handlers,\n getHandler: (method: string, path: string) =>\n handlers[`${method.toUpperCase()}:${path}`],\n };\n}\n\n/**\n * On-behalf-of shorthand for {@link createMockRequest}. `true` uses the default\n * test user; an object picks the identity. Sets the forwarded headers the real\n * `Plugin.asUser` reads (`x-forwarded-access-token`, `x-forwarded-user`, and —\n * when given — `x-forwarded-email`), so an OBO test is one flag instead of\n * hand-rolled headers.\n */\nexport type OboOption =\n | boolean\n | {\n /** `x-forwarded-user` — defaults to `\"test-user\"`. */\n userId?: string;\n /** `x-forwarded-access-token` — defaults to `\"test-user-token\"`. */\n token?: string;\n /** `x-forwarded-email` — omitted unless provided. */\n email?: string;\n };\n\n/**\n * The one fake of `ServiceContext.createUserContext` this kit uses, shared by\n * `mockServiceContext` and `createTestApp`.\n *\n * Shared rather than duplicated because the two used to disagree, and neither\n * matched production: a missing token went unrejected and `tokenFingerprint`\n * was absent, which silently disables Lakebase pool rotation — `pool-manager`\n * treats a missing fingerprint as \"not stale\", so the drain-and-recreate branch\n * could never run under a fake.\n *\n * @internal\n */\nexport function fakeUserContext(\n client: Any,\n ids: { warehouseId?: Any; workspaceId: Any },\n) {\n return (\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): Any => {\n // Same rejection as production, so a path that forgets to forward the token\n // fails here instead of only in a deployed app.\n if (!token) throw AuthenticationError.missingToken(\"user token\");\n return {\n client,\n userId,\n userName,\n userEmail,\n // Derived from the token exactly as production does. Keyed on the user it\n // would be constant across tokens, and rotation compares this value.\n tokenFingerprint: createHash(\"sha256\")\n .update(token)\n .digest(\"hex\")\n .slice(0, 16),\n warehouseId: ids.warehouseId,\n workspaceId: ids.workspaceId,\n isUserContext: true,\n };\n };\n}\n\n/**\n * Build the forwarded identity headers an `obo` option implies.\n *\n * Exported so `createTestApp`'s request methods use the same convention as\n * `createMockRequest` rather than a second one.\n *\n * @internal\n */\nexport function oboHeaders(\n obo: Exclude<OboOption, false>,\n): Record<string, string> {\n const opts = obo === true ? {} : obo;\n const headers: Record<string, string> = {\n \"x-forwarded-access-token\": opts.token ?? \"test-user-token\",\n \"x-forwarded-user\": opts.userId ?? \"test-user\",\n };\n if (opts.email) headers[\"x-forwarded-email\"] = opts.email;\n return headers;\n}\n\n/**\n * Creates a mock Express request. Pass `overrides` to set `params`, `query`,\n * `body`, `headers`, etc.\n *\n * For on-behalf-of tests, pass `obo` instead of hand-adding forwarded headers —\n * `createMockRequest({ obo: true })` sets the identity headers the real\n * `asUser` requires. Any explicit `headers` you also pass win over the ones\n * `obo` generates, so you can override a single field.\n *\n * @example\n * ```ts\n * createMockRequest({ obo: true }); // default test user + token\n * createMockRequest({ obo: { userId: \"alice\" } }); // pick the user\n * ```\n */\nexport function createMockRequest(overrides: Any = {}) {\n const { obo, headers: headerOverrides, ...rest } = overrides;\n\n // `obo` seeds the forwarded identity headers; an explicit `headers` override\n // still wins (merged last) so a test can tweak or drop a single field.\n //\n // Keys are lowercased on the way in, the way Node's parser gives them to\n // Express. `header()` looks up lowercased, so storing as-given would file a\n // mixed-case override under a second key and leave the obo one winning.\n const headers: Record<string, unknown> = Object.fromEntries(\n Object.entries({\n ...(obo ? oboHeaders(obo) : {}),\n ...headerOverrides,\n }).map(([name, value]) => [name.toLowerCase(), value]),\n );\n\n const req = {\n params: {},\n query: {},\n body: {},\n header: function (name: string) {\n return this.headers[name.toLowerCase()];\n },\n // `...rest` keeps the original override power over every default above;\n // `headers` is applied last as the one managed field (obo + overrides).\n ...rest,\n headers,\n };\n return req;\n}\n\n/**\n * Creates a mock Express response object. `write`/`send`/`setHeader` flip\n * `headersSent`, `end` flips `writableEnded` and fires any `close` listener —\n * enough for streaming handlers that branch on those flags.\n *\n * Every chunk passed to `write` (and a final chunk to `end`) is captured, so a\n * streaming route's real SSE output can be replayed: pass the response straight\n * to {@link expectStream}, or call `sseResponse()` for a real `Response`.\n *\n * @example Assert what a streaming route emitted\n * ```ts\n * const res = createMockResponse();\n * await plugin._handleStream(req, res);\n * await expectStream(res).toEmit(\"status\", \"result\");\n * ```\n */\nexport function createMockResponse() {\n const eventListeners: Record<string, Array<(...args: Any[]) => void>> = {};\n const chunks: string[] = [];\n\n const res = {\n // Flips to true once headers/body have gone out — mirrors Express so\n // streaming handlers can branch between a JSON error (pre-headers) and\n // aborting the socket (mid-stream).\n headersSent: false,\n status: vi.fn().mockReturnThis(),\n json: vi.fn().mockReturnThis(),\n send: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n sendStatus: vi.fn().mockReturnThis(),\n end: vi.fn(function (this: Any, chunk?: unknown) {\n // Express allows `end(chunk)` and `end(callback)`; capture only a data\n // chunk, never the completion callback.\n if (chunk != null && typeof chunk !== \"function\") {\n chunks.push(String(chunk));\n }\n this.writableEnded = true;\n if (eventListeners.close) {\n for (const handler of eventListeners.close) {\n handler();\n }\n }\n return this;\n }),\n write: vi.fn(function (this: Any, chunk?: unknown) {\n this.headersSent = true;\n if (chunk != null) chunks.push(String(chunk));\n // Return `this` (truthy) rather than a boolean: handlers that gate on\n // backpressure (`if (res.write(buf)) …`) then take the no-wait path.\n return this;\n }),\n setHeader: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n flushHeaders: vi.fn().mockReturnThis(),\n destroy: vi.fn().mockReturnThis(),\n on: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (!eventListeners[event]) {\n eventListeners[event] = [];\n }\n eventListeners[event].push(handler);\n return this;\n }),\n off: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (eventListeners[event]) {\n eventListeners[event] = eventListeners[event].filter(\n (h) => h !== handler,\n );\n }\n return this;\n }),\n writableEnded: false,\n /**\n * The SSE body captured so far, as a real `Response` — the bridge from a\n * `res.write`-based handler into {@link expectStream}. `expectStream`\n * detects this method and calls it for you, so `expectStream(res)` and\n * `expectStream(res.sseResponse())` are equivalent.\n */\n sseResponse(): Response {\n return new Response(chunks.join(\"\"));\n },\n };\n return res;\n}\n\n/**\n * Sets up common environment variables for Databricks testing so code that\n * reads `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` finds test values.\n */\nexport function setupDatabricksEnv(overrides: Record<string, string> = {}) {\n process.env.DATABRICKS_HOST = \"https://test.databricks.com\";\n process.env.DATABRICKS_WAREHOUSE_ID = \"test-warehouse-id\";\n Object.assign(process.env, overrides);\n}\n\n/**\n * Set environment variables and return a function that restores each key to its\n * prior state — the prior value, or a delete when the key was previously unset.\n * Shared capture/restore behind {@link withEnv} and the\n * {@link createTestPluginContext} options path; not part of the public surface.\n */\nexport function applyEnv(vars: Record<string, string>): () => void {\n const prior = new Map<string, string | undefined>();\n for (const key of Object.keys(vars)) {\n prior.set(key, process.env[key]);\n }\n Object.assign(process.env, vars);\n return () => {\n for (const [key, value] of prior) {\n if (value === undefined) {\n delete process.env[key];\n } else {\n process.env[key] = value;\n }\n }\n };\n}\n\n/**\n * Run a restore on an error path, suppressing any failure it throws so it\n * cannot replace the caller's original error.\n */\nfunction restoreQuietly(restore: () => void): void {\n try {\n restore();\n } catch (restoreError) {\n // A failed env restore must not mask the caller's original error.\n void restoreError;\n }\n}\n\n/**\n * Sets environment variables for the duration of `fn`, then restores them to\n * their prior state. Each key's prior value (or \"was absent\") is captured on\n * entry; on exit, the prior value is restored, or the key is deleted only if\n * it was previously unset.\n *\n * Supports both sync and async `fn`. If `fn` returns a thenable, `withEnv`\n * returns that promise and restores in `.finally()`. Otherwise, it restores\n * in a synchronous `finally` and returns the callback's return value.\n * Restoration runs even if `fn` throws. Nested calls restore in LIFO order.\n *\n * @example\n * ```ts\n * // Sync: restores synchronously after fn\n * withEnv({ MY_VAR: \"test\" }, () => {\n * console.log(process.env.MY_VAR); // \"test\"\n * });\n * console.log(process.env.MY_VAR); // prior value (or undefined)\n *\n * // Async: restores after promise settles\n * await withEnv({ MY_VAR: \"test\" }, async () => {\n * await fetch(...);\n * });\n * ```\n */\nexport function withEnv<T>(\n vars: Record<string, string>,\n fn: () => T | Promise<T>,\n): T | Promise<T> {\n const restore = applyEnv(vars);\n\n let result: T | Promise<T>;\n try {\n result = fn();\n } catch (err) {\n // Sync throw: restore, but never let a restore failure mask `err`.\n restoreQuietly(restore);\n throw err;\n }\n\n // Async: restore after the promise settles. On rejection, guard the restore\n // so it cannot replace the caller's error; on success, let a genuine restore\n // failure surface.\n if (result != null && typeof (result as Any).then === \"function\") {\n return (result as Promise<T>).then(\n (value) => {\n restore();\n return value;\n },\n (err: unknown) => {\n restoreQuietly(restore);\n throw err;\n },\n );\n }\n\n restore();\n return result;\n}\n\n/**\n * Clears AppKit's process-wide cache singleton so cached values don't leak\n * between tests in the same file.\n *\n * The cache `attach()` seeds is shared by every test in a file (Vitest isolates\n * files, not tests within a file). Call this in `beforeEach` when one test's\n * cached value must not be seen by the next, or mid-test to force a cache miss\n * before asserting a subsequent hit.\n *\n * No-ops when the cache has not been initialized yet, so it is safe to call\n * before any `attach()`.\n *\n * @example\n * ```ts\n * beforeEach(async () => {\n * await resetTestCache();\n * });\n * ```\n */\nexport async function resetTestCache(): Promise<void> {\n let cache: ReturnType<typeof CacheManager.getInstanceSync>;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Not initialized yet — nothing to clear.\n return;\n }\n await cache.clear();\n}\n\n/**\n * Context options for running tests with mocked service/user context\n */\nexport interface TestContextOptions {\n /** Mock WorkspaceClient for service principal operations */\n serviceDatabricksClient?: Any;\n /** Mock WorkspaceClient for user operations */\n userDatabricksClient?: Any;\n /** User ID for user context */\n userId?: string;\n /** Service user ID */\n serviceUserId?: string;\n /** Warehouse ID */\n warehouseId?: string;\n /** Workspace ID */\n workspaceId?: string;\n}\n\n/**\n * Builds a {@link ServiceContextState} value for testing without touching the\n * singleton. Internal building block for {@link mockServiceContext}, which\n * installs the state as spies — that installer is the public entry point.\n */\nfunction buildServiceContextState(\n options: TestContextOptions = {},\n): ServiceContextState {\n return {\n client: (options.serviceDatabricksClient ||\n createMockWorkspaceClient()) as Any,\n serviceUserId: options.serviceUserId || \"test-service-user\",\n warehouseId: Promise.resolve(options.warehouseId || \"test-warehouse-id\"),\n workspaceId: Promise.resolve(options.workspaceId || \"test-workspace-id\"),\n };\n}\n\n/**\n * Mocks the `ServiceContext` singleton for testing — spies `get`,\n * `initialize`, `isInitialized`, and `createUserContext` so code that resolves\n * the service principal or an on-behalf-of user context gets test doubles.\n * Call in `beforeEach`; call the returned `restore()` in `afterEach`.\n *\n * @returns The mock context plus the spies and a `restore()` helper.\n */\nexport function mockServiceContext(options: TestContextOptions = {}) {\n const serviceContext = buildServiceContextState(options);\n\n const getSpy = vi\n .spyOn(ServiceContext, \"get\")\n .mockReturnValue(serviceContext);\n\n const initSpy = vi\n .spyOn(ServiceContext, \"initialize\")\n .mockResolvedValue(serviceContext);\n\n const isInitializedSpy = vi\n .spyOn(ServiceContext, \"isInitialized\")\n .mockReturnValue(true);\n\n const createUserContextSpy = vi\n .spyOn(ServiceContext, \"createUserContext\")\n .mockImplementation(\n fakeUserContext(\n options.userDatabricksClient || createMockWorkspaceClient(),\n serviceContext,\n ),\n );\n\n return {\n serviceContext,\n getSpy,\n initSpy,\n isInitializedSpy,\n createUserContextSpy,\n restore: () => {\n getSpy.mockRestore();\n initSpy.mockRestore();\n isInitializedSpy.mockRestore();\n createUserContextSpy.mockRestore();\n },\n };\n}\n\n/** The handle {@link mockServiceContext} returns (spies + `restore`). */\nexport type ServiceContextMock = ReturnType<typeof mockServiceContext>;\n\n/**\n * Registers a fresh {@link mockServiceContext} before each test and restores it\n * after — the whole `beforeEach`/`afterEach` dance in one line.\n *\n * Call it at the top of a `describe` block (or module top-level), NOT inside a\n * test: Vitest's `beforeEach`/`afterEach` only register during collection, so a\n * call from within a test body registers nothing for that test.\n *\n * Returns a **live** accessor, not the handle: each `beforeEach` builds fresh\n * spies, so reading `.current` inside a test always sees that test's mock. A\n * handle captured once would go stale after the first hook runs.\n *\n * @example\n * ```ts\n * describe(\"my plugin\", () => {\n * const ctx = useServiceContextMock({ warehouseId: \"wh-1\" });\n *\n * test(\"resolves the warehouse\", async () => {\n * await myHandler(req, res);\n * expect(ctx.current.getSpy).toHaveBeenCalled();\n * });\n * });\n * ```\n *\n * @returns `{ current }` — the active {@link ServiceContextMock} for the test.\n */\nexport function useServiceContextMock(options: TestContextOptions = {}): {\n readonly current: ServiceContextMock;\n} {\n let handle: ServiceContextMock | undefined;\n\n beforeEach(() => {\n handle = mockServiceContext(options);\n });\n\n afterEach(() => {\n handle?.restore();\n handle = undefined;\n });\n\n return {\n get current(): ServiceContextMock {\n if (!handle) {\n throw new Error(\n \"useServiceContextMock: no active mock. Call useServiceContextMock() \" +\n \"at the top of a describe block (not inside a test), and read \" +\n \"`.current` from within a test.\",\n );\n }\n return handle;\n },\n };\n}\n\n/**\n * Runs a test function within a mocked service context: installs the mock,\n * runs `fn`, and restores the singleton afterward.\n */\nexport async function runWithRequestContext<T>(\n fn: () => T | Promise<T>,\n context?: TestContextOptions,\n): Promise<T> {\n const mocks = mockServiceContext(context);\n\n try {\n return await fn();\n } finally {\n mocks.restore();\n }\n}\n\n/**\n * Builds a SUCCEEDED SQL statement response with a synthetic statement id,\n * `data_array` rows, and a manifest schema derived from `columns`.\n */\nexport function createSuccessfulSQLResponse(\n data: Any[][],\n columns: Array<{ name: string; type_name?: string }>,\n) {\n return {\n status: { state: \"SUCCEEDED\" },\n statement_id: `stmt-${Date.now()}`,\n result: {\n data_array: data,\n },\n manifest: {\n schema: {\n columns: columns.map((col) => ({\n name: col.name,\n type_name: col.type_name ?? \"STRING\",\n })),\n },\n },\n };\n}\n\n/** Builds a FAILED SQL statement response carrying `errorMessage`. */\nexport function createFailedSQLResponse(errorMessage: string) {\n return {\n status: {\n state: \"FAILED\",\n error: {\n message: errorMessage,\n },\n },\n statement_id: `stmt-${Date.now()}`,\n };\n}\n\n/**\n * Creates a genuine `ApiError` instance for testing error paths. Returns a real\n * instance (where `error instanceof ApiError` holds), suitable for testing\n * `instanceof` checks and `.statusCode` / `.errorCode` / `.message` accessors.\n *\n * @param options Error details: `statusCode`, `message`, and `errorCode`. All required.\n * @returns A genuine `ApiError` instance.\n *\n * @example\n * ```ts\n * const error = createApiError({\n * statusCode: 404,\n * message: \"File not found\",\n * errorCode: \"NOT_FOUND\",\n * });\n * expect(error).toBeInstanceOf(ApiError);\n * expect(error.statusCode).toBe(404);\n * ```\n */\nexport function createApiError(options: {\n statusCode: number;\n message: string;\n errorCode: string;\n}): ApiError {\n return new ApiError(\n options.message,\n options.errorCode,\n options.statusCode,\n undefined, // response: sensible default for testing\n [], // details: empty array\n );\n}\n"],"mappings":"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"}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { PluginContext } from "../core/plugin-context.js";
|
|
2
|
+
import { OboOption, ServiceContextMock, TestContextOptions, createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createSuccessfulSQLResponse, mockServiceContext, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, withEnv } from "./fixtures.js";
|
|
3
|
+
import { CreateMockWorkspaceClientOptions, MockWorkspaceClient, createMockWorkspaceClient, getMock } from "./mock-workspace-client.js";
|
|
4
|
+
import { CreateTestAppOptions, TestApp, TestRequestOptions, createTestApp, getListeningPort } from "./create-test-app.js";
|
|
2
5
|
import { CapturedSSEResponse, ExpectStreamOptions, StreamAssertion, StreamEvent, StreamSource, expectStream, parseSSEResponse } from "./expect-stream.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
+
import { createTestPlugin } from "./create-test-plugin.js";
|
|
7
|
+
import { TestAppHandle, useTestApp } from "./test-app.js";
|
|
8
|
+
import { TestCacheHandle, useTestCache } from "./test-cache.js";
|
|
9
|
+
import { FakeProvider, FakeProviders, FakeToolResponse, RecordedRoute, RecordedToolCall, TestPluginContext, TestPluginContextOptions, createTestPluginContext } from "./test-plugin-context.js";
|
|
10
|
+
export { type CapturedSSEResponse, type CreateMockWorkspaceClientOptions, type CreateTestAppOptions, type ExpectStreamOptions, type FakeProvider, type FakeProviders, type FakeToolResponse, type MockWorkspaceClient, type OboOption, type PluginContext, type RecordedRoute, type RecordedToolCall, type ServiceContextMock, type StreamAssertion, type StreamEvent, type StreamSource, type TestApp, type TestAppHandle, type TestCacheHandle, type TestContextOptions, type TestPluginContext, type TestPluginContextOptions, type TestRequestOptions, createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createMockWorkspaceClient, createSuccessfulSQLResponse, createTestApp, createTestPlugin, createTestPluginContext, expectStream, getListeningPort, getMock, mockServiceContext, parseSSEResponse, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, useTestApp, useTestCache, withEnv };
|
package/dist/testing/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { createMockWorkspaceClient, getMock } from "./mock-workspace-client.js";
|
|
2
|
+
import { createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createSuccessfulSQLResponse, mockServiceContext, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, withEnv } from "./fixtures.js";
|
|
3
|
+
import { createTestApp, getListeningPort } from "./create-test-app.js";
|
|
1
4
|
import { expectStream, parseSSEResponse } from "./expect-stream.js";
|
|
2
|
-
import {
|
|
5
|
+
import { createTestPlugin } from "./create-test-plugin.js";
|
|
6
|
+
import { useTestApp } from "./test-app.js";
|
|
7
|
+
import { useTestCache } from "./test-cache.js";
|
|
3
8
|
import { createTestPluginContext } from "./test-plugin-context.js";
|
|
4
9
|
|
|
5
|
-
export {
|
|
10
|
+
export { createApiError, createFailedSQLResponse, createMockRequest, createMockResponse, createMockRouter, createMockTelemetry, createMockWorkspaceClient, createSuccessfulSQLResponse, createTestApp, createTestPlugin, createTestPluginContext, expectStream, getListeningPort, getMock, mockServiceContext, parseSSEResponse, resetTestCache, runWithRequestContext, setupDatabricksEnv, useServiceContextMock, useTestApp, useTestCache, withEnv };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { WorkspaceClient } from "../workspace-client/index.js";
|
|
2
|
+
import { Mock } from "vitest";
|
|
3
|
+
|
|
4
|
+
//#region src/testing/mock-workspace-client.d.ts
|
|
5
|
+
type Any = any;
|
|
6
|
+
/** Options for {@link createMockWorkspaceClient}. */
|
|
7
|
+
interface CreateMockWorkspaceClientOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Responses keyed by dotted path (`"jobs.getRun"`). A function value is called
|
|
10
|
+
* with the arguments, so a test can script behaviour or reject.
|
|
11
|
+
*/
|
|
12
|
+
responses?: Record<string, Any>;
|
|
13
|
+
/**
|
|
14
|
+
* Throw when a path with no declared response is *called*, instead of
|
|
15
|
+
* resolving `undefined`.
|
|
16
|
+
*
|
|
17
|
+
* Off by default: the never-crash floor is what lets a plugin touch services
|
|
18
|
+
* a test does not care about. Turn it on when a silently `undefined` return
|
|
19
|
+
* would let the test pass for the wrong reason.
|
|
20
|
+
*/
|
|
21
|
+
strict?: boolean;
|
|
22
|
+
/** Seed `config`; `host` must stay a real string. */
|
|
23
|
+
config?: Partial<WorkspaceClient["config"]>;
|
|
24
|
+
/** Apply the canned defaults (SQL succeeds, warehouse RUNNING). Default true. */
|
|
25
|
+
defaults?: boolean;
|
|
26
|
+
}
|
|
27
|
+
type MockWorkspaceClient = WorkspaceClient;
|
|
28
|
+
/**
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const client = createMockWorkspaceClient({
|
|
32
|
+
* responses: { "jobs.getRun": { state: "TERMINATED" } },
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare function createMockWorkspaceClient(options?: CreateMockWorkspaceClientOptions): MockWorkspaceClient;
|
|
37
|
+
/**
|
|
38
|
+
* The typed assertion path onto a mocked method — facade accessors are SDK-typed,
|
|
39
|
+
* so `expect(client.jobs.getRun).toHaveBeenCalled()` does not typecheck.
|
|
40
|
+
*
|
|
41
|
+
* Minting is idempotent, so this can be called before the code under test runs.
|
|
42
|
+
* Throws for a non-function member such as `"config.host"`.
|
|
43
|
+
*/
|
|
44
|
+
declare function getMock(client: MockWorkspaceClient, path: string): Mock;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { CreateMockWorkspaceClientOptions, MockWorkspaceClient, createMockWorkspaceClient, getMock };
|
|
47
|
+
//# sourceMappingURL=mock-workspace-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-workspace-client.d.ts","names":[],"sources":["../../src/testing/mock-workspace-client.ts"],"mappings":";;;;KAYK,GAAA;;UAKY,gCAAA;EAAA;;;;EAKf,SAAA,GAAY,MAAA,SAAe,GAAA;EAYV;;;;;;;;EAHjB,MAAA;EAGS;EAAT,MAAA,GAAS,OAAA,CAAQ,eAAA;EAGjB;EAAA,QAAA;AAAA;AAAA,KAGU,mBAAA,GAAsB,eAAA;;;;;AA+FlC;;;;iBAAgB,yBAAA,CACd,OAAA,GAAS,gCAAA,GACR,mBAAA;;;;;AA2JH;;;iBAAgB,OAAA,CAAQ,MAAA,EAAQ,mBAAA,EAAqB,IAAA,WAAe,IAAA"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
//#region src/testing/mock-workspace-client.ts
|
|
4
|
+
/**
|
|
5
|
+
* Applied beneath caller-supplied `responses`.
|
|
6
|
+
*
|
|
7
|
+
* `statementExecution.executeStatement`, `warehouses.get` and `warehouses.start`
|
|
8
|
+
* must stay byte-identical to the old `fixtures.ts` values — suites reach them
|
|
9
|
+
* implicitly through `mockServiceContext`. `currentUser.me` is
|
|
10
|
+
* required: `ServiceContext.createContext` reads `.id`, so `createApp({ client })`
|
|
11
|
+
* cannot boot without it.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_RESPONSES = {
|
|
14
|
+
"statementExecution.executeStatement": {
|
|
15
|
+
status: { state: "SUCCEEDED" },
|
|
16
|
+
result: { data: [] }
|
|
17
|
+
},
|
|
18
|
+
"warehouses.get": { state: "RUNNING" },
|
|
19
|
+
"warehouses.start": void 0,
|
|
20
|
+
"currentUser.me": {
|
|
21
|
+
id: "test-service-user",
|
|
22
|
+
userName: "test-service-user"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Generically proxied; `config`/`apiClient` are seeded below instead. */
|
|
26
|
+
const FACADE_SERVICES = [
|
|
27
|
+
"files",
|
|
28
|
+
"warehouses",
|
|
29
|
+
"genie",
|
|
30
|
+
"jobs",
|
|
31
|
+
"statementExecution",
|
|
32
|
+
"servingEndpoints",
|
|
33
|
+
"currentUser"
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Answered with `undefined` rather than a minted mock. `then` must stay listed:
|
|
37
|
+
* without it a service is thenable, so `await client.jobs` hangs.
|
|
38
|
+
*/
|
|
39
|
+
const PASSTHROUGH_DENY = new Set([
|
|
40
|
+
"then",
|
|
41
|
+
"catch",
|
|
42
|
+
"finally",
|
|
43
|
+
"toJSON",
|
|
44
|
+
"inspect",
|
|
45
|
+
"constructor",
|
|
46
|
+
"$$typeof",
|
|
47
|
+
"asymmetricMatch"
|
|
48
|
+
]);
|
|
49
|
+
/** `apiClient` members read synchronously; a seeded value must not be resolved. */
|
|
50
|
+
const SYNC_API_CLIENT_MEMBERS = new Set(["userAgent"]);
|
|
51
|
+
/** Distinguishes "no passthrough rule applied" from a rule answering `undefined`. */
|
|
52
|
+
const NOT_PASSTHROUGH = Symbol("not-passthrough");
|
|
53
|
+
/**
|
|
54
|
+
* The passthrough rules every trap in this file shares: symbols delegate to the
|
|
55
|
+
* target, denied names answer `undefined`, and anything already on the target
|
|
56
|
+
* (seeded members, `Object.prototype`) wins over minting.
|
|
57
|
+
*
|
|
58
|
+
* Shared rather than copied so a key added to {@link PASSTHROUGH_DENY} cannot
|
|
59
|
+
* cover one trap and miss another.
|
|
60
|
+
*/
|
|
61
|
+
function passthroughFor(target, prop) {
|
|
62
|
+
if (typeof prop === "symbol") return Reflect.get(target, prop);
|
|
63
|
+
if (PASSTHROUGH_DENY.has(prop)) return void 0;
|
|
64
|
+
if (prop in target) return target[prop];
|
|
65
|
+
return NOT_PASSTHROUGH;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `ownKeys`/`getOwnPropertyDescriptor` stay at their defaults on purpose —
|
|
69
|
+
* reporting keys makes `util.inspect` probe each one, minting a mock per probe.
|
|
70
|
+
*/
|
|
71
|
+
function neverCrashGet(namespace, mint) {
|
|
72
|
+
return (target, prop) => {
|
|
73
|
+
const passthrough = passthroughFor(target, prop);
|
|
74
|
+
if (passthrough !== NOT_PASSTHROUGH) return passthrough;
|
|
75
|
+
return mint(`${namespace}.${String(prop)}`);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const clientFns = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
/**
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const client = createMockWorkspaceClient({
|
|
83
|
+
* responses: { "jobs.getRun": { state: "TERMINATED" } },
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
function createMockWorkspaceClient(options = {}) {
|
|
88
|
+
const { responses = {}, config = {}, defaults = true, strict = false } = options;
|
|
89
|
+
const merged = defaults ? {
|
|
90
|
+
...DEFAULT_RESPONSES,
|
|
91
|
+
...responses
|
|
92
|
+
} : { ...responses };
|
|
93
|
+
const fns = /* @__PURE__ */ new Map();
|
|
94
|
+
/** Mint once per path, so call assertions see a stable reference. */
|
|
95
|
+
function mint(path) {
|
|
96
|
+
const cached = fns.get(path);
|
|
97
|
+
if (cached) return cached;
|
|
98
|
+
const response = merged[path];
|
|
99
|
+
const fn = vi.fn();
|
|
100
|
+
if (typeof response === "function") fn.mockImplementation(response);
|
|
101
|
+
else if (strict && !(path in merged)) fn.mockImplementation(() => {
|
|
102
|
+
throw new Error(`createMockWorkspaceClient: "${path}" was called with no declared response and \`strict: true\` is set. Add it to \`responses\`, or drop \`strict\` to have undeclared paths resolve undefined.`);
|
|
103
|
+
});
|
|
104
|
+
else fn.mockResolvedValue(response);
|
|
105
|
+
fns.set(path, fn);
|
|
106
|
+
return fn;
|
|
107
|
+
}
|
|
108
|
+
/** Memoized, so `client.jobs === client.jobs`. */
|
|
109
|
+
const services = /* @__PURE__ */ new Map();
|
|
110
|
+
function service(namespace) {
|
|
111
|
+
const cached = services.get(namespace);
|
|
112
|
+
if (cached) return cached;
|
|
113
|
+
const proxy = new Proxy({}, { get: neverCrashGet(namespace, mint) });
|
|
114
|
+
services.set(namespace, proxy);
|
|
115
|
+
return proxy;
|
|
116
|
+
}
|
|
117
|
+
/** Pull `"config.*"` / `"apiClient.*"` entries out so they seed real values. */
|
|
118
|
+
function seededOverrides(namespace) {
|
|
119
|
+
const prefix = `${namespace}.`;
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const [key, value] of Object.entries(merged)) if (key.startsWith(prefix)) out[key.slice(prefix.length)] = value;
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
const configTarget = {
|
|
125
|
+
host: "https://test.databricks.com",
|
|
126
|
+
authenticate: vi.fn((headers) => {
|
|
127
|
+
headers?.set?.("Authorization", "Bearer test-token");
|
|
128
|
+
}),
|
|
129
|
+
ensureResolved: vi.fn().mockResolvedValue(void 0),
|
|
130
|
+
...config,
|
|
131
|
+
...seededOverrides("config")
|
|
132
|
+
};
|
|
133
|
+
const apiClientTarget = {
|
|
134
|
+
userAgent: vi.fn().mockReturnValue("appkit-test/1.0"),
|
|
135
|
+
request: vi.fn().mockResolvedValue({})
|
|
136
|
+
};
|
|
137
|
+
for (const [key, value] of Object.entries(seededOverrides("apiClient"))) {
|
|
138
|
+
const fn = typeof value === "function" ? vi.fn(value) : vi.fn();
|
|
139
|
+
if (typeof value !== "function") if (SYNC_API_CLIENT_MEMBERS.has(key)) fn.mockReturnValue(value);
|
|
140
|
+
else fn.mockResolvedValue(value);
|
|
141
|
+
apiClientTarget[key] = fn;
|
|
142
|
+
fns.set(`apiClient.${key}`, fn);
|
|
143
|
+
}
|
|
144
|
+
for (const key of ["userAgent", "request"]) if (!fns.has(`apiClient.${key}`)) fns.set(`apiClient.${key}`, apiClientTarget[key]);
|
|
145
|
+
for (const [key, value] of Object.entries(configTarget)) if (typeof value === "function" && !fns.has(`config.${key}`)) fns.set(`config.${key}`, value);
|
|
146
|
+
const configProxy = new Proxy(configTarget, { get: neverCrashGet("config", mint) });
|
|
147
|
+
const apiClientProxy = new Proxy(apiClientTarget, { get: neverCrashGet("apiClient", mint) });
|
|
148
|
+
/** Memoized; routes facade names onto the same objects, others onto the floor. */
|
|
149
|
+
let legacy;
|
|
150
|
+
function toLegacyWorkspaceClient() {
|
|
151
|
+
legacy ??= new Proxy({}, { get: (target, prop) => {
|
|
152
|
+
const passthrough = passthroughFor(target, prop);
|
|
153
|
+
if (passthrough !== NOT_PASSTHROUGH) return passthrough;
|
|
154
|
+
if (prop === "config") return configProxy;
|
|
155
|
+
if (prop === "apiClient") return apiClientProxy;
|
|
156
|
+
if (prop === "toLegacyWorkspaceClient") return toLegacyWorkspaceClient;
|
|
157
|
+
return service(String(prop));
|
|
158
|
+
} });
|
|
159
|
+
return legacy;
|
|
160
|
+
}
|
|
161
|
+
const client = {
|
|
162
|
+
...Object.fromEntries(FACADE_SERVICES.map((name) => [name, service(name)])),
|
|
163
|
+
config: configProxy,
|
|
164
|
+
apiClient: apiClientProxy,
|
|
165
|
+
toLegacyWorkspaceClient
|
|
166
|
+
};
|
|
167
|
+
clientFns.set(client, fns);
|
|
168
|
+
return client;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The typed assertion path onto a mocked method — facade accessors are SDK-typed,
|
|
172
|
+
* so `expect(client.jobs.getRun).toHaveBeenCalled()` does not typecheck.
|
|
173
|
+
*
|
|
174
|
+
* Minting is idempotent, so this can be called before the code under test runs.
|
|
175
|
+
* Throws for a non-function member such as `"config.host"`.
|
|
176
|
+
*/
|
|
177
|
+
function getMock(client, path) {
|
|
178
|
+
const fns = clientFns.get(client);
|
|
179
|
+
if (!fns) throw new Error("getMock: not a createMockWorkspaceClient() client. Pass the client the builder returned, not a hand-rolled object.");
|
|
180
|
+
const cached = fns.get(path);
|
|
181
|
+
if (cached) return cached;
|
|
182
|
+
const dot = path.indexOf(".");
|
|
183
|
+
const namespace = dot === -1 ? path : path.slice(0, dot);
|
|
184
|
+
const member = dot === -1 ? "" : path.slice(dot + 1);
|
|
185
|
+
const resolved = member ? client[namespace]?.[member] : client[namespace];
|
|
186
|
+
if (typeof resolved !== "function") throw new Error(`getMock: "${path}" is not a mocked function (got ${typeof resolved}). Members seeded with a real value, such as config.host, have no mock.`);
|
|
187
|
+
return resolved;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
export { createMockWorkspaceClient, getMock };
|
|
192
|
+
//# sourceMappingURL=mock-workspace-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-workspace-client.js","names":[],"sources":["../../src/testing/mock-workspace-client.ts"],"sourcesContent":["/**\n * A never-crash fake `WorkspaceClient`. Declared paths resolve their value;\n * everything else resolves `undefined` instead of throwing.\n */\n\nimport type { Mock } from \"vitest\";\nimport { vi } from \"vitest\";\n\nimport type { WorkspaceClient } from \"../workspace-client\";\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\ntype LegacyClient = ReturnType<WorkspaceClient[\"toLegacyWorkspaceClient\"]>;\n\n/** Options for {@link createMockWorkspaceClient}. */\nexport interface CreateMockWorkspaceClientOptions {\n /**\n * Responses keyed by dotted path (`\"jobs.getRun\"`). A function value is called\n * with the arguments, so a test can script behaviour or reject.\n */\n responses?: Record<string, Any>;\n /**\n * Throw when a path with no declared response is *called*, instead of\n * resolving `undefined`.\n *\n * Off by default: the never-crash floor is what lets a plugin touch services\n * a test does not care about. Turn it on when a silently `undefined` return\n * would let the test pass for the wrong reason.\n */\n strict?: boolean;\n\n /** Seed `config`; `host` must stay a real string. */\n config?: Partial<WorkspaceClient[\"config\"]>;\n\n /** Apply the canned defaults (SQL succeeds, warehouse RUNNING). Default true. */\n defaults?: boolean;\n}\n\nexport type MockWorkspaceClient = WorkspaceClient;\n\n/**\n * Applied beneath caller-supplied `responses`.\n *\n * `statementExecution.executeStatement`, `warehouses.get` and `warehouses.start`\n * must stay byte-identical to the old `fixtures.ts` values — suites reach them\n * implicitly through `mockServiceContext`. `currentUser.me` is\n * required: `ServiceContext.createContext` reads `.id`, so `createApp({ client })`\n * cannot boot without it.\n */\nconst DEFAULT_RESPONSES: Record<string, Any> = {\n \"statementExecution.executeStatement\": {\n status: { state: \"SUCCEEDED\" },\n result: { data: [] },\n },\n \"warehouses.get\": { state: \"RUNNING\" },\n \"warehouses.start\": undefined,\n \"currentUser.me\": {\n id: \"test-service-user\",\n userName: \"test-service-user\",\n },\n};\n\n/** Generically proxied; `config`/`apiClient` are seeded below instead. */\nconst FACADE_SERVICES = [\n \"files\",\n \"warehouses\",\n \"genie\",\n \"jobs\",\n \"statementExecution\",\n \"servingEndpoints\",\n \"currentUser\",\n] as const;\n\n/**\n * Answered with `undefined` rather than a minted mock. `then` must stay listed:\n * without it a service is thenable, so `await client.jobs` hangs.\n */\nconst PASSTHROUGH_DENY: ReadonlySet<string> = new Set([\n \"then\",\n \"catch\",\n \"finally\",\n \"toJSON\",\n \"inspect\",\n \"constructor\",\n \"$$typeof\",\n \"asymmetricMatch\",\n]);\n\n/** `apiClient` members read synchronously; a seeded value must not be resolved. */\nconst SYNC_API_CLIENT_MEMBERS: ReadonlySet<string> = new Set([\"userAgent\"]);\n\n/** Distinguishes \"no passthrough rule applied\" from a rule answering `undefined`. */\nconst NOT_PASSTHROUGH = Symbol(\"not-passthrough\");\n\n/**\n * The passthrough rules every trap in this file shares: symbols delegate to the\n * target, denied names answer `undefined`, and anything already on the target\n * (seeded members, `Object.prototype`) wins over minting.\n *\n * Shared rather than copied so a key added to {@link PASSTHROUGH_DENY} cannot\n * cover one trap and miss another.\n */\nfunction passthroughFor(target: Any, prop: Any): Any {\n if (typeof prop === \"symbol\") return Reflect.get(target, prop);\n if (PASSTHROUGH_DENY.has(prop)) return undefined;\n if (prop in target) return target[prop];\n return NOT_PASSTHROUGH;\n}\n\n/**\n * `ownKeys`/`getOwnPropertyDescriptor` stay at their defaults on purpose —\n * reporting keys makes `util.inspect` probe each one, minting a mock per probe.\n */\nfunction neverCrashGet(namespace: string, mint: (path: string) => Mock) {\n return (target: Any, prop: Any): Any => {\n const passthrough = passthroughFor(target, prop);\n if (passthrough !== NOT_PASSTHROUGH) return passthrough;\n return mint(`${namespace}.${String(prop)}`);\n };\n}\n\n// In a WeakMap, not on the client: a stray own property would show up in\n// util.inspect, toEqual, and key enumeration.\nconst clientFns = new WeakMap<WorkspaceClient, Map<string, Mock>>();\n\n/**\n * @example\n * ```ts\n * const client = createMockWorkspaceClient({\n * responses: { \"jobs.getRun\": { state: \"TERMINATED\" } },\n * });\n * ```\n */\nexport function createMockWorkspaceClient(\n options: CreateMockWorkspaceClientOptions = {},\n): MockWorkspaceClient {\n const {\n responses = {},\n config = {},\n defaults = true,\n strict = false,\n } = options;\n\n // Caller entries win over the canned defaults for the same path.\n const merged: Record<string, Any> = defaults\n ? { ...DEFAULT_RESPONSES, ...responses }\n : { ...responses };\n\n // Shared with the legacy view and getMock, so both see the same functions.\n const fns = new Map<string, Mock>();\n\n /** Mint once per path, so call assertions see a stable reference. */\n function mint(path: string): Mock {\n const cached = fns.get(path);\n if (cached) return cached;\n\n const response = merged[path];\n const fn = vi.fn();\n if (typeof response === \"function\") {\n fn.mockImplementation(response);\n } else if (strict && !(path in merged)) {\n // Thrown on call, never on mint: `getMock` mints to hand back a handle\n // before the code under test runs, and that must not blow up.\n fn.mockImplementation(() => {\n throw new Error(\n `createMockWorkspaceClient: \"${path}\" was called with no declared ` +\n \"response and `strict: true` is set. Add it to `responses`, or drop \" +\n \"`strict` to have undeclared paths resolve undefined.\",\n );\n });\n } else {\n fn.mockResolvedValue(response);\n }\n\n fns.set(path, fn);\n return fn;\n }\n\n /** Memoized, so `client.jobs === client.jobs`. */\n const services = new Map<string, Any>();\n function service(namespace: string): Any {\n const cached = services.get(namespace);\n if (cached) return cached;\n const proxy = new Proxy({}, { get: neverCrashGet(namespace, mint) });\n services.set(namespace, proxy);\n return proxy;\n }\n\n /** Pull `\"config.*\"` / `\"apiClient.*\"` entries out so they seed real values. */\n function seededOverrides(namespace: string): Record<string, Any> {\n const prefix = `${namespace}.`;\n const out: Record<string, Any> = {};\n for (const [key, value] of Object.entries(merged)) {\n if (key.startsWith(prefix)) out[key.slice(prefix.length)] = value;\n }\n return out;\n }\n\n // `host` is read as a string and throws if falsy, so it cannot be a mock.\n const configTarget: Record<string, Any> = {\n host: \"https://test.databricks.com\",\n authenticate: vi.fn((headers?: Headers) => {\n headers?.set?.(\"Authorization\", \"Bearer test-token\");\n }),\n ensureResolved: vi.fn().mockResolvedValue(undefined),\n ...config,\n ...seededOverrides(\"config\"),\n };\n\n // userAgent() must be synchronous (a Promise stringifies to \"[object Promise]\"\n // inside a Headers value); request resolves {} so destructuring works.\n const apiClientTarget: Record<string, Any> = {\n userAgent: vi.fn().mockReturnValue(\"appkit-test/1.0\"),\n request: vi.fn().mockResolvedValue({}),\n };\n for (const [key, value] of Object.entries(seededOverrides(\"apiClient\"))) {\n const fn = typeof value === \"function\" ? vi.fn(value) : vi.fn();\n if (typeof value !== \"function\") {\n // Match the member's own shape. `userAgent()` is read straight into a\n // Headers value, so resolving a seed would put \"[object Promise]\" there —\n // the failure the synchronous default exists to avoid. Seed a function to\n // decide for yourself.\n if (SYNC_API_CLIENT_MEMBERS.has(key)) fn.mockReturnValue(value);\n else fn.mockResolvedValue(value);\n }\n apiClientTarget[key] = fn;\n fns.set(`apiClient.${key}`, fn);\n }\n for (const key of [\"userAgent\", \"request\"]) {\n if (!fns.has(`apiClient.${key}`)) {\n fns.set(`apiClient.${key}`, apiClientTarget[key] as Mock);\n }\n }\n for (const [key, value] of Object.entries(configTarget)) {\n if (typeof value === \"function\" && !fns.has(`config.${key}`)) {\n fns.set(`config.${key}`, value as Mock);\n }\n }\n\n const configProxy = new Proxy(configTarget, {\n get: neverCrashGet(\"config\", mint),\n });\n const apiClientProxy = new Proxy(apiClientTarget, {\n get: neverCrashGet(\"apiClient\", mint),\n });\n\n /** Memoized; routes facade names onto the same objects, others onto the floor. */\n let legacy: LegacyClient | undefined;\n function toLegacyWorkspaceClient(): LegacyClient {\n legacy ??= new Proxy(\n {},\n {\n get: (target: Any, prop: Any): Any => {\n // Same passthrough rules as `neverCrashGet` — shared, not copied, so a\n // key added to PASSTHROUGH_DENY cannot cover one trap and miss this\n // one (miss `then` here and `await client` hangs again).\n const passthrough = passthroughFor(target, prop);\n if (passthrough !== NOT_PASSTHROUGH) return passthrough;\n if (prop === \"config\") return configProxy;\n if (prop === \"apiClient\") return apiClientProxy;\n if (prop === \"toLegacyWorkspaceClient\") {\n return toLegacyWorkspaceClient;\n }\n return service(String(prop));\n },\n },\n ) as LegacyClient;\n return legacy;\n }\n\n const client: WorkspaceClient = {\n ...(Object.fromEntries(\n FACADE_SERVICES.map((name) => [name, service(name)]),\n ) as Pick<WorkspaceClient, (typeof FACADE_SERVICES)[number]>),\n config: configProxy as WorkspaceClient[\"config\"],\n apiClient: apiClientProxy as WorkspaceClient[\"apiClient\"],\n toLegacyWorkspaceClient,\n };\n\n clientFns.set(client, fns);\n return client;\n}\n\n/**\n * The typed assertion path onto a mocked method — facade accessors are SDK-typed,\n * so `expect(client.jobs.getRun).toHaveBeenCalled()` does not typecheck.\n *\n * Minting is idempotent, so this can be called before the code under test runs.\n * Throws for a non-function member such as `\"config.host\"`.\n */\nexport function getMock(client: MockWorkspaceClient, path: string): Mock {\n const fns = clientFns.get(client);\n if (!fns) {\n throw new Error(\n \"getMock: not a createMockWorkspaceClient() client. Pass the client \" +\n \"the builder returned, not a hand-rolled object.\",\n );\n }\n\n const cached = fns.get(path);\n if (cached) return cached;\n\n const dot = path.indexOf(\".\");\n const namespace = dot === -1 ? path : path.slice(0, dot);\n const member = dot === -1 ? \"\" : path.slice(dot + 1);\n const resolved = member\n ? (client as Any)[namespace]?.[member]\n : (client as Any)[namespace];\n\n if (typeof resolved !== \"function\") {\n throw new Error(\n `getMock: \"${path}\" is not a mocked function (got ${typeof resolved}). ` +\n \"Members seeded with a real value, such as config.host, have no mock.\",\n );\n }\n return resolved as Mock;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ServiceContext } from "../context/service-context.js";
|
|
2
|
+
import { createLogger } from "../logging/logger.js";
|
|
3
|
+
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
4
|
+
import "../telemetry/index.js";
|
|
5
|
+
import { CacheManager } from "../cache/index.js";
|
|
6
|
+
import "../context/index.js";
|
|
7
|
+
import { TelemetryReporter } from "../internal-telemetry/reporter.js";
|
|
8
|
+
import "../internal-telemetry/index.js";
|
|
9
|
+
|
|
10
|
+
//#region src/testing/reset-singletons.ts
|
|
11
|
+
const logger = createLogger("testing");
|
|
12
|
+
/**
|
|
13
|
+
* Drop the process-wide singletons `AppKit._createApp` initializes — called by
|
|
14
|
+
* the harness on boot to clear a previous test's leakage, and on teardown.
|
|
15
|
+
*
|
|
16
|
+
* Kit-owned: the only caller is the test harness, so it lives here rather than
|
|
17
|
+
* in core. A pointer drop, not teardown — close the app first (the harness runs
|
|
18
|
+
* the shutdown phases before this) or the old app's storage and exporters leak.
|
|
19
|
+
* A caller that drops then reads `ServiceContext.get()` gets an
|
|
20
|
+
* `InitializationError`.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
function dropCoreSingletons() {
|
|
24
|
+
const resets = [
|
|
25
|
+
["ServiceContext", () => ServiceContext.reset()],
|
|
26
|
+
["CacheManager", () => CacheManager.reset()],
|
|
27
|
+
["TelemetryReporter", () => TelemetryReporter._reset()],
|
|
28
|
+
["TelemetryManager", () => TelemetryManager.reset()]
|
|
29
|
+
];
|
|
30
|
+
for (const [name, reset] of resets) try {
|
|
31
|
+
reset();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.error("Error resetting %s: %O", name, err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { dropCoreSingletons };
|
|
39
|
+
//# sourceMappingURL=reset-singletons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset-singletons.js","names":[],"sources":["../../src/testing/reset-singletons.ts"],"sourcesContent":["import { CacheManager } from \"../cache\";\nimport { ServiceContext } from \"../context\";\nimport { TelemetryReporter } from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryManager } from \"../telemetry\";\n\nconst logger = createLogger(\"testing\");\n\n/**\n * Drop the process-wide singletons `AppKit._createApp` initializes — called by\n * the harness on boot to clear a previous test's leakage, and on teardown.\n *\n * Kit-owned: the only caller is the test harness, so it lives here rather than\n * in core. A pointer drop, not teardown — close the app first (the harness runs\n * the shutdown phases before this) or the old app's storage and exporters leak.\n * A caller that drops then reads `ServiceContext.get()` gets an\n * `InitializationError`.\n * @internal\n */\nexport function dropCoreSingletons(): void {\n const resets: [string, () => void][] = [\n [\"ServiceContext\", () => ServiceContext.reset()],\n [\"CacheManager\", () => CacheManager.reset()],\n [\"TelemetryReporter\", () => TelemetryReporter._reset()],\n [\"TelemetryManager\", () => TelemetryManager.reset()],\n ];\n\n for (const [name, reset] of resets) {\n try {\n reset();\n } catch (err) {\n logger.error(\"Error resetting %s: %O\", name, err);\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAMA,MAAM,SAAS,aAAa,UAAU;;;;;;;;;;;;AAatC,SAAgB,qBAA2B;CACzC,MAAM,SAAiC;EACrC,CAAC,wBAAwB,eAAe,OAAO,CAAC;EAChD,CAAC,sBAAsB,aAAa,OAAO,CAAC;EAC5C,CAAC,2BAA2B,kBAAkB,QAAQ,CAAC;EACvD,CAAC,0BAA0B,iBAAiB,OAAO,CAAC;EACrD;AAED,MAAK,MAAM,CAAC,MAAM,UAAU,OAC1B,KAAI;AACF,SAAO;UACA,KAAK;AACZ,SAAO,MAAM,0BAA0B,MAAM,IAAI"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PluginConstructor, PluginData } from "../shared/src/plugin.js";
|
|
2
|
+
import "../shared/src/index.js";
|
|
3
|
+
import { CreateTestAppOptions, TestApp } from "./create-test-app.js";
|
|
4
|
+
|
|
5
|
+
//#region src/testing/test-app.d.ts
|
|
6
|
+
/** Mirrors `create-test-app.ts`'s own constraint; not part of the public surface. */
|
|
7
|
+
type Plugins = PluginData<PluginConstructor, unknown, string>[];
|
|
8
|
+
/**
|
|
9
|
+
* The handle {@link useTestApp} returns: a live accessor for the harness app
|
|
10
|
+
* booted for the current test.
|
|
11
|
+
*/
|
|
12
|
+
interface TestAppHandle<T extends Plugins> {
|
|
13
|
+
/**
|
|
14
|
+
* The app booted for the current test. Read it inside a test body — each
|
|
15
|
+
* `beforeEach` boots a fresh app and each `afterEach` closes it.
|
|
16
|
+
*/
|
|
17
|
+
readonly current: TestApp<T>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Boot a harness app before each test and close it after, so a suite that needs
|
|
21
|
+
* an app per test never hand-wires the hooks or risks a forgotten `close()`.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors {@link useServiceContextMock} and {@link useTestCache}: call it at the
|
|
24
|
+
* top of a `describe` block (or module top-level), NOT inside a test — Vitest
|
|
25
|
+
* registers `beforeEach`/`afterEach` during collection.
|
|
26
|
+
*
|
|
27
|
+
* Reach for this when the app must outlive a single expression. `await using`
|
|
28
|
+
* covers one test more concisely, but it cannot carry an app from a `beforeEach`
|
|
29
|
+
* into the test body, and the harness allows only one open app at a time — so a
|
|
30
|
+
* `describe` that holds one in `beforeAll` cannot contain a test that boots its
|
|
31
|
+
* own.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* describe("my plugin over HTTP", () => {
|
|
36
|
+
* const app = useTestApp({
|
|
37
|
+
* plugins: [myPlugin()],
|
|
38
|
+
* responses: { "jobs.getRun": { state: "TERMINATED" } },
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* test("answers a request", async () => {
|
|
42
|
+
* const res = await app.current.post("/api/my-plugin/run", { body: { id: 1 } });
|
|
43
|
+
* expect(res.status).toBe(200);
|
|
44
|
+
* });
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @param options - Passed to {@link createTestApp} unchanged, for every boot.
|
|
49
|
+
* @returns `{ current }` — the app booted for the current test.
|
|
50
|
+
*/
|
|
51
|
+
declare function useTestApp<T extends Plugins>(options?: CreateTestAppOptions<T>): TestAppHandle<T>;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { TestAppHandle, useTestApp };
|
|
54
|
+
//# sourceMappingURL=test-app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-app.d.ts","names":[],"sources":["../../src/testing/test-app.ts"],"mappings":";;;;;;KAOK,OAAA,GAAU,UAAA,CAAW,iBAAA;;AAJ6C;;;UAUtD,aAAA,WAAwB,OAAA;EANE;AAM3C;;;EAN2C,SAWhC,OAAA,EAAS,OAAA,CAAQ,CAAA;AAAA;;;;;;;;;;;AAmC5B;;;;;;;;;;;;;;;;;;;;;;iBAAgB,UAAA,WAAqB,OAAA,CAAA,CACnC,OAAA,GAAS,oBAAA,CAAqB,CAAA,IAC7B,aAAA,CAAc,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createTestApp } from "./create-test-app.js";
|
|
2
|
+
import { afterEach, beforeEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
//#region src/testing/test-app.ts
|
|
5
|
+
/**
|
|
6
|
+
* Boot a harness app before each test and close it after, so a suite that needs
|
|
7
|
+
* an app per test never hand-wires the hooks or risks a forgotten `close()`.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors {@link useServiceContextMock} and {@link useTestCache}: call it at the
|
|
10
|
+
* top of a `describe` block (or module top-level), NOT inside a test — Vitest
|
|
11
|
+
* registers `beforeEach`/`afterEach` during collection.
|
|
12
|
+
*
|
|
13
|
+
* Reach for this when the app must outlive a single expression. `await using`
|
|
14
|
+
* covers one test more concisely, but it cannot carry an app from a `beforeEach`
|
|
15
|
+
* into the test body, and the harness allows only one open app at a time — so a
|
|
16
|
+
* `describe` that holds one in `beforeAll` cannot contain a test that boots its
|
|
17
|
+
* own.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* describe("my plugin over HTTP", () => {
|
|
22
|
+
* const app = useTestApp({
|
|
23
|
+
* plugins: [myPlugin()],
|
|
24
|
+
* responses: { "jobs.getRun": { state: "TERMINATED" } },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* test("answers a request", async () => {
|
|
28
|
+
* const res = await app.current.post("/api/my-plugin/run", { body: { id: 1 } });
|
|
29
|
+
* expect(res.status).toBe(200);
|
|
30
|
+
* });
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @param options - Passed to {@link createTestApp} unchanged, for every boot.
|
|
35
|
+
* @returns `{ current }` — the app booted for the current test.
|
|
36
|
+
*/
|
|
37
|
+
function useTestApp(options = {}) {
|
|
38
|
+
let app;
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
app = await createTestApp(options);
|
|
41
|
+
});
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
const booted = app;
|
|
44
|
+
app = void 0;
|
|
45
|
+
await booted?.close();
|
|
46
|
+
});
|
|
47
|
+
return { get current() {
|
|
48
|
+
if (!app) throw new Error("useTestApp: no active app. Call useTestApp() at the top of a describe block (not inside a test), and read `.current` from within a test.");
|
|
49
|
+
return app;
|
|
50
|
+
} };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { useTestApp };
|
|
55
|
+
//# sourceMappingURL=test-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-app.js","names":[],"sources":["../../src/testing/test-app.ts"],"sourcesContent":["import type { PluginConstructor, PluginData } from \"shared\";\nimport { afterEach, beforeEach } from \"vitest\";\n\nimport type { CreateTestAppOptions, TestApp } from \"./create-test-app\";\nimport { createTestApp } from \"./create-test-app\";\n\n/** Mirrors `create-test-app.ts`'s own constraint; not part of the public surface. */\ntype Plugins = PluginData<PluginConstructor, unknown, string>[];\n\n/**\n * The handle {@link useTestApp} returns: a live accessor for the harness app\n * booted for the current test.\n */\nexport interface TestAppHandle<T extends Plugins> {\n /**\n * The app booted for the current test. Read it inside a test body — each\n * `beforeEach` boots a fresh app and each `afterEach` closes it.\n */\n readonly current: TestApp<T>;\n}\n\n/**\n * Boot a harness app before each test and close it after, so a suite that needs\n * an app per test never hand-wires the hooks or risks a forgotten `close()`.\n *\n * Mirrors {@link useServiceContextMock} and {@link useTestCache}: call it at the\n * top of a `describe` block (or module top-level), NOT inside a test — Vitest\n * registers `beforeEach`/`afterEach` during collection.\n *\n * Reach for this when the app must outlive a single expression. `await using`\n * covers one test more concisely, but it cannot carry an app from a `beforeEach`\n * into the test body, and the harness allows only one open app at a time — so a\n * `describe` that holds one in `beforeAll` cannot contain a test that boots its\n * own.\n *\n * @example\n * ```ts\n * describe(\"my plugin over HTTP\", () => {\n * const app = useTestApp({\n * plugins: [myPlugin()],\n * responses: { \"jobs.getRun\": { state: \"TERMINATED\" } },\n * });\n *\n * test(\"answers a request\", async () => {\n * const res = await app.current.post(\"/api/my-plugin/run\", { body: { id: 1 } });\n * expect(res.status).toBe(200);\n * });\n * });\n * ```\n *\n * @param options - Passed to {@link createTestApp} unchanged, for every boot.\n * @returns `{ current }` — the app booted for the current test.\n */\nexport function useTestApp<T extends Plugins>(\n options: CreateTestAppOptions<T> = {},\n): TestAppHandle<T> {\n let app: TestApp<T> | undefined;\n\n beforeEach(async () => {\n app = await createTestApp(options);\n });\n\n afterEach(async () => {\n const booted = app;\n // Cleared before the await so a close that throws cannot leave a stale\n // handle readable by the next test.\n app = undefined;\n await booted?.close();\n });\n\n return {\n get current(): TestApp<T> {\n if (!app) {\n throw new Error(\n \"useTestApp: no active app. Call useTestApp() at the top of a \" +\n \"describe block (not inside a test), and read `.current` from \" +\n \"within a test.\",\n );\n }\n return app;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,WACd,UAAmC,EAAE,EACnB;CAClB,IAAI;AAEJ,YAAW,YAAY;AACrB,QAAM,MAAM,cAAc,QAAQ;GAClC;AAEF,WAAU,YAAY;EACpB,MAAM,SAAS;AAGf,QAAM;AACN,QAAM,QAAQ,OAAO;GACrB;AAEF,QAAO,EACL,IAAI,UAAsB;AACxB,MAAI,CAAC,IACH,OAAM,IAAI,MACR,2IAGD;AAEH,SAAO;IAEV"}
|