@databricks/appkit 0.82.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +21 -1
- package/dist/agents/databricks.d.ts +15 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +25 -8
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +6 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/agent/eval.js +1 -1
- package/dist/cli/commands/generate-types.js +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +28 -15
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/add.js +3 -11
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js +1 -1
- package/dist/connectors/lakebase/index.js +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js +6 -12
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +25 -0
- package/dist/context/caller-context.d.ts.map +1 -0
- package/dist/context/caller-context.js +21 -0
- package/dist/context/caller-context.js.map +1 -0
- package/dist/context/deprecation.js +14 -0
- package/dist/context/deprecation.js.map +1 -0
- package/dist/context/execution-context.d.ts +40 -4
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +87 -33
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -1
- package/dist/context/request-scope.d.ts +2 -0
- package/dist/context/request-scope.js +39 -0
- package/dist/context/request-scope.js.map +1 -0
- package/dist/context/resource-capabilities.js +59 -0
- package/dist/context/resource-capabilities.js.map +1 -0
- package/dist/context/scoped-api.js +104 -0
- package/dist/context/scoped-api.js.map +1 -0
- package/dist/context/service-context.d.ts +18 -12
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +45 -55
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +16 -22
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +57 -3
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +5 -2
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts +19 -6
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js +53 -17
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/types.d.ts +18 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts +4 -3
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +34 -11
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/plugin-context.d.ts +14 -15
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +24 -14
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/base.d.ts +2 -2
- package/dist/errors/base.js +2 -2
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/identity-expired.d.ts +14 -0
- package/dist/errors/identity-expired.d.ts.map +1 -0
- package/dist/errors/identity-expired.js +33 -0
- package/dist/errors/identity-expired.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -2
- package/dist/logging/logger.js +1 -1
- package/dist/plugin/execution-result.d.ts +4 -1
- package/dist/plugin/execution-result.d.ts.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +6 -4
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +14 -22
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +42 -123
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +13 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +65 -15
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/auth-mode.js +42 -0
- package/dist/plugins/agents/auth-mode.js.map +1 -0
- package/dist/plugins/agents/index.d.ts +1 -1
- package/dist/plugins/agents/mlflow.js +11 -3
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/plugins/agents/tool-dispatch.js +9 -1
- package/dist/plugins/agents/tool-dispatch.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +4 -4
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +3 -3
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +9 -7
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/crud/contract.js +2 -2
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/database/database.d.ts +1 -1
- package/dist/plugins/files/plugin.d.ts +21 -15
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +50 -29
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +10 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +12 -3
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/genie/manifest.js +1 -0
- package/dist/plugins/jobs/plugin.d.ts +1 -1
- package/dist/plugins/jobs/plugin.js +2 -2
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +11 -18
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +16 -21
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js +1 -4
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/dev-obo-middleware.js +60 -0
- package/dist/plugins/server/dev-obo-middleware.js.map +1 -0
- package/dist/plugins/server/index.d.ts +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +3 -0
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/serving/manifest.js +1 -0
- package/dist/plugins/serving/serving.d.ts +1 -1
- package/dist/plugins/serving/serving.js +6 -6
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/resources/index.d.ts +1 -0
- package/dist/resources/index.js +3 -0
- package/dist/resources/warehouse.d.ts +16 -0
- package/dist/resources/warehouse.d.ts.map +1 -0
- package/dist/resources/warehouse.js +93 -0
- package/dist/resources/warehouse.js.map +1 -0
- package/dist/schemas/manifest.d.ts +44 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +122 -3
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/dev-obo.js +86 -0
- package/dist/shared/src/dev-obo.js.map +1 -0
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +12 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +17 -0
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +53 -3
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +5 -2
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/telemetry/execution-span-processor.js +21 -0
- package/dist/telemetry/execution-span-processor.js.map +1 -0
- package/dist/telemetry/telemetry-manager.js +2 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +2 -2
- package/dist/testing/create-test-app.js +2 -2
- package/dist/testing/create-test-app.js.map +1 -1
- package/dist/testing/fixtures.d.ts +3 -2
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +21 -14
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/reset-singletons.js +1 -1
- package/dist/testing/test-plugin-context.d.ts +3 -12
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +24 -16
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/dist/utils/is-plain-object.js +11 -0
- package/dist/utils/is-plain-object.js.map +1 -0
- package/docs/api/appkit/Class.AppKitError.md +2 -1
- package/docs/api/appkit/Class.AuthenticationError.md +1 -1
- package/docs/api/appkit/Class.ConfigurationError.md +1 -1
- package/docs/api/appkit/Class.ConnectionError.md +1 -1
- package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
- package/docs/api/appkit/Class.ExecutionError.md +1 -1
- package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
- package/docs/api/appkit/Class.InitializationError.md +1 -1
- package/docs/api/appkit/Class.Plugin.md +9 -13
- package/docs/api/appkit/Class.ServerError.md +1 -1
- package/docs/api/appkit/Class.ServiceContext.md +169 -0
- package/docs/api/appkit/Class.TunnelError.md +1 -1
- package/docs/api/appkit/Class.ValidationError.md +1 -1
- package/docs/api/appkit/Function.createApp.md +12 -12
- package/docs/api/appkit/Function.getCallerContext.md +12 -0
- package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
- package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
- package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
- package/docs/api/appkit/Function.getExecutionContext.md +5 -3
- package/docs/api/appkit/Function.getUserContext.md +16 -0
- package/docs/api/appkit/Function.getWarehouseId.md +20 -0
- package/docs/api/appkit/Function.isInUserContext.md +12 -0
- package/docs/api/appkit/Function.isUserContext.md +20 -0
- package/docs/api/appkit/Function.runAgent.md +1 -1
- package/docs/api/appkit/Function.runInCallerContext.md +27 -0
- package/docs/api/appkit/Function.runInUserContext.md +29 -0
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
- package/docs/api/appkit/Interface.CallerContext.md +54 -0
- package/docs/api/appkit/Interface.IndexConfig.md +1 -1
- package/docs/api/appkit/Interface.PluginManifest.md +33 -2
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
- package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
- package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
- package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +9 -0
- package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
- package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
- package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
- package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
- package/docs/api/appkit.md +20 -0
- package/docs/plugins/agents.md +45 -0
- package/docs/plugins/analytics.md +8 -0
- package/docs/plugins/execution-context.md +101 -50
- package/docs/plugins/lakebase.md +6 -82
- package/llms.txt +21 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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 { snapshotCallerContext } from \"../context/caller-context\";\nimport type { ServiceContextState } from \"../context/service-context\";\nimport { ServiceContext } from \"../context/service-context\";\nimport { AuthenticationError } from \"../errors\";\nimport {\n WarehouseResource,\n type WarehouseBinding,\n} from \"../resources/warehouse\";\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.createCallerContext` 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(client: Any, ids: { workspaceId: Any }) {\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 snapshotCallerContext({\n client,\n principal: { type: \"user\", userId, userName, 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 workspaceId: ids.workspaceId,\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 resources: WarehouseBinding,\n): ServiceContextState {\n return {\n client: (options.serviceDatabricksClient ||\n createMockWorkspaceClient()) as Any,\n serviceUserId: options.serviceUserId || \"test-service-user\",\n // Preserve the deprecated field for external test callers during migration.\n warehouseId: resources.warehouseId,\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 `createCallerContext` so code that resolves\n * the service principal or an on-behalf-of user context gets test doubles.\n * Also supplies the app-level warehouse binding through WarehouseResource.\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 resources = Object.freeze({\n warehouseId: Promise.resolve(options.warehouseId || \"test-warehouse-id\"),\n });\n const serviceContext = buildServiceContextState(options, resources);\n\n const resourcesSpy = vi\n .spyOn(WarehouseResource, \"get\")\n .mockReturnValue(resources);\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 // Keep the public spy handle name for existing tests. The deprecated factory\n // delegates to createCallerContext, so this spy covers both entry points.\n const createUserContextSpy = vi\n .spyOn(ServiceContext, \"createCallerContext\")\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 resourcesSpy.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":";;;;;;;;;;;;;;;;;;AA6BA,SAAgB,sBAAkC;CAChD,MAAM,WAAiB;EACrB,SAAS,GAAG,IAAI;EAChB,UAAU,GAAG,IAAI;EACjB,KAAK,GAAG,IAAI;EACZ,cAAc,GAAG,IAAI;EACrB,eAAe,GAAG,IAAI;EACtB,WAAW,GAAG,IAAI;EAClB,iBAAiB,GAAG,IAAI;EACxB,YAAY,GAAG,IAAI;EACnB,UAAU,GAAG,IAAI;EACjB,aAAa,GAAG,IAAI,CAAC,gBAAgB,MAAM;EAC3C,aAAa,GAAG,IAAI;EACrB;AAED,QAAO;EACL,WAAW,GAAG,IAAI,CAAC,gBAAgB,EACjC,iBAAiB,GAAG,IAAI,CAAC,oBAAoB,GAAG,SAAgB;GAC9D,MAAM,KAAK,KAAK,KAAK,SAAS;AAC9B,OAAI,OAAO,OAAO,WAChB,QAAO,GAAG,SAAS;IAGrB,EACH,CAAC;EACF,UAAU,GAAG,IAAI,CAAC,gBAAgB;GAChC,eAAe,GAAG,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,IAAI,EAAE,CAAC;GACxD,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,IAAI,EAAE,CAAC;GAC9D,CAAC;EACF,WAAW,GAAG,IAAI,CAAC,gBAAgB,EACjC,MAAM,GAAG,IAAI,EACd,CAAC;EACF,MAAM,GAAG,IAAI;EACb,iBAAiB,GACd,IAAI,CACJ,mBACC,OACE,OACA,UACA,IACA,mBACG;AACH,UAAO,MAAM,GAAG,SAAS;IAE5B;EACH,0BAA0B,GAAG,IAAI;EAClC;;;;;;AAOH,SAAgB,mBAId;CACA,MAAM,WAAgC,EAAE;AAoBxC,QAAO;EACL,QAnBiB;GACjB,KAAK,GAAG,IAAI,MAAc,YAAiB;AACzC,aAAS,OAAO,UAAU;KAC1B;GACF,MAAM,GAAG,IAAI,MAAc,YAAiB;AAC1C,aAAS,QAAQ,UAAU;KAC3B;GACF,KAAK,GAAG,IAAI,MAAc,YAAiB;AACzC,aAAS,OAAO,UAAU;KAC1B;GACF,QAAQ,GAAG,IAAI,MAAc,YAAiB;AAC5C,aAAS,UAAU,UAAU;KAC7B;GACF,OAAO,GAAG,IAAI,MAAc,YAAiB;AAC3C,aAAS,SAAS,UAAU;KAC5B;GACH;EAIC;EACA,aAAa,QAAgB,SAC3B,SAAS,GAAG,OAAO,aAAa,CAAC,GAAG;EACvC;;;;;;;;;;;;;;AAiCH,SAAgB,gBAAgB,QAAa,KAA2B;AACtE,SACE,OACA,QACA,UACA,cACQ;AAGR,MAAI,CAAC,MAAO,OAAM,oBAAoB,aAAa,aAAa;AAChE,SAAO,sBAAsB;GAC3B;GACA,WAAW;IAAE,MAAM;IAAQ;IAAQ;IAAU;IAAW;GAGxD,kBAAkB,WAAW,SAAS,CACnC,OAAO,MAAM,CACb,OAAO,MAAM,CACb,MAAM,GAAG,GAAG;GACf,aAAa,IAAI;GAClB,CAAC;;;;;;;;;;;AAYN,SAAgB,WACd,KACwB;CACxB,MAAM,OAAO,QAAQ,OAAO,EAAE,GAAG;CACjC,MAAM,UAAkC;EACtC,4BAA4B,KAAK,SAAS;EAC1C,oBAAoB,KAAK,UAAU;EACpC;AACD,KAAI,KAAK,MAAO,SAAQ,uBAAuB,KAAK;AACpD,QAAO;;;;;;;;;;;;;;;;;AAkBT,SAAgB,kBAAkB,YAAiB,EAAE,EAAE;CACrD,MAAM,EAAE,KAAK,SAAS,iBAAiB,GAAG,SAAS;CAQnD,MAAM,UAAmC,OAAO,YAC9C,OAAO,QAAQ;EACb,GAAI,MAAM,WAAW,IAAI,GAAG,EAAE;EAC9B,GAAG;EACJ,CAAC,CAAC,KAAK,CAAC,MAAM,WAAW,CAAC,KAAK,aAAa,EAAE,MAAM,CAAC,CACvD;AAcD,QAZY;EACV,QAAQ,EAAE;EACV,OAAO,EAAE;EACT,MAAM,EAAE;EACR,QAAQ,SAAU,MAAc;AAC9B,UAAO,KAAK,QAAQ,KAAK,aAAa;;EAIxC,GAAG;EACH;EACD;;;;;;;;;;;;;;;;;;AAoBH,SAAgB,qBAAqB;CACnC,MAAM,iBAAkE,EAAE;CAC1E,MAAM,SAAmB,EAAE;AA2E3B,QAzEY;EAIV,aAAa;EACb,QAAQ,GAAG,IAAI,CAAC,gBAAgB;EAChC,MAAM,GAAG,IAAI,CAAC,gBAAgB;EAC9B,MAAM,GAAG,GAAG,WAAqB;AAC/B,QAAK,cAAc;AACnB,UAAO;IACP;EACF,YAAY,GAAG,IAAI,CAAC,gBAAgB;EACpC,KAAK,GAAG,GAAG,SAAqB,OAAiB;AAG/C,OAAI,SAAS,QAAQ,OAAO,UAAU,WACpC,QAAO,KAAK,OAAO,MAAM,CAAC;AAE5B,QAAK,gBAAgB;AACrB,OAAI,eAAe,MACjB,MAAK,MAAM,WAAW,eAAe,MACnC,UAAS;AAGb,UAAO;IACP;EACF,OAAO,GAAG,GAAG,SAAqB,OAAiB;AACjD,QAAK,cAAc;AACnB,OAAI,SAAS,KAAM,QAAO,KAAK,OAAO,MAAM,CAAC;AAG7C,UAAO;IACP;EACF,WAAW,GAAG,GAAG,WAAqB;AACpC,QAAK,cAAc;AACnB,UAAO;IACP;EACF,cAAc,GAAG,IAAI,CAAC,gBAAgB;EACtC,SAAS,GAAG,IAAI,CAAC,gBAAgB;EACjC,IAAI,GAAG,GAAG,SAER,OACA,SACA;AACA,OAAI,CAAC,eAAe,OAClB,gBAAe,SAAS,EAAE;AAE5B,kBAAe,OAAO,KAAK,QAAQ;AACnC,UAAO;IACP;EACF,KAAK,GAAG,GAAG,SAET,OACA,SACA;AACA,OAAI,eAAe,OACjB,gBAAe,SAAS,eAAe,OAAO,QAC3C,MAAM,MAAM,QACd;AAEH,UAAO;IACP;EACF,eAAe;EAOf,cAAwB;AACtB,UAAO,IAAI,SAAS,OAAO,KAAK,GAAG,CAAC;;EAEvC;;;;;;AAQH,SAAgB,mBAAmB,YAAoC,EAAE,EAAE;AACzE,SAAQ,IAAI,kBAAkB;AAC9B,SAAQ,IAAI,0BAA0B;AACtC,QAAO,OAAO,QAAQ,KAAK,UAAU;;;;;;;;AASvC,SAAgB,SAAS,MAA0C;CACjE,MAAM,wBAAQ,IAAI,KAAiC;AACnD,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,CACjC,OAAM,IAAI,KAAK,QAAQ,IAAI,KAAK;AAElC,QAAO,OAAO,QAAQ,KAAK,KAAK;AAChC,cAAa;AACX,OAAK,MAAM,CAAC,KAAK,UAAU,MACzB,KAAI,UAAU,OACZ,QAAO,QAAQ,IAAI;MAEnB,SAAQ,IAAI,OAAO;;;;;;;AAU3B,SAAS,eAAe,SAA2B;AACjD,KAAI;AACF,WAAS;UACF,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BzB,SAAgB,QACd,MACA,IACgB;CAChB,MAAM,UAAU,SAAS,KAAK;CAE9B,IAAI;AACJ,KAAI;AACF,WAAS,IAAI;UACN,KAAK;AAEZ,iBAAe,QAAQ;AACvB,QAAM;;AAMR,KAAI,UAAU,QAAQ,OAAQ,OAAe,SAAS,WACpD,QAAQ,OAAsB,MAC3B,UAAU;AACT,WAAS;AACT,SAAO;KAER,QAAiB;AAChB,iBAAe,QAAQ;AACvB,QAAM;GAET;AAGH,UAAS;AACT,QAAO;;;;;;;;;;;;;;;;;;;;;AAsBT,eAAsB,iBAAgC;CACpD,IAAI;AACJ,KAAI;AACF,UAAQ,aAAa,iBAAiB;SAChC;AAEN;;AAEF,OAAM,MAAM,OAAO;;;;;;;AA0BrB,SAAS,yBACP,SACA,WACqB;AACrB,QAAO;EACL,QAAS,QAAQ,2BACf,2BAA2B;EAC7B,eAAe,QAAQ,iBAAiB;EAExC,aAAa,UAAU;EACvB,aAAa,QAAQ,QAAQ,QAAQ,eAAe,oBAAoB;EACzE;;;;;;;;;;;AAYH,SAAgB,mBAAmB,UAA8B,EAAE,EAAE;CACnE,MAAM,YAAY,OAAO,OAAO,EAC9B,aAAa,QAAQ,QAAQ,QAAQ,eAAe,oBAAoB,EACzE,CAAC;CACF,MAAM,iBAAiB,yBAAyB,SAAS,UAAU;CAEnE,MAAM,eAAe,GAClB,MAAM,mBAAmB,MAAM,CAC/B,gBAAgB,UAAU;CAE7B,MAAM,SAAS,GACZ,MAAM,gBAAgB,MAAM,CAC5B,gBAAgB,eAAe;CAElC,MAAM,UAAU,GACb,MAAM,gBAAgB,aAAa,CACnC,kBAAkB,eAAe;CAEpC,MAAM,mBAAmB,GACtB,MAAM,gBAAgB,gBAAgB,CACtC,gBAAgB,KAAK;CAIxB,MAAM,uBAAuB,GAC1B,MAAM,gBAAgB,sBAAsB,CAC5C,mBACC,gBACE,QAAQ,wBAAwB,2BAA2B,EAC3D,eACD,CACF;AAEH,QAAO;EACL;EACA;EACA;EACA;EACA;EACA,eAAe;AACb,UAAO,aAAa;AACpB,WAAQ,aAAa;AACrB,oBAAiB,aAAa;AAC9B,wBAAqB,aAAa;AAClC,gBAAa,aAAa;;EAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCH,SAAgB,sBAAsB,UAA8B,EAAE,EAEpE;CACA,IAAI;AAEJ,kBAAiB;AACf,WAAS,mBAAmB,QAAQ;GACpC;AAEF,iBAAgB;AACd,UAAQ,SAAS;AACjB,WAAS;GACT;AAEF,QAAO,EACL,IAAI,UAA8B;AAChC,MAAI,CAAC,OACH,OAAM,IAAI,MACR,kKAGD;AAEH,SAAO;IAEV;;;;;;AAOH,eAAsB,sBACpB,IACA,SACY;CACZ,MAAM,QAAQ,mBAAmB,QAAQ;AAEzC,KAAI;AACF,SAAO,MAAM,IAAI;WACT;AACR,QAAM,SAAS;;;;;;;AAQnB,SAAgB,4BACd,MACA,SACA;AACA,QAAO;EACL,QAAQ,EAAE,OAAO,aAAa;EAC9B,cAAc,QAAQ,KAAK,KAAK;EAChC,QAAQ,EACN,YAAY,MACb;EACD,UAAU,EACR,QAAQ,EACN,SAAS,QAAQ,KAAK,SAAS;GAC7B,MAAM,IAAI;GACV,WAAW,IAAI,aAAa;GAC7B,EAAE,EACJ,EACF;EACF;;;AAIH,SAAgB,wBAAwB,cAAsB;AAC5D,QAAO;EACL,QAAQ;GACN,OAAO;GACP,OAAO,EACL,SAAS,cACV;GACF;EACD,cAAc,QAAQ,KAAK,KAAK;EACjC;;;;;;;;;;;;;;;;;;;;;AAsBH,SAAgB,eAAe,SAIlB;AACX,QAAO,IAAI,SACT,QAAQ,SACR,QAAQ,WACR,QAAQ,YACR,QACA,EAAE,CACH"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ServiceContext } from "../context/service-context.js";
|
|
2
1
|
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { ServiceContext } from "../context/service-context.js";
|
|
3
3
|
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
4
4
|
import "../telemetry/index.js";
|
|
5
5
|
import { CacheManager } from "../cache/index.js";
|
|
@@ -69,17 +69,8 @@ interface RecordedToolCall {
|
|
|
69
69
|
/** The abort signal `executeTool` composed (timeout ∘ caller). */
|
|
70
70
|
signal?: AbortSignal;
|
|
71
71
|
/**
|
|
72
|
-
* Whether
|
|
73
|
-
*
|
|
74
|
-
* for a tool reached through `executeTool` this is `true` — and, because the
|
|
75
|
-
* fake `asUser` enforces the same token precondition as the real
|
|
76
|
-
* {@link Plugin.asUser}, a request with no `x-forwarded-access-token` makes
|
|
77
|
-
* that call **throw** rather than record `asUser: true`. The meaningful
|
|
78
|
-
* assertions are therefore: a well-formed request records `asUser: true`
|
|
79
|
-
* with {@link userId} set, and a token-less request rejects.
|
|
80
|
-
*
|
|
81
|
-
* The fake replicates the token precondition only, not the real dev-mode
|
|
82
|
-
* OTel `isDevOboFallback()` marker — assert OBO here, not via that flag.
|
|
72
|
+
* Whether dispatch ran in a caller scope, inherited or established from
|
|
73
|
+
* the forwarded request credentials.
|
|
83
74
|
*/
|
|
84
75
|
asUser: boolean;
|
|
85
76
|
/**
|
|
@@ -161,7 +152,7 @@ interface TestPluginContext {
|
|
|
161
152
|
* workspace, no OpenTelemetry pipeline, no network.
|
|
162
153
|
*
|
|
163
154
|
* The context is the *real* class, so route buffering, the tool registry,
|
|
164
|
-
* timeout composition, and
|
|
155
|
+
* timeout composition, and ambient caller inheritance all run for real.
|
|
165
156
|
* Only three edges are faked, matching the seams the class actually has:
|
|
166
157
|
*
|
|
167
158
|
* - **Telemetry** is a mock provider injected into the context (the one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-plugin-context.d.ts","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"test-plugin-context.d.ts","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"mappings":";;;;;;;;;;;;;;;AAe+C;;KAgB1C,aAAA,GACD,MAAA;;;AAcJ;;;;;KAAY,gBAAA,GACR,aAAA,KACE,IAAA,WAAe,MAAA,GAAS,WAAA;;;;;AAY9B;;;;;;KAAY,aAAA,GAAgB,MAAA,SAAe,MAAA,SAAe,gBAAA;;;;;;UAOzC,wBAAA;EAAA;;;;EAKf,SAAA,GAAY,MAAA;EAAA;;;;;EAMZ,GAAA,GAAM,MAAA;EASS;;;;EAJf,MAAA;AAAA;;UAIe,gBAAA;EAQN;EANT,MAAA;EAiBA;EAfA,IAAA;EAeM;EAbN,IAAA;EAiB4B;EAf5B,MAAA,GAAS,WAAA;EAwBuB;;;;EAnBhC,MAAA;EAmBkB;;;AAIpB;;EAjBE,MAAA;AAAA;;UAIe,aAAA;EACf,MAAA;EACA,IAAA;EAeO;;;AAOT;;;EAfE,QAAA,EAAU,OAAA,CAAQ,cAAA;AAAA;;UAIH,YAAA;EAgCQ;EA9BvB,cAAA,EAAgB,OAAA,CAAQ,OAAA;EAmC6B;EAjCrD,KAAA,EAAO,mBAAA;AAAA;;;;;UAOQ,iBAAA;EAEf;EAAA,GAAA,EAAK,aAAA;EAOL;;;;;;EAAA,SAAA,EAAW,UAAA;EAYA;;;;EAPX,SAAA,EAAW,gBAAA;EAY0C;;;;EAPrD,MAAA,EAAQ,aAAA;EAeyB;EAbjC,SAAA,EAAW,GAAA,SAAY,YAAA;EAac;;;;EARrC,gBAAA,CAAiB,IAAA,UAAc,KAAA,EAAO,MAAA,SAAe,gBAAA;EAwDvC;;;;;;;EAhDd,MAAA,WAAiB,MAAA,EAAQ,MAAA,EAAQ,CAAA,GAAI,OAAA,CAAQ,CAAA;EAiDtC;;;;;;;EAzCP,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCc,uBAAA,CACd,KAAA,GAAO,aAAA,EACP,OAAA,GAAU,wBAAA,GACT,iBAAA"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "../errors/index.js";
|
|
1
|
+
import { getCallerContext } from "../context/execution-context.js";
|
|
3
2
|
import { InMemoryStorage } from "../cache/storage/memory.js";
|
|
4
3
|
import "../cache/storage/index.js";
|
|
5
4
|
import { CacheManager } from "../cache/index.js";
|
|
5
|
+
import "../context/index.js";
|
|
6
|
+
import { createRequestScope } from "../context/request-scope.js";
|
|
6
7
|
import { PluginContext, isToolProvider } from "../core/plugin-context.js";
|
|
7
8
|
import { createMockWorkspaceClient } from "./mock-workspace-client.js";
|
|
8
|
-
import { applyEnv, createMockTelemetry, mockServiceContext } from "./fixtures.js";
|
|
9
|
+
import { applyEnv, createMockTelemetry, fakeUserContext, mockServiceContext } from "./fixtures.js";
|
|
9
10
|
import { afterEach, onTestFinished } from "vitest";
|
|
10
11
|
|
|
11
12
|
//#region src/testing/test-plugin-context.ts
|
|
@@ -14,7 +15,7 @@ import { afterEach, onTestFinished } from "vitest";
|
|
|
14
15
|
* workspace, no OpenTelemetry pipeline, no network.
|
|
15
16
|
*
|
|
16
17
|
* The context is the *real* class, so route buffering, the tool registry,
|
|
17
|
-
* timeout composition, and
|
|
18
|
+
* timeout composition, and ambient caller inheritance all run for real.
|
|
18
19
|
* Only three edges are faked, matching the seams the class actually has:
|
|
19
20
|
*
|
|
20
21
|
* - **Telemetry** is a mock provider injected into the context (the one
|
|
@@ -50,9 +51,13 @@ function createTestPluginContext(fakes = {}, options) {
|
|
|
50
51
|
if (!options) return createTestPluginContextSync(fakes);
|
|
51
52
|
return createTestPluginContextWithOptions(fakes, options);
|
|
52
53
|
}
|
|
53
|
-
function createTestPluginContextSync(fakes) {
|
|
54
|
+
function createTestPluginContextSync(fakes, client = createMockWorkspaceClient()) {
|
|
54
55
|
const telemetry = createMockTelemetry();
|
|
55
|
-
const
|
|
56
|
+
const createCallerContext = fakeUserContext(client, { workspaceId: Promise.resolve("test-workspace-id") });
|
|
57
|
+
const ctx = new PluginContext({
|
|
58
|
+
telemetry,
|
|
59
|
+
createCallerContext
|
|
60
|
+
});
|
|
56
61
|
const toolCalls = [];
|
|
57
62
|
const routes = [];
|
|
58
63
|
const providers = /* @__PURE__ */ new Map();
|
|
@@ -99,18 +104,20 @@ function createTestPluginContextSync(fakes) {
|
|
|
99
104
|
};
|
|
100
105
|
const base = {
|
|
101
106
|
getAgentTools: () => record.tools,
|
|
102
|
-
executeAgentTool: (toolName, args, signal) =>
|
|
107
|
+
executeAgentTool: (toolName, args, signal) => {
|
|
108
|
+
const caller = getCallerContext();
|
|
109
|
+
return resolve(toolName, args, signal, !!caller, caller?.principal.userId);
|
|
110
|
+
}
|
|
103
111
|
};
|
|
104
112
|
const asUser = (req) => {
|
|
105
113
|
record.asUserRequests.push(req);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (token && !userId && !isDev) throw AuthenticationError.missingUserId();
|
|
114
|
+
const scope = createRequestScope(req, [], {
|
|
115
|
+
createCaller: createCallerContext,
|
|
116
|
+
legacy: true
|
|
117
|
+
});
|
|
111
118
|
return {
|
|
112
119
|
...base,
|
|
113
|
-
executeAgentTool: (toolName, args, signal) =>
|
|
120
|
+
executeAgentTool: (toolName, args, signal) => scope.run(() => base.executeAgentTool(toolName, args, signal))
|
|
114
121
|
};
|
|
115
122
|
};
|
|
116
123
|
const provider = {
|
|
@@ -143,12 +150,13 @@ function createTestPluginContextSync(fakes) {
|
|
|
143
150
|
}
|
|
144
151
|
function createTestPluginContextWithOptions(fakes, options) {
|
|
145
152
|
const { responses = {}, env: envVars = {}, strict = false } = options;
|
|
146
|
-
const
|
|
153
|
+
const client = createMockWorkspaceClient({
|
|
147
154
|
responses,
|
|
148
155
|
strict
|
|
149
|
-
})
|
|
156
|
+
});
|
|
157
|
+
const serviceContextMock = mockServiceContext({ serviceDatabricksClient: client });
|
|
150
158
|
const restoreEnv = applyEnv(envVars);
|
|
151
|
-
const base = createTestPluginContextSync(fakes);
|
|
159
|
+
const base = createTestPluginContextSync(fakes, client);
|
|
152
160
|
let hasRestored = false;
|
|
153
161
|
const restore = () => {
|
|
154
162
|
if (hasRestored) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-plugin-context.js","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n AgentToolDefinition,\n BasePlugin,\n IAppRequest,\n ToolProvider,\n} from \"shared\";\nimport { afterEach, onTestFinished } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport { InMemoryStorage } from \"../cache/storage\";\nimport { isToolProvider, PluginContext } from \"../core/plugin-context\";\nimport { AuthenticationError } from \"../errors\";\nimport type { Plugin } from \"../plugin\";\nimport type { ITelemetry } from \"../telemetry\";\nimport { applyEnv, createMockTelemetry, mockServiceContext } from \"./fixtures\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\n\n/**\n * A concrete (non-function) fake tool response — returned as-is. Covers the\n * JSON-serializable shapes a tool call yields (rows, objects, primitives,\n * nullish). A bare `unknown` is intentionally not used here: unioned with the\n * function form below it would collapse to `unknown` and strip contextual\n * types from the callback's parameters.\n */\ntype FakeToolValue =\n | Record<string, unknown>\n | unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * A canned tool response. Either a static {@link FakeToolValue} returned\n * as-is, or a function of the call arguments (and the abort signal\n * `PluginContext.executeTool` composes) so a fake can assert on inputs or\n * simulate slow/aborting work. Returning a promise is supported (the return\n * type is intentionally `unknown`, which also covers `Promise<...>`).\n */\nexport type FakeToolResponse =\n | FakeToolValue\n | ((args: unknown, signal?: AbortSignal) => unknown);\n\n/**\n * Fake connector responses, keyed by plugin name and then tool name:\n *\n * ```ts\n * createTestPluginContext({ analytics: { query: fixtureRows } });\n * ```\n *\n * Each top-level key registers a fake {@link ToolProvider} under that plugin\n * name; each inner key becomes a tool that returns the mapped response.\n */\nexport type FakeProviders = Record<string, Record<string, FakeToolResponse>>;\n\n/**\n * Options for {@link createTestPluginContext} when called with a second parameter.\n * When provided, `createTestPluginContext` installs a service context seeded\n * from a mock workspace client, plus optional environment variables.\n */\nexport interface TestPluginContextOptions {\n /**\n * Responses keyed by dotted path (`\"jobs.getRun\"`) for the mocked workspace\n * client. Passed directly to {@link createMockWorkspaceClient}.\n */\n responses?: Record<string, unknown>;\n /**\n * Environment variables to set for the test. Captured on entry, restored\n * (or deleted if they were unset) on exit via an `afterEach` hook and/or\n * explicit {@link TestPluginContext.restore}.\n */\n env?: Record<string, string>;\n /**\n * If `true`, throw when a workspace client path with no declared response is\n * called, instead of resolving `undefined`. Defaults to `false` (never crash).\n */\n strict?: boolean;\n}\n\n/** A single dispatch observed by a fake provider. */\nexport interface RecordedToolCall {\n /** Registered plugin name (the key in {@link FakeProviders}). */\n plugin: string;\n /** Tool name passed to `executeAgentTool`. */\n tool: string;\n /** Arguments the tool received. */\n args: unknown;\n /** The abort signal `executeTool` composed (timeout ∘ caller). */\n signal?: AbortSignal;\n /**\n * Whether the dispatch was resolved through the on-behalf-of (`asUser`)\n * path. `PluginContext.executeTool` always calls `provider.asUser(req)`, so\n * for a tool reached through `executeTool` this is `true` — and, because the\n * fake `asUser` enforces the same token precondition as the real\n * {@link Plugin.asUser}, a request with no `x-forwarded-access-token` makes\n * that call **throw** rather than record `asUser: true`. The meaningful\n * assertions are therefore: a well-formed request records `asUser: true`\n * with {@link userId} set, and a token-less request rejects.\n *\n * The fake replicates the token precondition only, not the real dev-mode\n * OTel `isDevOboFallback()` marker — assert OBO here, not via that flag.\n */\n asUser: boolean;\n /**\n * The user the on-behalf-of scope resolved to (from `x-forwarded-user`), or\n * `undefined` for a service-principal call (`asUser: false`). Lets a test\n * assert the tool ran as the expected end user, not just that OBO was used.\n */\n userId?: string;\n}\n\n/** A single route registered through the context's `addRoute`/`addMiddleware`. */\nexport interface RecordedRoute {\n method: string;\n path: string;\n /**\n * The raw handlers as passed to `addRoute` — before `PluginContext` wraps\n * them with `forwardAsyncErrors`. Recorded here so aliasing assertions\n * (\"both routes mount the same handler\") can compare the original\n * references, which the wrapped express-level handlers no longer share.\n */\n handlers: express.RequestHandler[];\n}\n\n/** A fake tool provider registered on a mock context. */\nexport interface FakeProvider {\n /** Every `asUser(req)` the context resolved for this provider. */\n asUserRequests: express.Request[];\n /** Definitions returned from `getAgentTools()`. */\n tools: AgentToolDefinition[];\n}\n\n/**\n * The result of {@link createTestPluginContext}: the real `PluginContext` plus the\n * seams a test needs to drive and inspect it.\n */\nexport interface TestPluginContext {\n /** The real {@link PluginContext}, constructed with mock telemetry. */\n ctx: PluginContext;\n /**\n * The mock telemetry provider injected into the {@link PluginContext}.\n * Captures the spans the *context* opens (notably `executeTool`) — not the\n * plugin's own spans: `attachContext` rebuilds the plugin's `this.telemetry`\n * from the real `TelemetryManager`, so plugin-internal spans do not land here.\n */\n telemetry: ITelemetry;\n /**\n * Tool dispatches observed across all fake providers, in call order. Live —\n * read it after the action under test runs.\n */\n toolCalls: RecordedToolCall[];\n /**\n * Routes registered through the context, in registration order. Live —\n * populated when the plugin calls `addRoute`/`addMiddleware`.\n */\n routes: RecordedRoute[];\n /** Fake providers by plugin name, for direct assertions. */\n providers: Map<string, FakeProvider>;\n /**\n * Register (or replace) a fake tool provider after construction.\n * Same shape as one {@link FakeProviders} entry.\n */\n registerProvider(name: string, tools: Record<string, FakeToolResponse>): void;\n /**\n * Attach this context to a plugin the production way: seed an in-memory\n * cache (if AppKit hasn't already), then call `plugin.attachContext`, which\n * also rebuilds the plugin's telemetry and flips `isReady` to `true`. Await\n * it before exercising handlers that read `this.context`, `this.cache`, or\n * gate on `isReady`. Returns the same plugin for chaining.\n */\n attach<P extends Plugin>(plugin: P): Promise<P>;\n /**\n * Restore the service context and environment variables to their pre-test state.\n * Called automatically via `afterEach` when options were provided to\n * `createTestPluginContext`. Can also be called explicitly for escape hatches\n * (e.g., cleanup inside a test body). Idempotent — safe to call multiple times.\n * Only present if the context was created with options.\n */\n restore?: () => void;\n}\n\n/**\n * Build a real {@link PluginContext} with faked edges for testing — no live\n * workspace, no OpenTelemetry pipeline, no network.\n *\n * The context is the *real* class, so route buffering, the tool registry,\n * timeout composition, and the on-behalf-of (`asUser`) path all run for real.\n * Only three edges are faked, matching the seams the class actually has:\n *\n * - **Telemetry** is a mock provider injected into the context (the one\n * injectable production seam); it records the context's own spans, not the\n * plugin's.\n * - **Tool providers** are fakes registered through the existing public\n * `registerToolProvider`; their `asUser`/`executeAgentTool` are recorded.\n * - **Routes** are captured by wrapping the public `addRoute`/`addMiddleware`.\n *\n * Nothing about `PluginContext` is reimplemented.\n *\n * @param fakes - Canned tool responses keyed by plugin then tool name.\n * @param options - When provided, installs a mock workspace client seeded from\n * `responses`, mocks the service context, and sets `env`. Omit it for the\n * original behavior.\n *\n * @example\n * ```ts\n * // No options\n * const mock = createTestPluginContext({ analytics: { query: fixtureRows } });\n * await mock.attach(agentsPlugin);\n * // ...exercise a handler that dispatches analytics.query...\n * expect(mock.toolCalls[0]).toMatchObject({ plugin: \"analytics\", asUser: true });\n *\n * // With options — installs service context + seeded client\n * const mock = createTestPluginContext(\n * {},\n * { responses: { \"jobs.getRun\": { state: \"DONE\" } } },\n * );\n * ```\n */\nexport function createTestPluginContext(\n fakes: FakeProviders = {},\n options?: TestPluginContextOptions,\n): TestPluginContext {\n // No options: original behavior.\n if (!options) {\n return createTestPluginContextSync(fakes);\n }\n\n // Options provided: install the seeded client, service context, and scoped env.\n return createTestPluginContextWithOptions(fakes, options);\n}\n\nfunction createTestPluginContextSync(fakes: FakeProviders): TestPluginContext {\n const telemetry = createMockTelemetry();\n const ctx = new PluginContext({ telemetry });\n\n const toolCalls: RecordedToolCall[] = [];\n const routes: RecordedRoute[] = [];\n const providers = new Map<string, FakeProvider>();\n\n // Wrap the public route API so raw (pre-wrap) handlers are inspectable while\n // the real buffering/flush path stays intact.\n const realAddRoute = ctx.addRoute.bind(ctx);\n ctx.addRoute = (\n method: string,\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method, path, handlers });\n realAddRoute(method, path, ...handlers);\n };\n const realAddMiddleware = ctx.addMiddleware.bind(ctx);\n ctx.addMiddleware = (\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method: \"use\", path, handlers });\n realAddMiddleware(path, ...handlers);\n };\n\n function registerProvider(\n name: string,\n tools: Record<string, FakeToolResponse>,\n ): void {\n const record: FakeProvider = {\n asUserRequests: [],\n tools: Object.keys(tools).map((toolName) => ({\n name: toolName,\n description: `Fake tool ${name}.${toolName}`,\n parameters: { type: \"object\" },\n })),\n };\n providers.set(name, record);\n\n const resolve = async (\n toolName: string,\n args: unknown,\n signal: AbortSignal | undefined,\n asUser: boolean,\n userId: string | undefined,\n ): Promise<unknown> => {\n toolCalls.push({\n plugin: name,\n tool: toolName,\n args,\n signal,\n asUser,\n userId,\n });\n // `Object.hasOwn`, not `tools[toolName] === undefined`: a tool named\n // \"constructor\"/\"toString\"/etc. would otherwise resolve to an inherited\n // Object.prototype method and be invoked instead of reported missing.\n if (!Object.hasOwn(tools, toolName)) {\n throw new Error(\n `createTestPluginContext: plugin \"${name}\" has no fake tool \"${toolName}\". ` +\n `Available: ${Object.keys(tools).join(\", \") || \"(none)\"}`,\n );\n }\n const response = tools[toolName];\n return typeof response === \"function\"\n ? await (response as (a: unknown, s?: AbortSignal) => unknown)(\n args,\n signal,\n )\n : response;\n };\n\n const base: ToolProvider = {\n getAgentTools: () => record.tools,\n executeAgentTool: (toolName, args, signal) =>\n resolve(toolName, args, signal, false, undefined),\n };\n\n // Mirror the real `Plugin.asUser` token precondition (plugin.ts) so the\n // recorded `asUser` flag reflects genuine user-scope resolution rather than\n // being unconditionally true: a request with no `x-forwarded-access-token`\n // throws `missingToken` (production behavior), except in development where\n // the real code skips impersonation. This is edge-faking of asUser's\n // *contract*, not a reimplementation of `runInUserContext`/`ServiceContext`.\n //\n // Deliberately NOT reproduced: the real dev-mode path sets an OTel\n // `DEV_OBO_FALLBACK_KEY` marker (read by `isDevOboFallback()`). That key is\n // module-private telemetry plumbing; assert OBO via the recorded\n // `asUser`/`userId` fields, not `isDevOboFallback()`.\n const asUser = (req: IAppRequest): ToolProvider => {\n record.asUserRequests.push(req as express.Request);\n const token = (req as express.Request)\n .header?.(\"x-forwarded-access-token\")\n ?.trim();\n const userId = (req as express.Request)\n .header?.(\"x-forwarded-user\")\n ?.trim();\n const isDev = process.env.NODE_ENV === \"development\";\n\n if (!token && !isDev) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n if (token && !userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n return {\n ...base,\n executeAgentTool: (toolName, args, signal) =>\n resolve(toolName, args, signal, true, userId),\n };\n };\n\n // `registerToolProvider` expects the full ToolProviderPlugin shape\n // (BasePlugin & ToolProvider & { asUser }). executeTool only ever calls\n // `asUser` and `executeAgentTool`; the remaining BasePlugin surface is\n // never touched for a registered provider, so a focused fake plus a cast\n // is sufficient and avoids reimplementing a plugin.\n const provider = {\n name,\n setup: async () => {},\n injectRoutes: () => {},\n getEndpoints: () => ({}),\n ...base,\n asUser,\n } as unknown as BasePlugin &\n ToolProvider & {\n asUser: (req: IAppRequest) => ToolProvider;\n };\n\n ctx.registerToolProvider(name, provider);\n }\n\n for (const [name, tools] of Object.entries(fakes)) {\n registerProvider(name, tools);\n }\n\n async function attach<P extends Plugin>(plugin: P): Promise<P> {\n // Seed a real in-memory cache if AppKit hasn't initialized one. Idempotent:\n // getInstance returns any existing singleton (e.g. one a suite already set\n // up) and ignores the storage argument in that case.\n if (!cacheReady()) {\n await CacheManager.getInstance({ storage: new InMemoryStorage({}) });\n }\n plugin.attachContext({ context: ctx });\n\n // Mirror what AppKit core does after attachContext (core/appkit.ts): put\n // the plugin in the registry so `getPlugins()`/`getPluginNames()`/\n // `hasPlugin()` and any sibling-plugin lookup behave as in production. Only\n // register it as a tool provider when it actually is one AND its name does\n // not collide with an injected fake — the fakes are the authored test\n // doubles and must not be overwritten by the plugin under test.\n ctx.registerPlugin(plugin.name, plugin as unknown as BasePlugin);\n if (isToolProvider(plugin) && !providers.has(plugin.name)) {\n ctx.registerToolProvider(\n plugin.name,\n plugin as unknown as Parameters<typeof ctx.registerToolProvider>[1],\n );\n }\n return plugin;\n }\n\n return {\n ctx,\n telemetry,\n toolCalls,\n routes,\n providers,\n registerProvider,\n attach,\n };\n}\n\nfunction createTestPluginContextWithOptions(\n fakes: FakeProviders,\n options: TestPluginContextOptions,\n): TestPluginContext {\n const { responses = {}, env: envVars = {}, strict = false } = options;\n\n // Build a mock workspace client seeded from responses\n const client = createMockWorkspaceClient({\n responses,\n strict,\n });\n\n // Install the mock service context with the seeded client\n const serviceContextMock = mockServiceContext({\n serviceDatabricksClient: client,\n });\n\n // Set env (captured for restore) via the shared helper.\n const restoreEnv = applyEnv(envVars);\n\n // Create the base context (without options this time, since we're handling everything)\n const base = createTestPluginContextSync(fakes);\n\n // Restore function: restores env and service context (idempotent)\n let hasRestored = false;\n const restore = () => {\n if (hasRestored) return;\n hasRestored = true;\n restoreEnv();\n serviceContextMock.restore();\n };\n\n // Auto-restore after the current test. This helper is documented and used\n // from inside a test body, where a runtime-registered `afterEach` does NOT run\n // for that test (Vitest only collects `afterEach` before the test runs) — the\n // reason the old `afterEach` here silently leaked. `onTestFinished` is the hook\n // built for runtime registration and fires after the creating test. If called\n // at collection scope instead, it throws, so fall back to `afterEach` there.\n try {\n onTestFinished(() => {\n restore();\n });\n } catch {\n afterEach(() => {\n restore();\n });\n }\n\n // Return the context with the restore method\n return {\n ...base,\n restore,\n };\n}\n\nfunction cacheReady(): boolean {\n try {\n CacheManager.getInstanceSync();\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"test-plugin-context.js","names":[],"sources":["../../src/testing/test-plugin-context.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n AgentToolDefinition,\n BasePlugin,\n IAppRequest,\n ToolProvider,\n} from \"shared\";\nimport { afterEach, onTestFinished } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport { InMemoryStorage } from \"../cache/storage\";\nimport { getCallerContext } from \"../context\";\nimport { createRequestScope } from \"../context/request-scope\";\nimport { isToolProvider, PluginContext } from \"../core/plugin-context\";\nimport type { Plugin } from \"../plugin\";\nimport type { ITelemetry } from \"../telemetry\";\nimport {\n applyEnv,\n createMockTelemetry,\n fakeUserContext,\n mockServiceContext,\n} from \"./fixtures\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\n\n/**\n * A concrete (non-function) fake tool response — returned as-is. Covers the\n * JSON-serializable shapes a tool call yields (rows, objects, primitives,\n * nullish). A bare `unknown` is intentionally not used here: unioned with the\n * function form below it would collapse to `unknown` and strip contextual\n * types from the callback's parameters.\n */\ntype FakeToolValue =\n | Record<string, unknown>\n | unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * A canned tool response. Either a static {@link FakeToolValue} returned\n * as-is, or a function of the call arguments (and the abort signal\n * `PluginContext.executeTool` composes) so a fake can assert on inputs or\n * simulate slow/aborting work. Returning a promise is supported (the return\n * type is intentionally `unknown`, which also covers `Promise<...>`).\n */\nexport type FakeToolResponse =\n | FakeToolValue\n | ((args: unknown, signal?: AbortSignal) => unknown);\n\n/**\n * Fake connector responses, keyed by plugin name and then tool name:\n *\n * ```ts\n * createTestPluginContext({ analytics: { query: fixtureRows } });\n * ```\n *\n * Each top-level key registers a fake {@link ToolProvider} under that plugin\n * name; each inner key becomes a tool that returns the mapped response.\n */\nexport type FakeProviders = Record<string, Record<string, FakeToolResponse>>;\n\n/**\n * Options for {@link createTestPluginContext} when called with a second parameter.\n * When provided, `createTestPluginContext` installs a service context seeded\n * from a mock workspace client, plus optional environment variables.\n */\nexport interface TestPluginContextOptions {\n /**\n * Responses keyed by dotted path (`\"jobs.getRun\"`) for the mocked workspace\n * client. Passed directly to {@link createMockWorkspaceClient}.\n */\n responses?: Record<string, unknown>;\n /**\n * Environment variables to set for the test. Captured on entry, restored\n * (or deleted if they were unset) on exit via an `afterEach` hook and/or\n * explicit {@link TestPluginContext.restore}.\n */\n env?: Record<string, string>;\n /**\n * If `true`, throw when a workspace client path with no declared response is\n * called, instead of resolving `undefined`. Defaults to `false` (never crash).\n */\n strict?: boolean;\n}\n\n/** A single dispatch observed by a fake provider. */\nexport interface RecordedToolCall {\n /** Registered plugin name (the key in {@link FakeProviders}). */\n plugin: string;\n /** Tool name passed to `executeAgentTool`. */\n tool: string;\n /** Arguments the tool received. */\n args: unknown;\n /** The abort signal `executeTool` composed (timeout ∘ caller). */\n signal?: AbortSignal;\n /**\n * Whether dispatch ran in a caller scope, inherited or established from\n * the forwarded request credentials.\n */\n asUser: boolean;\n /**\n * The user the on-behalf-of scope resolved to (from `x-forwarded-user`), or\n * `undefined` for a service-principal call (`asUser: false`). Lets a test\n * assert the tool ran as the expected end user, not just that OBO was used.\n */\n userId?: string;\n}\n\n/** A single route registered through the context's `addRoute`/`addMiddleware`. */\nexport interface RecordedRoute {\n method: string;\n path: string;\n /**\n * The raw handlers as passed to `addRoute` — before `PluginContext` wraps\n * them with `forwardAsyncErrors`. Recorded here so aliasing assertions\n * (\"both routes mount the same handler\") can compare the original\n * references, which the wrapped express-level handlers no longer share.\n */\n handlers: express.RequestHandler[];\n}\n\n/** A fake tool provider registered on a mock context. */\nexport interface FakeProvider {\n /** Every `asUser(req)` the context resolved for this provider. */\n asUserRequests: express.Request[];\n /** Definitions returned from `getAgentTools()`. */\n tools: AgentToolDefinition[];\n}\n\n/**\n * The result of {@link createTestPluginContext}: the real `PluginContext` plus the\n * seams a test needs to drive and inspect it.\n */\nexport interface TestPluginContext {\n /** The real {@link PluginContext}, constructed with mock telemetry. */\n ctx: PluginContext;\n /**\n * The mock telemetry provider injected into the {@link PluginContext}.\n * Captures the spans the *context* opens (notably `executeTool`) — not the\n * plugin's own spans: `attachContext` rebuilds the plugin's `this.telemetry`\n * from the real `TelemetryManager`, so plugin-internal spans do not land here.\n */\n telemetry: ITelemetry;\n /**\n * Tool dispatches observed across all fake providers, in call order. Live —\n * read it after the action under test runs.\n */\n toolCalls: RecordedToolCall[];\n /**\n * Routes registered through the context, in registration order. Live —\n * populated when the plugin calls `addRoute`/`addMiddleware`.\n */\n routes: RecordedRoute[];\n /** Fake providers by plugin name, for direct assertions. */\n providers: Map<string, FakeProvider>;\n /**\n * Register (or replace) a fake tool provider after construction.\n * Same shape as one {@link FakeProviders} entry.\n */\n registerProvider(name: string, tools: Record<string, FakeToolResponse>): void;\n /**\n * Attach this context to a plugin the production way: seed an in-memory\n * cache (if AppKit hasn't already), then call `plugin.attachContext`, which\n * also rebuilds the plugin's telemetry and flips `isReady` to `true`. Await\n * it before exercising handlers that read `this.context`, `this.cache`, or\n * gate on `isReady`. Returns the same plugin for chaining.\n */\n attach<P extends Plugin>(plugin: P): Promise<P>;\n /**\n * Restore the service context and environment variables to their pre-test state.\n * Called automatically via `afterEach` when options were provided to\n * `createTestPluginContext`. Can also be called explicitly for escape hatches\n * (e.g., cleanup inside a test body). Idempotent — safe to call multiple times.\n * Only present if the context was created with options.\n */\n restore?: () => void;\n}\n\n/**\n * Build a real {@link PluginContext} with faked edges for testing — no live\n * workspace, no OpenTelemetry pipeline, no network.\n *\n * The context is the *real* class, so route buffering, the tool registry,\n * timeout composition, and ambient caller inheritance all run for real.\n * Only three edges are faked, matching the seams the class actually has:\n *\n * - **Telemetry** is a mock provider injected into the context (the one\n * injectable production seam); it records the context's own spans, not the\n * plugin's.\n * - **Tool providers** are fakes registered through the existing public\n * `registerToolProvider`; their `asUser`/`executeAgentTool` are recorded.\n * - **Routes** are captured by wrapping the public `addRoute`/`addMiddleware`.\n *\n * Nothing about `PluginContext` is reimplemented.\n *\n * @param fakes - Canned tool responses keyed by plugin then tool name.\n * @param options - When provided, installs a mock workspace client seeded from\n * `responses`, mocks the service context, and sets `env`. Omit it for the\n * original behavior.\n *\n * @example\n * ```ts\n * // No options\n * const mock = createTestPluginContext({ analytics: { query: fixtureRows } });\n * await mock.attach(agentsPlugin);\n * // ...exercise a handler that dispatches analytics.query...\n * expect(mock.toolCalls[0]).toMatchObject({ plugin: \"analytics\", asUser: true });\n *\n * // With options — installs service context + seeded client\n * const mock = createTestPluginContext(\n * {},\n * { responses: { \"jobs.getRun\": { state: \"DONE\" } } },\n * );\n * ```\n */\nexport function createTestPluginContext(\n fakes: FakeProviders = {},\n options?: TestPluginContextOptions,\n): TestPluginContext {\n // No options: original behavior.\n if (!options) {\n return createTestPluginContextSync(fakes);\n }\n\n // Options provided: install the seeded client, service context, and scoped env.\n return createTestPluginContextWithOptions(fakes, options);\n}\n\nfunction createTestPluginContextSync(\n fakes: FakeProviders,\n client = createMockWorkspaceClient(),\n): TestPluginContext {\n const telemetry = createMockTelemetry();\n const createCallerContext = fakeUserContext(client, {\n workspaceId: Promise.resolve(\"test-workspace-id\"),\n });\n const ctx = new PluginContext({ telemetry, createCallerContext });\n\n const toolCalls: RecordedToolCall[] = [];\n const routes: RecordedRoute[] = [];\n const providers = new Map<string, FakeProvider>();\n\n // Wrap the public route API so raw (pre-wrap) handlers are inspectable while\n // the real buffering/flush path stays intact.\n const realAddRoute = ctx.addRoute.bind(ctx);\n ctx.addRoute = (\n method: string,\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method, path, handlers });\n realAddRoute(method, path, ...handlers);\n };\n const realAddMiddleware = ctx.addMiddleware.bind(ctx);\n ctx.addMiddleware = (\n path: string,\n ...handlers: express.RequestHandler[]\n ): void => {\n routes.push({ method: \"use\", path, handlers });\n realAddMiddleware(path, ...handlers);\n };\n\n function registerProvider(\n name: string,\n tools: Record<string, FakeToolResponse>,\n ): void {\n const record: FakeProvider = {\n asUserRequests: [],\n tools: Object.keys(tools).map((toolName) => ({\n name: toolName,\n description: `Fake tool ${name}.${toolName}`,\n parameters: { type: \"object\" },\n })),\n };\n providers.set(name, record);\n\n const resolve = async (\n toolName: string,\n args: unknown,\n signal: AbortSignal | undefined,\n asUser: boolean,\n userId: string | undefined,\n ): Promise<unknown> => {\n toolCalls.push({\n plugin: name,\n tool: toolName,\n args,\n signal,\n asUser,\n userId,\n });\n // `Object.hasOwn`, not `tools[toolName] === undefined`: a tool named\n // \"constructor\"/\"toString\"/etc. would otherwise resolve to an inherited\n // Object.prototype method and be invoked instead of reported missing.\n if (!Object.hasOwn(tools, toolName)) {\n throw new Error(\n `createTestPluginContext: plugin \"${name}\" has no fake tool \"${toolName}\". ` +\n `Available: ${Object.keys(tools).join(\", \") || \"(none)\"}`,\n );\n }\n const response = tools[toolName];\n return typeof response === \"function\"\n ? await (response as (a: unknown, s?: AbortSignal) => unknown)(\n args,\n signal,\n )\n : response;\n };\n\n const base: ToolProvider = {\n getAgentTools: () => record.tools,\n executeAgentTool: (toolName, args, signal) => {\n const caller = getCallerContext();\n return resolve(\n toolName,\n args,\n signal,\n !!caller,\n caller?.principal.userId,\n );\n },\n };\n\n // Reuse production header validation and ALS. Only the client is faked.\n const asUser = (req: IAppRequest): ToolProvider => {\n record.asUserRequests.push(req as express.Request);\n const scope = createRequestScope(req as express.Request, [], {\n createCaller: createCallerContext,\n legacy: true,\n });\n\n return {\n ...base,\n executeAgentTool: (toolName, args, signal) =>\n scope.run(() => base.executeAgentTool(toolName, args, signal)),\n };\n };\n\n // executeTool calls executeAgentTool under the ambient caller scope.\n // Keep asUser for older tests that call the provider directly.\n // The remaining BasePlugin surface is\n // never touched for a registered provider, so a focused fake plus a cast\n // is sufficient and avoids reimplementing a plugin.\n const provider = {\n name,\n setup: async () => {},\n injectRoutes: () => {},\n getEndpoints: () => ({}),\n ...base,\n asUser,\n } as unknown as BasePlugin &\n ToolProvider & {\n asUser: (req: IAppRequest) => ToolProvider;\n };\n\n ctx.registerToolProvider(name, provider);\n }\n\n for (const [name, tools] of Object.entries(fakes)) {\n registerProvider(name, tools);\n }\n\n async function attach<P extends Plugin>(plugin: P): Promise<P> {\n // Seed a real in-memory cache if AppKit hasn't initialized one. Idempotent:\n // getInstance returns any existing singleton (e.g. one a suite already set\n // up) and ignores the storage argument in that case.\n if (!cacheReady()) {\n await CacheManager.getInstance({ storage: new InMemoryStorage({}) });\n }\n plugin.attachContext({ context: ctx });\n\n // Mirror what AppKit core does after attachContext (core/appkit.ts): put\n // the plugin in the registry so `getPlugins()`/`getPluginNames()`/\n // `hasPlugin()` and any sibling-plugin lookup behave as in production. Only\n // register it as a tool provider when it actually is one AND its name does\n // not collide with an injected fake — the fakes are the authored test\n // doubles and must not be overwritten by the plugin under test.\n ctx.registerPlugin(plugin.name, plugin as unknown as BasePlugin);\n if (isToolProvider(plugin) && !providers.has(plugin.name)) {\n ctx.registerToolProvider(\n plugin.name,\n plugin as unknown as Parameters<typeof ctx.registerToolProvider>[1],\n );\n }\n return plugin;\n }\n\n return {\n ctx,\n telemetry,\n toolCalls,\n routes,\n providers,\n registerProvider,\n attach,\n };\n}\n\nfunction createTestPluginContextWithOptions(\n fakes: FakeProviders,\n options: TestPluginContextOptions,\n): TestPluginContext {\n const { responses = {}, env: envVars = {}, strict = false } = options;\n\n // Build a mock workspace client seeded from responses\n const client = createMockWorkspaceClient({\n responses,\n strict,\n });\n\n // Install the mock service context with the seeded client\n const serviceContextMock = mockServiceContext({\n serviceDatabricksClient: client,\n });\n\n // Set env (captured for restore) via the shared helper.\n const restoreEnv = applyEnv(envVars);\n\n // Create the base context (without options this time, since we're handling everything)\n const base = createTestPluginContextSync(fakes, client);\n\n // Restore function: restores env and service context (idempotent)\n let hasRestored = false;\n const restore = () => {\n if (hasRestored) return;\n hasRestored = true;\n restoreEnv();\n serviceContextMock.restore();\n };\n\n // Auto-restore after the current test. This helper is documented and used\n // from inside a test body, where a runtime-registered `afterEach` does NOT run\n // for that test (Vitest only collects `afterEach` before the test runs) — the\n // reason the old `afterEach` here silently leaked. `onTestFinished` is the hook\n // built for runtime registration and fires after the creating test. If called\n // at collection scope instead, it throws, so fall back to `afterEach` there.\n try {\n onTestFinished(() => {\n restore();\n });\n } catch {\n afterEach(() => {\n restore();\n });\n }\n\n // Return the context with the restore method\n return {\n ...base,\n restore,\n };\n}\n\nfunction cacheReady(): boolean {\n try {\n CacheManager.getInstanceSync();\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/utils/is-plain-object.ts
|
|
2
|
+
/** Test for an object whose direct prototype is Object.prototype or null. */
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5
|
+
const prototype = Object.getPrototypeOf(value);
|
|
6
|
+
return prototype === Object.prototype || prototype === null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { isPlainObject };
|
|
11
|
+
//# sourceMappingURL=is-plain-object.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-plain-object.js","names":[],"sources":["../../src/utils/is-plain-object.ts"],"sourcesContent":["/** Test for an object whose direct prototype is Object.prototype or null. */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n"],"mappings":";;AACA,SAAgB,cACd,OACkC;AAClC,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,YAAY,OAAO,eAAe,MAAM;AAC9C,QAAO,cAAc,OAAO,aAAa,cAAc"}
|
|
@@ -32,6 +32,7 @@ console.error(error.toJSON()); // Safe for logging, sensitive values redacted
|
|
|
32
32
|
* [`ConnectionError`](./docs/api/appkit/Class.ConnectionError.md)
|
|
33
33
|
* [`DatabaseValidationError`](./docs/api/appkit/Class.DatabaseValidationError.md)
|
|
34
34
|
* [`ExecutionError`](./docs/api/appkit/Class.ExecutionError.md)
|
|
35
|
+
* [`IdentityExpiredError`](./docs/api/appkit/Class.IdentityExpiredError.md)
|
|
35
36
|
* [`InitializationError`](./docs/api/appkit/Class.InitializationError.md)
|
|
36
37
|
* [`ServerError`](./docs/api/appkit/Class.ServerError.md)
|
|
37
38
|
* [`TunnelError`](./docs/api/appkit/Class.TunnelError.md)
|
|
@@ -80,7 +81,7 @@ protected readonly optional _clientMessage: string;
|
|
|
80
81
|
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message
|
|
84
|
+
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message`. `message` may contain raw upstream / SDK text including statement fragments, internal object names, and correlation IDs.
|
|
84
85
|
|
|
85
86
|
Subclasses can set this in their constructor for a fixed sanitized string. When unset, `clientMessage` defaults to a generic per-code string (see the getter), and the raw `message` is kept server-side only.
|
|
86
87
|
|
|
@@ -54,7 +54,7 @@ protected readonly optional _clientMessage: string;
|
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message
|
|
57
|
+
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message`. `message` may contain raw upstream / SDK text including statement fragments, internal object names, and correlation IDs.
|
|
58
58
|
|
|
59
59
|
Subclasses can set this in their constructor for a fixed sanitized string. When unset, `clientMessage` defaults to a generic per-code string (see the getter), and the raw `message` is kept server-side only.
|
|
60
60
|
|
|
@@ -54,7 +54,7 @@ protected readonly optional _clientMessage: string;
|
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message
|
|
57
|
+
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message`. `message` may contain raw upstream / SDK text including statement fragments, internal object names, and correlation IDs.
|
|
58
58
|
|
|
59
59
|
Subclasses can set this in their constructor for a fixed sanitized string. When unset, `clientMessage` defaults to a generic per-code string (see the getter), and the raw `message` is kept server-side only.
|
|
60
60
|
|
|
@@ -54,7 +54,7 @@ protected readonly optional _clientMessage: string;
|
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message
|
|
57
|
+
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message`. `message` may contain raw upstream / SDK text including statement fragments, internal object names, and correlation IDs.
|
|
58
58
|
|
|
59
59
|
Subclasses can set this in their constructor for a fixed sanitized string. When unset, `clientMessage` defaults to a generic per-code string (see the getter), and the raw `message` is kept server-side only.
|
|
60
60
|
|
|
@@ -39,7 +39,7 @@ protected readonly optional _clientMessage: string;
|
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message
|
|
42
|
+
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message`. `message` may contain raw upstream / SDK text including statement fragments, internal object names, and correlation IDs.
|
|
43
43
|
|
|
44
44
|
Subclasses can set this in their constructor for a fixed sanitized string. When unset, `clientMessage` defaults to a generic per-code string (see the getter), and the raw `message` is kept server-side only.
|
|
45
45
|
|
|
@@ -56,7 +56,7 @@ protected readonly optional _clientMessage: string;
|
|
|
56
56
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message
|
|
59
|
+
Client-safe error message. When set, callers serializing the error to a client (SSE, HTTP body) MUST prefer `clientMessage` over `message`. `message` may contain raw upstream / SDK text including statement fragments, internal object names, and correlation IDs.
|
|
60
60
|
|
|
61
61
|
Subclasses can set this in their constructor for a fixed sanitized string. When unset, `clientMessage` defaults to a generic per-code string (see the getter), and the raw `message` is kept server-side only.
|
|
62
62
|
|