@dbx-tools/appkit-mastra 0.3.36 → 0.3.39

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/README.md CHANGED
@@ -388,6 +388,33 @@ plugin.mastra({
388
388
  Use `mcp: false` to disable MCP. Turn on `tools: true` only for ambient tools
389
389
  that are safe outside an in-process chat turn.
390
390
 
391
+ ## Driving A Turn From Outside The Routes
392
+
393
+ Another plugin (or a scheduled job) can run an agent turn directly, but a raw
394
+ `agent.generate(prompt)` loses everything the HTTP middleware stamps - most
395
+ visibly the AppKit user, which every user-scoped tool reads. `ask_genie` then
396
+ fails with "invoke the tool from an agent turn served by the mastra plugin", so
397
+ the turn answers "the data source is unreachable" where the chat routes answer
398
+ with real data.
399
+
400
+ `exports().createRequestContext()` builds the missing context:
401
+
402
+ ```ts
403
+ const mastra = context.getPlugins()?.get("mastra")?.exports();
404
+ const requestContext = await mastra.createRequestContext({
405
+ threadId: conversationId,
406
+ resourceId: userId,
407
+ });
408
+ const result = await mastra.getDefault().generate(prompt, { requestContext });
409
+ ```
410
+
411
+ The AppKit user, the memory thread / resource pair, and a request id (so the
412
+ turn's spans join up in traces) are stamped exactly as the request middleware
413
+ stamps them. Call it inside an `asUser(req)` scope to inherit the caller's OBO
414
+ identity; outside one it resolves to the service principal.
415
+ [`@dbx-tools/teams`](../teams) uses this so a Teams card turn has the same tool
416
+ reach - and therefore the same answer - as a chat turn.
417
+
391
418
  ## API Gate
392
419
 
393
420
  The stock `@mastra/express` app has broad management routes. The plugin's
@@ -468,7 +495,9 @@ client that talks to these routes.
468
495
  defaults, and approval-gated tool inspection.
469
496
  - `config` - plugin config types and RequestContext key constants.
470
497
  - `model` / `serving` / `servingSanitize` - Mastra model config, request
471
- overrides, serving-endpoint config, and request-body cleanup.
498
+ overrides, serving-endpoint config, and the on-the-wire request/response
499
+ cleanup that keeps provider-specific payload quirks (Claude's replayed
500
+ thinking blocks, Gemini's content-parts responses) from failing a turn.
472
501
  - `genie` - Genie prompt, space normalization, Genie toolkits, and suggestions.
473
502
  - `chart` / `statement` / `writer` - chart cache, statement row fetches, and
474
503
  safe writer events.
package/package.json CHANGED
@@ -28,27 +28,31 @@
28
28
  "express": "^5.1.0",
29
29
  "pg": "^8.22.0",
30
30
  "zod": "4.3.6",
31
- "@dbx-tools/appkit": "0.3.36",
32
- "@dbx-tools/core": "0.3.36",
33
- "@dbx-tools/genie": "0.3.36",
34
- "@dbx-tools/model": "0.3.36",
35
- "@dbx-tools/shared-core": "0.3.36",
36
- "@dbx-tools/shared-genie": "0.3.36",
37
- "@dbx-tools/shared-mastra": "0.3.36",
38
- "@dbx-tools/shared-model": "0.3.36"
31
+ "@dbx-tools/appkit": "0.3.39",
32
+ "@dbx-tools/genie": "0.3.39",
33
+ "@dbx-tools/core": "0.3.39",
34
+ "@dbx-tools/shared-core": "0.3.39",
35
+ "@dbx-tools/shared-genie": "0.3.39",
36
+ "@dbx-tools/model": "0.3.39",
37
+ "@dbx-tools/shared-mastra": "0.3.39",
38
+ "@dbx-tools/shared-model": "0.3.39"
39
39
  },
40
40
  "main": "index.ts",
41
41
  "license": "UNLICENSED",
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "version": "0.3.36",
45
+ "version": "0.3.39",
46
46
  "types": "index.ts",
47
47
  "type": "module",
48
48
  "exports": {
49
49
  ".": "./index.ts",
50
50
  "./package.json": "./package.json"
51
51
  },
52
+ "files": [
53
+ "index.ts",
54
+ "src"
55
+ ],
52
56
  "dbxToolsConfig": {
53
57
  "tags": [
54
58
  "node"
package/src/model.ts CHANGED
@@ -32,7 +32,7 @@ import type { RequestContext } from "@mastra/core/request-context";
32
32
 
33
33
  import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config";
34
34
  import { MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving";
35
- import { rewriteServingBody } from "./serving-sanitize";
35
+ import { rewriteServingBody, rewriteServingResponseBody } from "./serving-sanitize";
36
36
 
37
37
  type ModelClass = model.ModelClass;
38
38
  const { parseModelClass } = classes;
@@ -140,6 +140,10 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
140
140
  * 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
141
141
  * 4xx debugging doesn't have to fight AI SDK's `[Array]`
142
142
  * formatter.
143
+ * 3. Repairs the non-streaming JSON response, where Databricks-hosted
144
+ * Gemini returns `choices[].message.content` as a parts array that
145
+ * the AI SDK's OpenAI schema rejects (see
146
+ * {@link rewriteServingResponseBody}).
143
147
  *
144
148
  * Safe to call from any hot path: {@link functionModule.memoize} ensures
145
149
  * the wrapper is installed at most once per process, so subsequent
@@ -169,6 +173,38 @@ const setupFetchInterceptor = functionModule.memoize((): void => {
169
173
  ? { url: url.toString(), bodyType: "non-JSON" }
170
174
  : { url: url.toString(), body: parsed },
171
175
  );
172
- return original(input, init);
176
+ const response = await original(input, init);
177
+ return repairServingResponse(response);
173
178
  }) as typeof globalThis.fetch;
174
179
  });
180
+
181
+ /**
182
+ * Rewrite a non-streaming serving response whose body needs repair, leaving
183
+ * everything else byte-identical.
184
+ *
185
+ * Streaming turns are passed straight through: an SSE body must stay a live
186
+ * stream (buffering it to a string would defeat streaming and break
187
+ * `text/event-stream` parsing), and the delta frames already carry string
188
+ * content, so they never hit the array-shaped `content` bug. Likewise a
189
+ * non-JSON or error body is returned untouched, so the caller still sees the
190
+ * original status and headers.
191
+ */
192
+ async function repairServingResponse(response: Response): Promise<Response> {
193
+ const contentType = response.headers.get("content-type") ?? "";
194
+ if (!contentType.includes("application/json")) return response;
195
+
196
+ const body = await response.clone().text();
197
+ const rewritten = rewriteServingResponseBody(body);
198
+ if (rewritten === body) return response;
199
+
200
+ // `content-length` / `content-encoding` describe the ORIGINAL bytes, so they
201
+ // are dropped: the rewritten body is a different length and already decoded.
202
+ const headers = new Headers(response.headers);
203
+ headers.delete("content-length");
204
+ headers.delete("content-encoding");
205
+ return new Response(rewritten, {
206
+ status: response.status,
207
+ statusText: response.statusText,
208
+ headers,
209
+ });
210
+ }
package/src/plugin.ts CHANGED
@@ -70,6 +70,7 @@ import {
70
70
  import { display, type ServingEndpointSummary } from "@dbx-tools/shared-model";
71
71
  import type { Agent } from "@mastra/core/agent";
72
72
  import { Mastra } from "@mastra/core/mastra";
73
+ import type { RequestContext } from "@mastra/core/request-context";
73
74
  import express from "express";
74
75
  import type { Pool } from "pg";
75
76
 
@@ -89,7 +90,12 @@ import { buildMcpServer, type ResolvedMcp } from "./mcp";
89
90
  import { createMemoryBuilder, createServicePrincipalPool, needsLakebase } from "./memory";
90
91
  import { logFeedback, resolveFeedbackEnabled } from "./mlflow";
91
92
  import { buildObservability } from "./observability";
92
- import { attachRoutePatchMiddleware, isMastraRequestAllowed, MastraServer } from "./server";
93
+ import {
94
+ attachRoutePatchMiddleware,
95
+ createRequestContext,
96
+ isMastraRequestAllowed,
97
+ MastraServer,
98
+ } from "./server";
93
99
  import { resolveServingConfig } from "./serving";
94
100
  import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement";
95
101
  import { threadsRoute } from "./threads";
@@ -303,6 +309,24 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
303
309
  (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
304
310
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
305
311
  getMastra: () => this.mastra,
312
+ /**
313
+ * Build the `RequestContext` an agent turn driven from OUTSIDE this
314
+ * plugin's routes needs (a Teams activity on another plugin's endpoint, a
315
+ * scheduled job).
316
+ *
317
+ * Mastra's user-scoped tools - `ask_genie` most visibly - read the AppKit
318
+ * user off the request context, which only the HTTP middleware stamps.
319
+ * Calling `agent.generate` with a raw prompt therefore answers "the data
320
+ * source is unreachable" where the chat routes answer with real data.
321
+ * Passing this as `requestContext` closes that gap, so an out-of-band turn
322
+ * has exactly the capabilities a chat turn has.
323
+ *
324
+ * Must be called INSIDE an `asUser(req)` scope to inherit the caller's
325
+ * OBO identity; outside one it resolves to the service principal.
326
+ */
327
+ createRequestContext: (
328
+ options: { threadId?: string; resourceId?: string; requestId?: string } = {},
329
+ ): Promise<RequestContext> => createRequestContext(options),
306
330
  /**
307
331
  * MCP endpoint info when `config.mcp` is enabled, else `null`.
308
332
  * Streamable HTTP is `http`; the SSE pair is the legacy transport.
package/src/server.ts CHANGED
@@ -13,7 +13,7 @@ import { feedback, thread } from "@dbx-tools/shared-mastra";
13
13
  import {
14
14
  MASTRA_RESOURCE_ID_KEY,
15
15
  MASTRA_THREAD_ID_KEY,
16
- type RequestContext,
16
+ RequestContext,
17
17
  } from "@mastra/core/request-context";
18
18
  import { MastraServer as MastraServerExpress } from "@mastra/express";
19
19
  import { trace } from "@opentelemetry/api";
@@ -40,6 +40,78 @@ import { extractModelOverride, MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig }
40
40
  */
41
41
  const INVALID_TRACE_ID = "0".repeat(32);
42
42
 
43
+ /**
44
+ * Stamp the AppKit user (plus the resource id and trace metadata) onto
45
+ * `requestContext`.
46
+ *
47
+ * Split out of {@link MastraServer} because the HTTP middleware is not the only
48
+ * caller that needs it: an out-of-band turn (a Teams activity arriving on
49
+ * another plugin's route, a scheduled job) drives `agent.generate` directly, and
50
+ * without this stamp every user-scoped tool - `ask_genie` above all - fails with
51
+ * "invoke the tool from an agent turn served by the mastra plugin". Those
52
+ * callers build a context with {@link createRequestContext} instead of a
53
+ * request.
54
+ *
55
+ * Idempotent: returns immediately when the user and resource id are already
56
+ * present, so the middleware can call it over a context another layer stamped.
57
+ */
58
+ export async function stampRequestContextUser(requestContext: RequestContext): Promise<void> {
59
+ if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
60
+ const executionContext = getExecutionContext();
61
+ const user: User = {
62
+ id: executionContextUserId(executionContext),
63
+ executionContext,
64
+ };
65
+ requestContext.set(MASTRA_USER_KEY, user);
66
+ requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
67
+ // AppKit's `UserContext` surfaces display name / email only on
68
+ // OBO requests. Service-context calls (background tasks, server
69
+ // start-up) leave these undefined and we skip the stamp so
70
+ // downstream trace metadata stays absent rather than empty.
71
+ let userName: string | undefined;
72
+ let email: string | undefined;
73
+ if ("isUserContext" in executionContext) {
74
+ userName = executionContext.userName;
75
+ email = executionContext.userEmail;
76
+ } else if (process.env.NODE_ENV === "development") {
77
+ const currentUser = await executionContext.client.currentUser.me();
78
+ userName = currentUser?.userName;
79
+ email = currentUser?.emails?.filter((email) => email.primary).find((email) => email.value)
80
+ ?.value as string;
81
+ }
82
+ if (userName) {
83
+ requestContext.set(MASTRA_USER_NAME_KEY, userName);
84
+ }
85
+ if (email) {
86
+ requestContext.set(MASTRA_USER_EMAIL_KEY, email);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Build the `RequestContext` a NON-HTTP agent turn needs.
92
+ *
93
+ * Everything Mastra's tools read off the context is stamped the same way the
94
+ * request middleware stamps it - the AppKit user (so `ask_genie` and the model
95
+ * resolver can mint user-scoped tokens), the memory thread / resource pair, and
96
+ * a request id so the turn's spans join up in traces. The result is passed as
97
+ * `requestContext` to `agent.generate` / `agent.stream`.
98
+ *
99
+ * This is what makes a card turn answer with the SAME data a chat turn does: the
100
+ * difference between the two endpoints is presentation, not capability.
101
+ */
102
+ export async function createRequestContext(
103
+ options: { threadId?: string; resourceId?: string; requestId?: string } = {},
104
+ ): Promise<RequestContext> {
105
+ const requestContext = new RequestContext();
106
+ await stampRequestContextUser(requestContext);
107
+ if (options.threadId) requestContext.set(MASTRA_THREAD_ID_KEY, options.threadId);
108
+ // Stamped AFTER the user so an explicit resource id (a channel member, say)
109
+ // wins over the ambient identity `stampRequestContextUser` defaulted to.
110
+ if (options.resourceId) requestContext.set(MASTRA_RESOURCE_ID_KEY, options.resourceId);
111
+ requestContext.set(MASTRA_REQUEST_ID_KEY, options.requestId ?? hash.id());
112
+ return requestContext;
113
+ }
114
+
43
115
  /**
44
116
  * `@mastra/express` subclass that stamps `RequestContext` with the
45
117
  * AppKit user, resource id, and a thread id backed by an HTTP-only
@@ -92,35 +164,7 @@ export class MastraServer extends MastraServerExpress {
92
164
  }
93
165
 
94
166
  async configureRequestContextUser(requestContext: RequestContext) {
95
- if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
96
- const executionContext = getExecutionContext();
97
- const user: User = {
98
- id: executionContextUserId(executionContext),
99
- executionContext,
100
- };
101
- requestContext.set(MASTRA_USER_KEY, user);
102
- requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
103
- // AppKit's `UserContext` surfaces display name / email only on
104
- // OBO requests. Service-context calls (background tasks, server
105
- // start-up) leave these undefined and we skip the stamp so
106
- // downstream trace metadata stays absent rather than empty.
107
- let userName: string | undefined;
108
- let email: string | undefined;
109
- if ("isUserContext" in executionContext) {
110
- userName = executionContext.userName;
111
- email = executionContext.userEmail;
112
- } else if (process.env.NODE_ENV === "development") {
113
- const currentUser = await executionContext.client.currentUser.me();
114
- userName = currentUser?.userName;
115
- email = currentUser?.emails?.filter((email) => email.primary).find((email) => email.value)
116
- ?.value as string;
117
- }
118
- if (userName) {
119
- requestContext.set(MASTRA_USER_NAME_KEY, userName);
120
- }
121
- if (email) {
122
- requestContext.set(MASTRA_USER_EMAIL_KEY, email);
123
- }
167
+ await stampRequestContextUser(requestContext);
124
168
  }
125
169
 
126
170
  /**
@@ -1,14 +1,20 @@
1
1
  /**
2
- * Repairs Mastra / AI SDK message replays sent to Databricks Model
3
- * Serving before they hit the OpenAI-compatible `/chat/completions`
4
- * route.
2
+ * Repairs traffic between Mastra / the AI SDK and Databricks Model Serving's
3
+ * OpenAI-compatible `/chat/completions` route, in both directions.
5
4
  *
6
- * Exists because the transcript Mastra persists is not always a transcript the
7
- * provider will accept back: Databricks-hosted Claude rejects replayed
8
- * extended-thinking blocks and reads a trailing assistant message as a prefill
9
- * request. Both repairs are provider quirks, not schema violations, so they are
10
- * applied on the wire (see the `globalThis.fetch` wrapper in `model.ts`) rather
11
- * than by changing what the agent stores or what the UI shows.
5
+ * Outbound ({@link rewriteServingBody}), because the transcript Mastra
6
+ * persists is not always a transcript the provider will accept back:
7
+ * Databricks-hosted Claude rejects replayed extended-thinking blocks and reads
8
+ * a trailing assistant message as a prefill request.
9
+ *
10
+ * Inbound ({@link rewriteServingResponseBody}), because Databricks-hosted
11
+ * Gemini answers with its native content-parts array where the OpenAI contract
12
+ * (and therefore the AI SDK's response schema) requires a plain string.
13
+ *
14
+ * Every repair here is a provider quirk rather than a schema violation, so all
15
+ * of them are applied on the wire (see the `globalThis.fetch` wrapper in
16
+ * `model.ts`) rather than by changing what the agent stores or what the UI
17
+ * shows.
12
18
  *
13
19
  * @module
14
20
  */
@@ -186,3 +192,56 @@ function isEmptyServingContent(content: ServingChatMessage["content"]): boolean
186
192
  return false;
187
193
  });
188
194
  }
195
+
196
+ /**
197
+ * Parse, sanitize, and re-serialize a `/serving-endpoints/...` non-streaming
198
+ * JSON RESPONSE body. Returns the original string verbatim when the body is
199
+ * not JSON or no rewrite was needed, mirroring
200
+ * {@link rewriteServingBody} on the request side.
201
+ */
202
+ export function rewriteServingResponseBody(body: string): string {
203
+ const parsed = json.parseRecord(body);
204
+ if (!parsed) return body;
205
+ return flattenChoiceMessageContent(parsed) ? JSON.stringify(parsed) : body;
206
+ }
207
+
208
+ /**
209
+ * Collapse a structured `choices[].message.content` array to the plain string
210
+ * the OpenAI Chat Completions contract specifies.
211
+ *
212
+ * Databricks-hosted Gemini answers a non-streaming `/chat/completions` call
213
+ * with the Gemini-native parts shape:
214
+ *
215
+ * ```json
216
+ * "content": [{ "type": "text", "text": "...", "thoughtSignature": "..." }]
217
+ * ```
218
+ *
219
+ * `@ai-sdk/openai-compatible` validates the response against the OpenAI
220
+ * schema, where `content` is a nullable string, so it throws
221
+ * `Type validation failed: ... expected string, received array` and the whole
222
+ * call fails (`AI_APICallError: Invalid JSON response` on an HTTP 200). Mastra
223
+ * uses `doGenerate` for its side calls, so the visible symptom is
224
+ * `Error generating title` - every thread keeps its placeholder name.
225
+ *
226
+ * Flattening on the wire keeps the repair in one place: the streaming path is
227
+ * unaffected (deltas already carry string content), and neither the agent's
228
+ * stored transcript nor the UI has to know the provider emitted parts. Any
229
+ * non-text part (a `thoughtSignature`-only entry, an inline image) contributes
230
+ * nothing, matching {@link openaiChat.chatContentToText}, and an all-parts-empty
231
+ * message flattens to `""` rather than being dropped, so `finish_reason` and
232
+ * `usage` still round-trip.
233
+ */
234
+ export function flattenChoiceMessageContent(payload: Record<string, unknown>): boolean {
235
+ if (!Array.isArray(payload.choices)) return false;
236
+ let changed = false;
237
+ for (const choice of payload.choices) {
238
+ if (!choice || typeof choice !== "object") continue;
239
+ const message = (choice as { message?: unknown }).message;
240
+ if (!message || typeof message !== "object") continue;
241
+ const target = message as { content?: unknown };
242
+ if (!Array.isArray(target.content)) continue;
243
+ target.content = openaiChat.chatContentToText(target.content, { types: ["text"] });
244
+ changed = true;
245
+ }
246
+ return changed;
247
+ }
@@ -1,63 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { z } from "zod";
4
-
5
- import { chartPlanSchema } from "../src/chart";
6
-
7
- /**
8
- * Walk every nested schema node, yielding each object so a test can
9
- * assert on keywords wherever they appear in the tree.
10
- */
11
- function* nodes(schema: unknown): Generator<Record<string, unknown>> {
12
- if (Array.isArray(schema)) {
13
- for (const item of schema) yield* nodes(item);
14
- return;
15
- }
16
- if (typeof schema !== "object" || schema === null) return;
17
- const record = schema as Record<string, unknown>;
18
- yield record;
19
- for (const value of Object.values(record)) yield* nodes(value);
20
- }
21
-
22
- describe("chart plan JSON schema", () => {
23
- // Databricks' Gemini serving endpoints validate `response_json_schema`
24
- // and reject JSON Schema 2020-12 `prefixItems` with "schema at
25
- // properties.series.items.properties.data.items.anyOf.2.items must be a
26
- // boolean or an object". Zod emits `prefixItems` for `z.tuple`, so the
27
- // scatter `[x, y]` point must stay a length-constrained number array.
28
- it("emits no prefixItems, so Gemini accepts it as a response schema", () => {
29
- const jsonSchema = z.toJSONSchema(chartPlanSchema);
30
- const offenders = [...nodes(jsonSchema)].filter((node) => "prefixItems" in node);
31
- assert.deepEqual(offenders, []);
32
- });
33
-
34
- it("types the scatter point as a two-number array", () => {
35
- const jsonSchema = z.toJSONSchema(chartPlanSchema) as Record<string, any>;
36
- const variants = jsonSchema.properties.series.items.properties.data.items.anyOf;
37
- const arrayVariant = variants.find((v: Record<string, unknown>) => v.type === "array");
38
- assert.deepEqual(arrayVariant, {
39
- type: "array",
40
- items: { type: "number" },
41
- minItems: 2,
42
- maxItems: 2,
43
- });
44
- });
45
- });
46
-
47
- describe("chart data point coercion", () => {
48
- const dataPoint = chartPlanSchema.shape.series.element.shape.data.element;
49
-
50
- it("keeps the shapes a SQL row set produces", () => {
51
- assert.equal(dataPoint.parse(12.5), 12.5);
52
- assert.equal(dataPoint.parse("12.5"), 12.5);
53
- assert.equal(dataPoint.parse(null), null);
54
- assert.deepEqual(dataPoint.parse([1, 2]), [1, 2]);
55
- assert.deepEqual(dataPoint.parse({ name: "ATL", value: 3 }), { name: "ATL", value: 3 });
56
- });
57
-
58
- it("degrades unusable values to null instead of failing the plan", () => {
59
- assert.equal(dataPoint.parse("not a number"), null);
60
- assert.equal(dataPoint.parse(Number.POSITIVE_INFINITY), null);
61
- assert.equal(dataPoint.parse({ nope: true }), null);
62
- });
63
- });
@@ -1,64 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { ConfigurationError } from "@databricks/appkit";
4
-
5
- import { MASTRA_CONFIG_SCHEMA } from "../src/config";
6
- import { normalizeGenieSpaces } from "../src/genie";
7
- import { invalidFields } from "../src/validation";
8
-
9
- describe("mastra config schema", () => {
10
- it("describes every published property", () => {
11
- const properties = Object.entries(MASTRA_CONFIG_SCHEMA.properties ?? {});
12
- assert.ok(properties.length > 0);
13
- for (const [name, schema] of properties) {
14
- assert.equal(typeof schema, "object", `${name} must be a schema object`);
15
- const description = (schema as { description?: unknown }).description;
16
- assert.equal(typeof description, "string", `${name} must carry a description`);
17
- assert.ok((description as string).length > 0, `${name} description must be non-empty`);
18
- }
19
- });
20
- });
21
-
22
- describe("genie space normalization", () => {
23
- it("wraps bare space ids and passes objects through", () => {
24
- assert.deepEqual(normalizeGenieSpaces({ default: "01ef", sales: { spaceId: "02ef" } }), {
25
- default: { spaceId: "01ef" },
26
- sales: { spaceId: "02ef" },
27
- });
28
- });
29
-
30
- it("treats no config as no spaces", () => {
31
- assert.deepEqual(normalizeGenieSpaces(undefined), {});
32
- assert.deepEqual(normalizeGenieSpaces({}), {});
33
- });
34
-
35
- it("fails loudly on an alias with no space id", () => {
36
- for (const spaces of [
37
- { default: undefined },
38
- { default: "" },
39
- { sales: { spaceId: "" } },
40
- ] as Parameters<typeof normalizeGenieSpaces>[0][]) {
41
- assert.throws(() => normalizeGenieSpaces(spaces), ConfigurationError);
42
- }
43
- });
44
-
45
- it("names the env var only for the default alias", () => {
46
- assert.throws(() => normalizeGenieSpaces({ default: undefined }), /DATABRICKS_GENIE_SPACE_ID/);
47
- assert.throws(() => normalizeGenieSpaces({ sales: undefined }), /genieSpaces\.sales/);
48
- });
49
- });
50
-
51
- describe("request body validation", () => {
52
- it("reports distinct dot-joined field paths", () => {
53
- assert.deepEqual(
54
- invalidFields({
55
- issues: [{ path: ["traceId"] }, { path: ["value", 0] }, { path: ["traceId"] }],
56
- }),
57
- ["traceId", "value.0"],
58
- );
59
- });
60
-
61
- it("drops the root path, which names no field", () => {
62
- assert.deepEqual(invalidFields({ issues: [{ path: [] }] }), []);
63
- });
64
- });
@@ -1,14 +0,0 @@
1
- // ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
2
- {
3
- "extends": "../tsconfig.json",
4
- "compilerOptions": {
5
- "noEmit": true,
6
- "rootDir": ".."
7
- },
8
- "include": [
9
- "**/*.ts"
10
- ],
11
- "exclude": [
12
- "node_modules"
13
- ]
14
- }
package/tsconfig.json DELETED
@@ -1,41 +0,0 @@
1
- // ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
2
- {
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "lib",
6
- "alwaysStrict": true,
7
- "declaration": true,
8
- "esModuleInterop": true,
9
- "experimentalDecorators": true,
10
- "inlineSourceMap": true,
11
- "inlineSources": true,
12
- "lib": [
13
- "ES2022"
14
- ],
15
- "module": "ESNext",
16
- "noEmitOnError": false,
17
- "noFallthroughCasesInSwitch": true,
18
- "noImplicitAny": true,
19
- "noImplicitReturns": true,
20
- "noImplicitThis": true,
21
- "noUnusedLocals": true,
22
- "noUnusedParameters": true,
23
- "resolveJsonModule": true,
24
- "strict": true,
25
- "strictNullChecks": true,
26
- "strictPropertyInitialization": true,
27
- "stripInternal": true,
28
- "target": "ES2022",
29
- "types": [
30
- "node"
31
- ],
32
- "moduleResolution": "bundler",
33
- "skipLibCheck": true
34
- },
35
- "include": [
36
- "src/**/*.ts"
37
- ],
38
- "exclude": [
39
- "node_modules"
40
- ]
41
- }