@malloy-publisher/server 0.0.225 → 0.0.227
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 +2 -11
- package/dist/app/api-doc.yaml +29 -24
- package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
- package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
- package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
- package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
- package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
- package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
- package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
- package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
- package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
- package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
- package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
- package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
- package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +71 -15
- package/dist/package_load_worker.mjs +169 -40
- package/dist/server.mjs +782 -7050
- package/package.json +1 -4
- package/src/controller/package.controller.spec.ts +17 -17
- package/src/controller/package.controller.ts +7 -6
- package/src/health.ts +3 -8
- package/src/logger.spec.ts +193 -0
- package/src/logger.ts +115 -18
- package/src/mcp/error_messages.ts +8 -37
- package/src/mcp/handler_utils.ts +10 -129
- package/src/mcp/mcp_constants.ts +0 -16
- package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
- package/src/mcp/server.ts +29 -37
- package/src/mcp/skills/build_skills_bundle.ts +1 -1
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/tools/docs_search_tool.ts +1 -1
- package/src/mcp/tools/execute_query_tool.ts +2 -2
- package/src/mcp/tools/get_context_eval.ts +3 -3
- package/src/mcp/tools/get_context_tool.spec.ts +196 -1
- package/src/mcp/tools/get_context_tool.ts +165 -67
- package/src/package_load/package_load_pool.ts +8 -1
- package/src/package_load/package_load_worker.ts +75 -24
- package/src/package_load/protocol.ts +21 -1
- package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
- package/src/package_load/rpc_wait_accountant.ts +76 -0
- package/src/package_load_metrics.spec.ts +114 -0
- package/src/package_load_metrics.ts +127 -0
- package/src/pg_helpers.spec.ts +2 -206
- package/src/pg_helpers.ts +4 -120
- package/src/server.ts +0 -16
- package/src/service/build_plan.spec.ts +42 -60
- package/src/service/build_plan.ts +37 -61
- package/src/service/environment.ts +5 -1
- package/src/service/materialization_test_fixtures.ts +6 -8
- package/src/service/package.ts +165 -86
- package/src/service/package_manifest.spec.ts +22 -1
- package/src/service/package_manifest.ts +29 -0
- package/src/service/persistence_policy.spec.ts +234 -0
- package/src/test_helpers/metrics_harness.ts +40 -0
- package/tests/harness/mcp_test_setup.ts +1 -1
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/src/dto/connection.dto.spec.ts +0 -186
- package/src/dto/connection.dto.ts +0 -308
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -27
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/ducklake_version.spec.ts +0 -43
- package/src/ducklake_version.ts +0 -26
- package/src/mcp/agent_server.spec.ts +0 -18
- package/src/mcp/agent_server.ts +0 -144
- package/src/mcp/prompts/handlers.ts +0 -84
- package/src/mcp/prompts/index.ts +0 -11
- package/src/mcp/prompts/prompt_definitions.ts +0 -160
- package/src/mcp/prompts/prompt_service.ts +0 -67
- package/src/mcp/prompts/utils.ts +0 -62
- package/src/mcp/resource_metadata.ts +0 -47
- package/src/mcp/resources/environment_resource.ts +0 -187
- package/src/mcp/resources/model_resource.ts +0 -155
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -373
- package/src/mcp/resources/query_resource.ts +0 -122
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/tools/discovery_tools.ts +0 -280
- package/src/service/materialization_cron_gate.spec.ts +0 -181
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/StorageManager.mock.ts +0 -50
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mocks.ts +0 -39
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
|
@@ -0,0 +1,234 @@
|
|
|
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 } from "./environment";
|
|
7
|
+
import type { Package } from "./package";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The publish gate for the package's Malloy Persistence policy (persistence.md
|
|
11
|
+
* §3.1, §9.2–§9.5). Scope is a single package-level mode (`Package.scope`) and a
|
|
12
|
+
* materialization cron is package-root-only + version-scope-only:
|
|
13
|
+
*
|
|
14
|
+
* - per-source `#@ persist ... sharing=`/`schedule=` are retired (declaring
|
|
15
|
+
* either on a source is a manifest error);
|
|
16
|
+
* - `materialization.schedule` is legal only when `scope: version`;
|
|
17
|
+
* - `materialization.schedule` and freshness (package or source) are mutually
|
|
18
|
+
* exclusive.
|
|
19
|
+
*
|
|
20
|
+
* These tests run a real `Environment` + `Package.create` over temp dirs, so
|
|
21
|
+
* they also prove end-to-end that the pinned compiler still ACCEPTS the
|
|
22
|
+
* `#@ persist` annotation keys (`sharing`/`schedule`/`freshness.*`) at compile
|
|
23
|
+
* — the rejection is the publisher's manifest-level gate, not a compile error —
|
|
24
|
+
* and that `scope` is parsed from the manifest root and surfaced on the package.
|
|
25
|
+
*/
|
|
26
|
+
describe("persistence policy gate", () => {
|
|
27
|
+
let rootDir: string;
|
|
28
|
+
let envPath: string;
|
|
29
|
+
|
|
30
|
+
async function loadPackage(
|
|
31
|
+
model: string,
|
|
32
|
+
manifest: Record<string, unknown> = {},
|
|
33
|
+
): Promise<Package> {
|
|
34
|
+
const dir = path.join(envPath, "pkg");
|
|
35
|
+
await fs.mkdir(dir, { recursive: true });
|
|
36
|
+
await fs.writeFile(
|
|
37
|
+
path.join(dir, "publisher.json"),
|
|
38
|
+
JSON.stringify({ name: "pkg", description: "fixture", ...manifest }),
|
|
39
|
+
);
|
|
40
|
+
await fs.writeFile(path.join(dir, "model.malloy"), model);
|
|
41
|
+
const env = await Environment.create("testEnv", envPath, []);
|
|
42
|
+
await env.addPackage("pkg");
|
|
43
|
+
return env.getPackage("pkg", false);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sourceByName(pkg: Package, name: string) {
|
|
47
|
+
const sources = pkg.getBuildPlan()?.sources ?? {};
|
|
48
|
+
const found = Object.values(sources).find((s) => s.name === name);
|
|
49
|
+
if (!found) throw new Error(`persist source '${name}' not in build plan`);
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "publisher-policy-"));
|
|
55
|
+
envPath = path.join(rootDir, "env");
|
|
56
|
+
await fs.mkdir(envPath, { recursive: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
await fs.rm(rootDir, { recursive: true, force: true }).catch(() => {});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const PLAIN_MODEL = `##! experimental.persistence
|
|
64
|
+
|
|
65
|
+
#@ persist name="a_table"
|
|
66
|
+
source: a is duckdb.sql("SELECT 1 as x")
|
|
67
|
+
|
|
68
|
+
#@ persist name="b_table" refresh=full
|
|
69
|
+
source: b is duckdb.sql("SELECT 2 as x")
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const SHARING_SOURCE_MODEL = `##! experimental.persistence
|
|
73
|
+
|
|
74
|
+
#@ persist name="s_table" sharing=private
|
|
75
|
+
source: s is duckdb.sql("SELECT 1 as x")
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const SCHEDULE_SOURCE_MODEL = `##! experimental.persistence
|
|
79
|
+
|
|
80
|
+
#@ persist name="s_table" schedule="0 */6 * * *"
|
|
81
|
+
source: s is duckdb.sql("SELECT 1 as x")
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const FRESHNESS_SOURCE_MODEL = `##! experimental.persistence
|
|
85
|
+
|
|
86
|
+
#@ persist name="f_table" freshness.window="1h"
|
|
87
|
+
source: f is duckdb.sql("SELECT 1 as x")
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
// ── scope parsing + surfacing ─────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
it(
|
|
93
|
+
"defaults scope to 'package' when the manifest omits it",
|
|
94
|
+
async () => {
|
|
95
|
+
const pkg = await loadPackage(PLAIN_MODEL);
|
|
96
|
+
expect(pkg.getPackageMetadata().scope).toBe("package");
|
|
97
|
+
},
|
|
98
|
+
{ timeout: 30000 },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
it(
|
|
102
|
+
"surfaces an explicit scope: version from the manifest root",
|
|
103
|
+
async () => {
|
|
104
|
+
const pkg = await loadPackage(PLAIN_MODEL, { scope: "version" });
|
|
105
|
+
expect(pkg.getPackageMetadata().scope).toBe("version");
|
|
106
|
+
},
|
|
107
|
+
{ timeout: 30000 },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
it(
|
|
111
|
+
"fails the load on an invalid scope value",
|
|
112
|
+
async () => {
|
|
113
|
+
await expect(
|
|
114
|
+
loadPackage(PLAIN_MODEL, { scope: "shared" }),
|
|
115
|
+
).rejects.toThrow(/scope/i);
|
|
116
|
+
},
|
|
117
|
+
{ timeout: 30000 },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// ── Rule 1: per-source sharing/schedule retired ───────────────────────
|
|
121
|
+
|
|
122
|
+
it(
|
|
123
|
+
"rejects a per-source sharing= annotation, pointing at root scope",
|
|
124
|
+
async () => {
|
|
125
|
+
const pkg = await loadPackage(SHARING_SOURCE_MODEL, {
|
|
126
|
+
scope: "version",
|
|
127
|
+
});
|
|
128
|
+
const joined = pkg.formatInvalidPersistencePolicy();
|
|
129
|
+
expect(joined).toContain('"s"');
|
|
130
|
+
expect(joined).toContain("sharing");
|
|
131
|
+
expect(joined).toContain('"scope"');
|
|
132
|
+
},
|
|
133
|
+
{ timeout: 30000 },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
it(
|
|
137
|
+
"rejects a per-source schedule= annotation, pointing at the root schedule",
|
|
138
|
+
async () => {
|
|
139
|
+
const pkg = await loadPackage(SCHEDULE_SOURCE_MODEL, {
|
|
140
|
+
scope: "version",
|
|
141
|
+
});
|
|
142
|
+
const joined = pkg.formatInvalidPersistencePolicy();
|
|
143
|
+
expect(joined).toContain('"s"');
|
|
144
|
+
expect(joined).toContain("schedule");
|
|
145
|
+
expect(joined).toContain("materialization.schedule");
|
|
146
|
+
// The retired field is not emitted on the wire plan.
|
|
147
|
+
const source = sourceByName(pkg, "s") as Record<string, unknown>;
|
|
148
|
+
expect(source.schedule).toBeUndefined();
|
|
149
|
+
expect(source.sharing).toBeUndefined();
|
|
150
|
+
},
|
|
151
|
+
{ timeout: 30000 },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ── Rule 2: schedule requires scope: version ──────────────────────────
|
|
155
|
+
|
|
156
|
+
it(
|
|
157
|
+
"rejects a package schedule when scope is package (the default)",
|
|
158
|
+
async () => {
|
|
159
|
+
const pkg = await loadPackage(PLAIN_MODEL, {
|
|
160
|
+
materialization: { schedule: "0 6 * * *" },
|
|
161
|
+
});
|
|
162
|
+
const joined = pkg.formatInvalidPersistencePolicy();
|
|
163
|
+
expect(joined).toContain("materialization.schedule");
|
|
164
|
+
expect(joined).toContain('"scope": "version"');
|
|
165
|
+
},
|
|
166
|
+
{ timeout: 30000 },
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
it(
|
|
170
|
+
"accepts a package schedule when scope is version and no freshness is set",
|
|
171
|
+
async () => {
|
|
172
|
+
const pkg = await loadPackage(PLAIN_MODEL, {
|
|
173
|
+
scope: "version",
|
|
174
|
+
materialization: { schedule: "0 6 * * *" },
|
|
175
|
+
});
|
|
176
|
+
expect(pkg.persistencePolicyWarnings()).toEqual([]);
|
|
177
|
+
},
|
|
178
|
+
{ timeout: 30000 },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// ── Rule 3: schedule XOR freshness ────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
it(
|
|
184
|
+
"rejects a schedule declared alongside package-level freshness",
|
|
185
|
+
async () => {
|
|
186
|
+
const pkg = await loadPackage(PLAIN_MODEL, {
|
|
187
|
+
scope: "version",
|
|
188
|
+
materialization: {
|
|
189
|
+
schedule: "0 6 * * *",
|
|
190
|
+
freshness: { window: "24h" },
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const joined = pkg.formatInvalidPersistencePolicy();
|
|
194
|
+
expect(joined).toContain("mutually exclusive");
|
|
195
|
+
},
|
|
196
|
+
{ timeout: 30000 },
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
it(
|
|
200
|
+
"rejects a schedule declared alongside a source-level freshness",
|
|
201
|
+
async () => {
|
|
202
|
+
const pkg = await loadPackage(FRESHNESS_SOURCE_MODEL, {
|
|
203
|
+
scope: "version",
|
|
204
|
+
materialization: { schedule: "0 6 * * *" },
|
|
205
|
+
});
|
|
206
|
+
const joined = pkg.formatInvalidPersistencePolicy();
|
|
207
|
+
expect(joined).toContain("mutually exclusive");
|
|
208
|
+
},
|
|
209
|
+
{ timeout: 30000 },
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// ── inert when nothing is declared ────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
it(
|
|
215
|
+
"is inert for a plain package with no schedule and no forbidden knobs",
|
|
216
|
+
async () => {
|
|
217
|
+
const pkg = await loadPackage(PLAIN_MODEL, { scope: "package" });
|
|
218
|
+
expect(pkg.persistencePolicyWarnings()).toEqual([]);
|
|
219
|
+
},
|
|
220
|
+
{ timeout: 30000 },
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
it(
|
|
224
|
+
"accepts source-level freshness without a schedule in either scope",
|
|
225
|
+
async () => {
|
|
226
|
+
const pkg = await loadPackage(FRESHNESS_SOURCE_MODEL, {
|
|
227
|
+
scope: "package",
|
|
228
|
+
});
|
|
229
|
+
expect(pkg.persistencePolicyWarnings()).toEqual([]);
|
|
230
|
+
expect(sourceByName(pkg, "f").freshness).toEqual({ window: "1h" });
|
|
231
|
+
},
|
|
232
|
+
{ timeout: 30000 },
|
|
233
|
+
);
|
|
234
|
+
});
|
|
@@ -62,6 +62,15 @@ export interface MetricsHarness {
|
|
|
62
62
|
* fired for this gauge yet.
|
|
63
63
|
*/
|
|
64
64
|
collectGauge(name: string): Promise<number | undefined>;
|
|
65
|
+
/**
|
|
66
|
+
* Force a collection cycle and return the aggregate `count`/`sum` across all
|
|
67
|
+
* data points for the named histogram (optionally scoped to one label set).
|
|
68
|
+
* `count` is 0 when the histogram has not been emitted yet.
|
|
69
|
+
*/
|
|
70
|
+
collectHistogram(
|
|
71
|
+
name: string,
|
|
72
|
+
attributeFilter?: Record<string, string | number | boolean>,
|
|
73
|
+
): Promise<{ count: number; sum: number; boundaries: number[] }>;
|
|
65
74
|
shutdown(): Promise<void>;
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -100,6 +109,37 @@ export async function startMetricsHarness(): Promise<MetricsHarness> {
|
|
|
100
109
|
}
|
|
101
110
|
return total;
|
|
102
111
|
},
|
|
112
|
+
async collectHistogram(
|
|
113
|
+
name: string,
|
|
114
|
+
attributeFilter?: Record<string, string | number | boolean>,
|
|
115
|
+
): Promise<{ count: number; sum: number; boundaries: number[] }> {
|
|
116
|
+
const result = await reader.collect();
|
|
117
|
+
let count = 0;
|
|
118
|
+
let sum = 0;
|
|
119
|
+
let boundaries: number[] = [];
|
|
120
|
+
for (const rm of result.resourceMetrics.scopeMetrics) {
|
|
121
|
+
for (const metric of rm.metrics) {
|
|
122
|
+
if (metric.descriptor.name !== name) continue;
|
|
123
|
+
for (const dp of metric.dataPoints) {
|
|
124
|
+
if (attributeFilter) {
|
|
125
|
+
const allMatch = Object.entries(attributeFilter).every(
|
|
126
|
+
([k, v]) => dp.attributes?.[k] === v,
|
|
127
|
+
);
|
|
128
|
+
if (!allMatch) continue;
|
|
129
|
+
}
|
|
130
|
+
const point = dp.value as {
|
|
131
|
+
count: number;
|
|
132
|
+
sum?: number;
|
|
133
|
+
buckets?: { boundaries: number[] };
|
|
134
|
+
};
|
|
135
|
+
count += point.count;
|
|
136
|
+
sum += point.sum ?? 0;
|
|
137
|
+
if (point.buckets) boundaries = point.buckets.boundaries;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { count, sum, boundaries };
|
|
142
|
+
},
|
|
103
143
|
async collectGauge(name: string): Promise<number | undefined> {
|
|
104
144
|
const result = await reader.collect();
|
|
105
145
|
for (const rm of result.resourceMetrics.scopeMetrics) {
|
|
@@ -30,7 +30,7 @@ let portCounter = 0;
|
|
|
30
30
|
|
|
31
31
|
// True mutex: the previous Promise-based lock had a TOCTOU race where two beforeAll hooks
|
|
32
32
|
// could both pass the `if (initializationLock)` check and run setup concurrently, sharing
|
|
33
|
-
// one publisher.db / server singleton and causing flaky
|
|
33
|
+
// one publisher.db / server singleton and causing flaky tool calls and port collisions.
|
|
34
34
|
const e2eSetupMutex = new Mutex();
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -34,32 +34,20 @@ describe.serial("MCP Transport Tests (E2E Integration)", () => {
|
|
|
34
34
|
|
|
35
35
|
describe("Basic Protocol", () => {
|
|
36
36
|
it(
|
|
37
|
-
"should handle a simple
|
|
37
|
+
"should handle a simple listTools request",
|
|
38
38
|
async () => {
|
|
39
39
|
if (!env) throw new Error("Test environment not initialized");
|
|
40
|
-
const result = await mcpClient.
|
|
40
|
+
const result = await mcpClient.listTools();
|
|
41
41
|
// Assert based on actual successful response structure
|
|
42
|
-
expect(result).toHaveProperty("
|
|
43
|
-
expect(Array.isArray(result.
|
|
42
|
+
expect(result).toHaveProperty("tools");
|
|
43
|
+
expect(Array.isArray(result.tools)).toBe(true);
|
|
44
|
+
expect(
|
|
45
|
+
result.tools.some((t) => t.name === "malloy_executeQuery"),
|
|
46
|
+
).toBe(true);
|
|
44
47
|
},
|
|
45
48
|
{ timeout: 30000 },
|
|
46
49
|
);
|
|
47
50
|
|
|
48
|
-
it("should receive InvalidParams error when calling a known method with invalid params", async () => {
|
|
49
|
-
if (!env) throw new Error("Test environment not initialized");
|
|
50
|
-
expect.assertions(2);
|
|
51
|
-
try {
|
|
52
|
-
// Use the client from the test environment
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
-
await mcpClient.readResource({} as any); // Force invalid params
|
|
55
|
-
} catch (error) {
|
|
56
|
-
expect(error).toBeInstanceOf(Error);
|
|
57
|
-
// Check error code and message
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
-
expect((error as any).code).toBe(ErrorCode.InternalError);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
51
|
it("should receive MethodNotFound error for unknown methods", async () => {
|
|
64
52
|
if (!env) throw new Error("Test environment not initialized");
|
|
65
53
|
expect.assertions(2); // Expecting error instance and specific code
|
|
@@ -84,18 +72,6 @@ describe.serial("MCP Transport Tests (E2E Integration)", () => {
|
|
|
84
72
|
});
|
|
85
73
|
|
|
86
74
|
describe("Connection Management & Bridge Logic", () => {
|
|
87
|
-
// DELETED - Test irrelevant in stateless mode
|
|
88
|
-
// it("should maintain session across multiple sequential requests", async () => {
|
|
89
|
-
// ...
|
|
90
|
-
// });
|
|
91
|
-
|
|
92
|
-
// DELETED - Test irrelevant in stateless mode
|
|
93
|
-
// it("should handle reconnection after close", async () => {
|
|
94
|
-
// ...
|
|
95
|
-
// });
|
|
96
|
-
|
|
97
|
-
// --- Bridge Error Handling ---
|
|
98
|
-
|
|
99
75
|
it("should return 405 Method Not Allowed for GET requests", async () => {
|
|
100
76
|
if (!env) throw new Error("Test environment not initialized");
|
|
101
77
|
const getUrl = new URL(`${env.serverUrl}/mcp`); // No session ID needed
|
|
@@ -116,11 +116,6 @@ async function startServer(opts: { watch: boolean }): Promise<TestServer> {
|
|
|
116
116
|
const port = await getFreePort();
|
|
117
117
|
let mcpPort = await getFreePort();
|
|
118
118
|
while (mcpPort === port) mcpPort = await getFreePort();
|
|
119
|
-
// The agent MCP server binds its own listener; give it a distinct free port
|
|
120
|
-
// too, otherwise every spawned child binds the fixed default and collides.
|
|
121
|
-
let agentMcpPort = await getFreePort();
|
|
122
|
-
while (agentMcpPort === port || agentMcpPort === mcpPort)
|
|
123
|
-
agentMcpPort = await getFreePort();
|
|
124
119
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
125
120
|
|
|
126
121
|
const env: NodeJS.ProcessEnv = {
|
|
@@ -129,7 +124,6 @@ async function startServer(opts: { watch: boolean }): Promise<TestServer> {
|
|
|
129
124
|
PUBLISHER_HOST: "127.0.0.1",
|
|
130
125
|
PUBLISHER_PORT: String(port),
|
|
131
126
|
MCP_PORT: String(mcpPort),
|
|
132
|
-
AGENT_MCP_PORT: String(agentMcpPort),
|
|
133
127
|
};
|
|
134
128
|
if (opts.watch) env.PUBLISHER_WATCH = ENV_NAME;
|
|
135
129
|
|