@malloy-publisher/server 0.0.227 → 0.0.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.docker.md +8 -8
  2. package/dist/app/assets/{EnvironmentPage-DvOJ7L_b.js → EnvironmentPage-EW2lbGvb.js} +1 -1
  3. package/dist/app/assets/{HomePage-CXguJsXS.js → HomePage-Bkwc9Woc.js} +1 -1
  4. package/dist/app/assets/{LightMode-ZsshUznu.js → LightMode-Bum_KBpN.js} +1 -1
  5. package/dist/app/assets/{MainPage-BIe0VwBa.js → MainPage-oiEy7TNM.js} +1 -1
  6. package/dist/app/assets/{MaterializationsPage-BuZ6UJVx.js → MaterializationsPage-C_VJsTgU.js} +1 -1
  7. package/dist/app/assets/{ModelPage-DsPf-s8B.js → ModelPage-z8REqAmk.js} +1 -1
  8. package/dist/app/assets/{PackagePage-CEVNAKZa.js → PackagePage-C2Vtt1Ln.js} +1 -1
  9. package/dist/app/assets/{RouteError-Chn7lL96.js → RouteError-DmJLpLXm.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-DWC_FdNU.js → ThemeEditorPage-BywFjC7A.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-CGrsFz8p.js → WorkbookPage-DCMizDMR.js} +1 -1
  12. package/dist/app/assets/{core-vVgoO8IR.es-BD_THWs_.js → core-CEDZMHV1.es-_yGzNgNH.js} +1 -1
  13. package/dist/app/assets/{index-D6YtyiJ0.js → index-CE9xhdra.js} +1 -1
  14. package/dist/app/assets/{index-BioohWQj.js → index-CdmFub34.js} +1 -1
  15. package/dist/app/assets/{index-gEWxu09x.js → index-DDMrjIT3.js} +1 -1
  16. package/dist/app/assets/{index-DNUZpnaa.js → index-EqslXZ44.js} +4 -4
  17. package/dist/app/index.html +1 -1
  18. package/dist/default-publisher.config.json +7 -7
  19. package/dist/runtime/publisher.js +5 -0
  20. package/dist/server.mjs +841 -963
  21. package/package.json +1 -1
  22. package/publisher.config.example.bigquery.json +7 -7
  23. package/publisher.config.example.duckdb.json +7 -7
  24. package/publisher.config.json +7 -11
  25. package/src/config.spec.ts +2 -2
  26. package/src/controller/package.controller.ts +62 -31
  27. package/src/default-publisher.config.json +7 -7
  28. package/src/mcp/handler_utils.spec.ts +108 -0
  29. package/src/mcp/handler_utils.ts +98 -4
  30. package/src/mcp/server.protocol.spec.ts +58 -0
  31. package/src/mcp/server.ts +29 -1
  32. package/src/mcp/skills/skills_bundle.json +1 -1
  33. package/src/mcp/tools/compile_tool.spec.ts +207 -0
  34. package/src/mcp/tools/compile_tool.ts +177 -0
  35. package/src/mcp/tools/execute_query_tool.spec.ts +143 -0
  36. package/src/mcp/tools/execute_query_tool.ts +37 -4
  37. package/src/mcp/tools/get_context_tool.ts +1 -1
  38. package/src/mcp/tools/reload_package_tool.spec.ts +232 -0
  39. package/src/mcp/tools/reload_package_tool.ts +158 -0
  40. package/src/runtime/publisher.js +5 -0
  41. package/src/server.ts +7 -5
  42. package/src/service/environment.ts +70 -6
  43. package/src/service/model.spec.ts +92 -0
  44. package/src/service/model.ts +58 -7
  45. package/src/service/package.ts +31 -13
  46. package/src/service/package_reload_safety.spec.ts +193 -0
  47. package/tests/fixtures/query-givens/data/orders.csv +7 -0
  48. package/tests/fixtures/query-givens/model.malloy +34 -0
  49. package/tests/fixtures/query-givens/publisher.json +5 -0
  50. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +22 -22
  51. package/tests/integration/query_givens/query_givens.integration.spec.ts +146 -0
  52. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +121 -0
  53. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +110 -0
@@ -1147,6 +1147,7 @@ export class Model {
1147
1147
  `Model compilation failed: ${this.compilationError.message}`,
1148
1148
  );
1149
1149
  }
1150
+
1150
1151
  let runnable: QueryMaterializer;
1151
1152
  if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
1152
1153
  throw new BadRequestError("Model has no queryable entities.");
@@ -1309,16 +1310,23 @@ export class Model {
1309
1310
  // (for the row limit) and the run so a stale persist source falls back per
1310
1311
  // its declared policy — and prep/run agree on the same substitution.
1311
1312
  const buildManifest = this.resolveFreshBuildManifest();
1312
- const rowLimit = resolveModelQueryRowLimit(
1313
- (await runnable.getPreparedResult({ givens, buildManifest }))
1314
- .resultExplore.limit,
1315
- { defaultLimit: getDefaultQueryRowLimit(), maxRows },
1316
- );
1317
- const endTime = performance.now();
1318
- const executionTime = endTime - startTime;
1319
1313
 
1314
+ // Prepare INSIDE the run try/catch: a bad-given / value-type throw at
1315
+ // prepare time (getPreparedResult binds the givens) gets the same
1316
+ // MalloyError→rethrow / else→400 handling as run, instead of escaping as
1317
+ // a 500. `executionTime` is still captured after prepare and before run,
1318
+ // preserving the pre-existing timing recorded by the success histogram.
1319
+ let rowLimit = 0;
1320
+ let executionTime = 0;
1320
1321
  let queryResults;
1321
1322
  try {
1323
+ rowLimit = resolveModelQueryRowLimit(
1324
+ (await runnable.getPreparedResult({ givens, buildManifest }))
1325
+ .resultExplore.limit,
1326
+ { defaultLimit: getDefaultQueryRowLimit(), maxRows },
1327
+ );
1328
+ executionTime = performance.now() - startTime;
1329
+
1322
1330
  queryResults = await runnable.run({
1323
1331
  rowLimit,
1324
1332
  givens,
@@ -1337,6 +1345,31 @@ export class Model {
1337
1345
  "malloy.model.query.status": "error",
1338
1346
  });
1339
1347
 
1348
+ // Bad client-supplied givens (unknown name, wrong-typed value, an
1349
+ // operator-finalized override, ...) all surface as a Malloy
1350
+ // `runtime-given-*` error. Malloy is the single validator; the publisher
1351
+ // just maps its rejection to a clean 400. Duck-type on `.code`
1352
+ // (MalloyCompileError extends Error, not MalloyError, and isn't
1353
+ // root-exported). The `runtime-given-` prefix is a pinned coupling to
1354
+ // Malloy's error codes (@malloydata/malloy given_binding.ts / runtime.ts);
1355
+ // if they're renamed upstream, update it here (and in environment.ts) —
1356
+ // otherwise these fall through to the generic 400 below with a worse
1357
+ // message, and the /compile path silently omits `sql`.
1358
+ const givenCode = (error as { code?: string })?.code;
1359
+ if (
1360
+ typeof givenCode === "string" &&
1361
+ givenCode.startsWith("runtime-given-")
1362
+ ) {
1363
+ logger.debug("Rejected client-supplied given", {
1364
+ environmentName: this.packageName,
1365
+ modelPath: this.modelPath,
1366
+ error: error instanceof Error ? error.message : String(error),
1367
+ });
1368
+ throw new BadRequestError(
1369
+ error instanceof Error ? error.message : String(error),
1370
+ );
1371
+ }
1372
+
1340
1373
  // Re-throw Malloy errors as-is (they will be handled by error handler)
1341
1374
  if (error instanceof MalloyError) {
1342
1375
  throw error;
@@ -1607,6 +1640,24 @@ export class Model {
1607
1640
  if (error instanceof FilterValidationError) {
1608
1641
  throw new BadRequestError(error.message);
1609
1642
  }
1643
+ // Bad client-supplied givens (unknown name, wrong-typed value,
1644
+ // finalized override, ...) surface as a Malloy `runtime-given-*`
1645
+ // error; see getQueryResults. Malloy validates, the publisher maps
1646
+ // to 400. Duck-type on `.code` (not a MalloyError, not root-exported).
1647
+ const givenCode = (error as { code?: string })?.code;
1648
+ if (
1649
+ typeof givenCode === "string" &&
1650
+ givenCode.startsWith("runtime-given-")
1651
+ ) {
1652
+ logger.debug("Rejected client-supplied given", {
1653
+ environmentName: this.packageName,
1654
+ modelPath: this.modelPath,
1655
+ error: error instanceof Error ? error.message : String(error),
1656
+ });
1657
+ throw new BadRequestError(
1658
+ error instanceof Error ? error.message : String(error),
1659
+ );
1660
+ }
1610
1661
  if (error instanceof MalloyError) {
1611
1662
  throw error;
1612
1663
  }
@@ -237,6 +237,17 @@ export class Package {
237
237
  packageName: string,
238
238
  packagePath: string,
239
239
  environmentMalloyConfig: PackageConnectionInput,
240
+ /**
241
+ * Delete `packagePath` if the load fails. Opt-in, and only correct for a
242
+ * caller that created the directory itself (an install staged into place),
243
+ * where the half-built tree is Publisher's to clean up and `installPackage`
244
+ * rolls the previous one back. Every other caller loads a directory that
245
+ * already existed: a reload of a package that is currently serving, or a
246
+ * user directory registered via addPackage. Deleting those on a transient
247
+ * compile error destroys the source and takes the package offline, so the
248
+ * default is to leave the directory alone.
249
+ */
250
+ cleanupDirectoryOnFailure: boolean = false,
240
251
  ): Promise<Package> {
241
252
  assertSafeEnvironmentPath(packagePath);
242
253
  const startTime = performance.now();
@@ -278,23 +289,30 @@ export class Package {
278
289
  malloy_package_name: packageName,
279
290
  status: packageLoadFailureStatus(error),
280
291
  });
281
- // Clean up the package directory on failure, but NOT when packagePath
282
- // is an in-place mount symlink (watch mode). Removing it would unmount
283
- // the package, so a transient compile error from a half-typed model
284
- // saved mid-edit would brick the package until a restart. The symlink
285
- // points at the user's live source, which is left untouched; the next
286
- // save recompiles against it.
292
+ // Clean up the package directory only when the caller opted in (an
293
+ // install that staged this tree), and never when packagePath is an
294
+ // in-place mount symlink (watch mode). Removing it would unmount the
295
+ // package, so a transient compile error from a half-typed model saved
296
+ // mid-edit would brick the package until a restart. The symlink points
297
+ // at the user's live source, which is left untouched; the next save
298
+ // recompiles against it.
287
299
  try {
288
- const stat = await fs.lstat(packagePath).catch(() => null);
289
- if (stat?.isSymbolicLink()) {
300
+ if (!cleanupDirectoryOnFailure) {
290
301
  logger.info(
291
- `Skipping cleanup of symlinked package path on failure: ${packagePath}`,
302
+ `Preserving existing package directory after failed load: ${packagePath}`,
292
303
  );
293
304
  } else {
294
- await fs.rm(packagePath, { recursive: true, force: true });
295
- logger.info(
296
- `Cleaned up failed package directory: ${packagePath}`,
297
- );
305
+ const stat = await fs.lstat(packagePath).catch(() => null);
306
+ if (stat?.isSymbolicLink()) {
307
+ logger.info(
308
+ `Skipping cleanup of symlinked package path on failure: ${packagePath}`,
309
+ );
310
+ } else {
311
+ await fs.rm(packagePath, { recursive: true, force: true });
312
+ logger.info(
313
+ `Cleaned up failed package directory: ${packagePath}`,
314
+ );
315
+ }
298
316
  }
299
317
  } catch (cleanupError) {
300
318
  logger.warn(`Failed to clean up package directory ${packagePath}`, {
@@ -0,0 +1,193 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import * as fs from "fs/promises";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+
6
+ import { Environment, PackageStatus } from "./environment";
7
+
8
+ /**
9
+ * Regression tests for RELOAD data loss.
10
+ *
11
+ * `Package.create`'s failure cleanup was written for a fresh install, where the
12
+ * half-built tree is Publisher's to remove. A reload runs the same code against
13
+ * a directory that already exists and is already serving, so a model that
14
+ * failed to compile deleted the user's package directory and evicted the
15
+ * last-good compiled model, taking the package offline until it was
16
+ * re-provisioned. That is reachable from the reload endpoint and the
17
+ * malloy_reloadPackage MCP tool by any caller that saves a broken model and
18
+ * reloads, which is the ordinary authoring mistake.
19
+ *
20
+ * Directory cleanup is now opt-in and only `installPackage` asks for it, so
21
+ * these two guard the reload path. Run against a real `Environment` and a real
22
+ * `Package.create` over temp dirs, the same way package_rollback.spec.ts does.
23
+ */
24
+ describe("failed reload does not destroy a serving package", () => {
25
+ let rootDir: string;
26
+ let envPath: string;
27
+
28
+ const GOOD_MODEL = `source: ones is duckdb.sql("SELECT 1 as x")\n`;
29
+ const BROKEN_MODEL = `source: broken is @@@ not valid malloy !!!\n`;
30
+
31
+ async function writePackageDir(dir: string, model: string): Promise<void> {
32
+ await fs.mkdir(dir, { recursive: true });
33
+ await fs.writeFile(
34
+ path.join(dir, "publisher.json"),
35
+ JSON.stringify({ name: "pkg", description: "reload fixture" }),
36
+ );
37
+ await fs.writeFile(path.join(dir, "model.malloy"), model);
38
+ }
39
+
40
+ async function copyDir(src: string, dst: string): Promise<void> {
41
+ await fs.mkdir(dst, { recursive: true });
42
+ await fs.cp(src, dst, { recursive: true });
43
+ }
44
+
45
+ beforeEach(async () => {
46
+ rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "publisher-reload-"));
47
+ envPath = path.join(rootDir, "env");
48
+ await fs.mkdir(envPath, { recursive: true });
49
+ });
50
+
51
+ afterEach(async () => {
52
+ await fs.rm(rootDir, { recursive: true, force: true }).catch(() => {});
53
+ });
54
+
55
+ it("keeps the package directory on disk when the reloaded model does not compile", async () => {
56
+ const env = await Environment.create("testEnv", envPath, []);
57
+ const pkgDir = path.join(envPath, "pkg");
58
+ await writePackageDir(pkgDir, GOOD_MODEL);
59
+ await env.addPackage("pkg");
60
+
61
+ // The ordinary authoring mistake: save something that does not compile,
62
+ // then reload.
63
+ await fs.writeFile(path.join(pkgDir, "model.malloy"), BROKEN_MODEL);
64
+ await expect(env.getPackage("pkg", true)).rejects.toThrow();
65
+
66
+ // The directory and the file the user was editing both survive.
67
+ const modelText = await fs.readFile(
68
+ path.join(pkgDir, "model.malloy"),
69
+ "utf-8",
70
+ );
71
+ expect(modelText).toBe(BROKEN_MODEL);
72
+ });
73
+
74
+ it("keeps serving the last good model after a failed reload", async () => {
75
+ const env = await Environment.create("testEnv", envPath, []);
76
+ const pkgDir = path.join(envPath, "pkg");
77
+ await writePackageDir(pkgDir, GOOD_MODEL);
78
+ await env.addPackage("pkg");
79
+
80
+ const servingBefore = await env.getPackage("pkg", false);
81
+
82
+ await fs.writeFile(path.join(pkgDir, "model.malloy"), BROKEN_MODEL);
83
+ await expect(env.getPackage("pkg", true)).rejects.toThrow();
84
+
85
+ // The package is still loaded and answerable: a failed reload reports the
86
+ // compile error, it does not take the package down. Assert the exact
87
+ // state, not just that something is there: a status stranded at LOADING
88
+ // would satisfy toBeDefined() while listPackages skips it, so the package
89
+ // would answer getPackage and be invisible to listings and discovery.
90
+ const stillServing = await env.getPackage("pkg", false);
91
+ expect(stillServing).toBe(servingBefore);
92
+ expect(env.getPackageStatus("pkg")?.status).toBe(PackageStatus.SERVING);
93
+ expect((await env.listPackages()).map((p) => p.name)).toContain("pkg");
94
+ });
95
+
96
+ it("keeps a rolled-back reinstall listed and serving", async () => {
97
+ // The other reload path. A package with an install location reloads via
98
+ // installPackage, whose rollback restores the previous tree but used to
99
+ // drop the status while `packages` kept the old package: it answered
100
+ // getPackage but vanished from listPackages and discovery until a
101
+ // restart. Both maps must agree that it is still serving.
102
+ const env = await Environment.create("testEnv", envPath, []);
103
+ const goodFixture = path.join(rootDir, "good");
104
+ const brokenFixture = path.join(rootDir, "broken");
105
+ await writePackageDir(goodFixture, GOOD_MODEL);
106
+ await writePackageDir(brokenFixture, BROKEN_MODEL);
107
+
108
+ await env.installPackage("pkg", (stagingPath) =>
109
+ copyDir(goodFixture, stagingPath),
110
+ );
111
+ const servingBefore = await env.getPackage("pkg", false);
112
+
113
+ await expect(
114
+ env.installPackage("pkg", (stagingPath) =>
115
+ copyDir(brokenFixture, stagingPath),
116
+ ),
117
+ ).rejects.toThrow();
118
+
119
+ expect(env.getPackageStatus("pkg")?.status).toBe(PackageStatus.SERVING);
120
+ expect((await env.listPackages()).map((p) => p.name)).toContain("pkg");
121
+ expect(await env.getPackage("pkg", false)).toBe(servingBefore);
122
+ // The on-disk half of the same claim: the rejected tree is gone and the
123
+ // user's is back. Without this, nothing pins "your files are left alone".
124
+ expect(
125
+ await fs.readFile(path.join(envPath, "pkg", "model.malloy"), "utf-8"),
126
+ ).toBe(GOOD_MODEL);
127
+ });
128
+
129
+ it("does not advertise a package as serving when the rollback could not restore it", async () => {
130
+ // Keeping the last-good package served is only honest when the rollback
131
+ // actually put its tree back. If nothing was restored, the cached package
132
+ // no longer matches disk, and claiming SERVING would advertise a package
133
+ // over a canonical path that is missing or still holds rejected content.
134
+ const env = await Environment.create("testEnv", envPath, []);
135
+ const pkgDir = path.join(envPath, "pkg");
136
+ await writePackageDir(pkgDir, GOOD_MODEL);
137
+ await env.addPackage("pkg");
138
+ expect(env.getPackageStatus("pkg")?.status).toBe(PackageStatus.SERVING);
139
+
140
+ // Record retirements rather than scheduling the real drain, which is a
141
+ // 30s timer this test has no reason to wait on.
142
+ const retiredLabels: string[] = [];
143
+ (
144
+ env as unknown as {
145
+ retireConnectionGeneration: (label: string) => void;
146
+ }
147
+ ).retireConnectionGeneration = (label: string) => {
148
+ retiredLabels.push(label);
149
+ };
150
+
151
+ // The tree disappears from under Publisher, so the reinstall has nothing
152
+ // to retire and the rollback has nothing to restore.
153
+ await fs.rm(pkgDir, { recursive: true, force: true });
154
+
155
+ const brokenFixture = path.join(rootDir, "broken-nofallback");
156
+ await writePackageDir(brokenFixture, BROKEN_MODEL);
157
+ await expect(
158
+ env.installPackage("pkg", (stagingPath) =>
159
+ copyDir(brokenFixture, stagingPath),
160
+ ),
161
+ ).rejects.toThrow();
162
+
163
+ // Eviction is the honest outcome here, and both maps must agree on it.
164
+ expect(env.getPackageStatus("pkg")).toBeUndefined();
165
+ expect((await env.listPackages()).map((p) => p.name)).not.toContain(
166
+ "pkg",
167
+ );
168
+ // Assert the packages map too, not just the status: asserting only the
169
+ // status leaves the half that actually strands a package untested.
170
+ await expect(env.getPackage("pkg", false)).rejects.toThrow();
171
+
172
+ // Evicting must retire the old package's connections. Once it leaves
173
+ // this.packages, closeAllConnections cannot reach its MalloyConfig, so
174
+ // skipping the retire orphans its native handles for the process's life.
175
+ // The drain is timer-based, so assert it was scheduled, not that it ran.
176
+ expect(retiredLabels).toContain("package pkg");
177
+ });
178
+
179
+ it("recovers on the next reload once the model compiles again", async () => {
180
+ const env = await Environment.create("testEnv", envPath, []);
181
+ const pkgDir = path.join(envPath, "pkg");
182
+ await writePackageDir(pkgDir, GOOD_MODEL);
183
+ await env.addPackage("pkg");
184
+
185
+ await fs.writeFile(path.join(pkgDir, "model.malloy"), BROKEN_MODEL);
186
+ await expect(env.getPackage("pkg", true)).rejects.toThrow();
187
+
188
+ // Because nothing was deleted, fixing the model and reloading is enough.
189
+ await fs.writeFile(path.join(pkgDir, "model.malloy"), GOOD_MODEL);
190
+ const reloaded = await env.getPackage("pkg", true);
191
+ expect(reloaded).toBeDefined();
192
+ });
193
+ });
@@ -0,0 +1,7 @@
1
+ id,region,status,amount
2
+ 1,US,active,100
3
+ 2,US,active,200
4
+ 3,US,cancelled,50
5
+ 4,EU,active,300
6
+ 5,EU,cancelled,400
7
+ 6,EU,active,500
@@ -0,0 +1,34 @@
1
+ ##! experimental.givens
2
+
3
+ given: target_region :: string is 'US'
4
+ given: min_amount :: number is 0
5
+ given: role :: string is 'guest'
6
+
7
+ #(filter) dimension=status type=equal
8
+ source: orders is duckdb.table('data/orders.csv') extend {
9
+ primary_key: id
10
+
11
+ measure:
12
+ order_count is count()
13
+
14
+ view: by_given_region is {
15
+ where: region = $target_region and amount >= $min_amount
16
+ aggregate: order_count
17
+ }
18
+ }
19
+
20
+ // Gated variant for authorize-interaction coverage: only $role = 'admin' may
21
+ // read it. Shares the givens above so we can prove the name-vs-value-vs-authorize
22
+ // status matrix on a gated source.
23
+ #(authorize) "$role = 'admin'"
24
+ source: gated_orders is duckdb.table('data/orders.csv') extend {
25
+ primary_key: id
26
+
27
+ measure:
28
+ order_count is count()
29
+
30
+ view: by_given_region is {
31
+ where: region = $target_region and amount >= $min_amount
32
+ aggregate: order_count
33
+ }
34
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "query-givens",
3
+ "version": "1.0.0",
4
+ "description": "Fixture for /query givens forwarding (server + SDK)"
5
+ }
@@ -23,8 +23,8 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
23
23
  let env: McpE2ETestEnvironment | null = null;
24
24
  let mcpClient: Client;
25
25
 
26
- const ENVIRONMENT_NAME = "malloy-samples";
27
- const PACKAGE_NAME = "faa";
26
+ const ENVIRONMENT_NAME = "examples";
27
+ const PACKAGE_NAME = "storefront";
28
28
 
29
29
  beforeAll(async () => {
30
30
  // Setup the E2E environment (starts server, connects client)
@@ -49,8 +49,8 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
49
49
  arguments: {
50
50
  environmentName: ENVIRONMENT_NAME,
51
51
  packageName: PACKAGE_NAME,
52
- modelPath: "flights.malloy",
53
- query: "run: flights->{ aggregate: c is count() }",
52
+ modelPath: "storefront.malloy",
53
+ query: "run: order_items->{ aggregate: c is count() }",
54
54
  },
55
55
  });
56
56
 
@@ -97,9 +97,9 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
97
97
  const params = {
98
98
  environmentName: ENVIRONMENT_NAME,
99
99
  packageName: PACKAGE_NAME,
100
- modelPath: "flights.malloy",
101
- sourceName: "flights", // Added sourceName
102
- queryName: "top_carriers",
100
+ modelPath: "storefront.malloy",
101
+ sourceName: "order_items", // Added sourceName
102
+ queryName: "top_products",
103
103
  };
104
104
 
105
105
  // Expect RESOLUTION with success
@@ -142,8 +142,8 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
142
142
  const params = {
143
143
  environmentName: ENVIRONMENT_NAME,
144
144
  packageName: PACKAGE_NAME,
145
- modelPath: "flights.malloy",
146
- query: "run: flights->{BAD SYNTAX aggregate: flight_count is count()}",
145
+ modelPath: "storefront.malloy",
146
+ query: "run: order_items->{BAD SYNTAX aggregate: order_count is count()}",
147
147
  };
148
148
 
149
149
  // Application Error (Malloy Compilation): Expect RESOLUTION with isError: true
@@ -181,9 +181,9 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
181
181
  const params = {
182
182
  environmentName: ENVIRONMENT_NAME,
183
183
  packageName: PACKAGE_NAME,
184
- modelPath: "flights.malloy",
185
- query: "run: flights->{aggregate: c is count()}",
186
- queryName: "top_carriers",
184
+ modelPath: "storefront.malloy",
185
+ query: "run: order_items->{aggregate: c is count()}",
186
+ queryName: "top_products",
187
187
  };
188
188
 
189
189
  // Expect RESOLUTION because the error is thrown *inside* the handler
@@ -209,7 +209,7 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
209
209
  const params = {
210
210
  environmentName: ENVIRONMENT_NAME,
211
211
  packageName: PACKAGE_NAME,
212
- modelPath: "flights.malloy",
212
+ modelPath: "storefront.malloy",
213
213
  // Missing query AND queryName
214
214
  };
215
215
 
@@ -237,7 +237,7 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
237
237
  // Missing modelPath
238
238
  environmentName: ENVIRONMENT_NAME,
239
239
  packageName: PACKAGE_NAME,
240
- query: "run: flights->{aggregate: flight_count is count()}",
240
+ query: "run: order_items->{aggregate: c is count()}",
241
241
  };
242
242
 
243
243
  // Protocol Error (Caught by Zod/MCP): Expect REJECTION
@@ -259,8 +259,8 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
259
259
  const params = {
260
260
  environmentName: ENVIRONMENT_NAME,
261
261
  packageName: "nonexistent_package", // Use a package that doesn't exist
262
- modelPath: "flights.malloy",
263
- query: "run: flights->{aggregate: c is count()}",
262
+ modelPath: "storefront.malloy",
263
+ query: "run: order_items->{aggregate: c is count()}",
264
264
  };
265
265
 
266
266
  // Application Error (Service Layer): Expect RESOLUTION with isError: true
@@ -304,7 +304,7 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
304
304
  environmentName: ENVIRONMENT_NAME,
305
305
  packageName: PACKAGE_NAME,
306
306
  modelPath: "nonexistent_model.malloy", // Use a model that doesn't exist
307
- query: "run: flights->{aggregate: c is count()}",
307
+ query: "run: order_items->{aggregate: c is count()}",
308
308
  };
309
309
 
310
310
  // Application Error (Service Layer): Expect RESOLUTION with isError: true
@@ -364,8 +364,8 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
364
364
  arguments: {
365
365
  environmentName: ENVIRONMENT_NAME,
366
366
  packageName: PACKAGE_NAME,
367
- modelPath: "flights.malloy",
368
- query: "run: flights->{aggregate: c is count()}",
367
+ modelPath: "storefront.malloy",
368
+ query: "run: order_items->{aggregate: c is count()}",
369
369
  },
370
370
  }),
371
371
  ).rejects.toThrow();
@@ -377,8 +377,8 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
377
377
  const params = {
378
378
  environmentName: ENVIRONMENT_NAME,
379
379
  packageName: PACKAGE_NAME,
380
- modelPath: "flights.malloy",
381
- queryName: "top_carriers", // Nested view, but sourceName is missing
380
+ modelPath: "storefront.malloy",
381
+ queryName: "top_products", // Nested view, but sourceName is missing
382
382
  };
383
383
 
384
384
  // Expect RESOLUTION with error because it's invalid usage processed by the handler
@@ -405,7 +405,7 @@ describe.serial("MCP Tool Handlers (E2E Integration)", () => {
405
405
  const errorPayload = JSON.parse(errorJsonText);
406
406
  // Expect error about the query/view itself not found
407
407
  expect(errorPayload.error).toMatch(
408
- /Query 'top_carriers' not found|Reference to undefined object 'top_carriers'/i,
408
+ /Query 'top_products' not found|Reference to undefined object 'top_products'/i,
409
409
  );
410
410
  expect(Array.isArray(errorPayload.suggestions)).toBe(true);
411
411
  });