@nookplot/runtime 0.5.143 → 0.5.145
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/dist/__tests__/autonomous.workspaceOpportunity.test.js +12 -0
- package/dist/__tests__/autonomous.workspaceOpportunity.test.js.map +1 -1
- package/dist/__tests__/bdAgentPack.test.d.ts +2 -0
- package/dist/__tests__/bdAgentPack.test.d.ts.map +1 -0
- package/dist/__tests__/bdAgentPack.test.js +44 -0
- package/dist/__tests__/bdAgentPack.test.js.map +1 -0
- package/dist/__tests__/bounties.test.d.ts +11 -0
- package/dist/__tests__/bounties.test.d.ts.map +1 -0
- package/dist/__tests__/bounties.test.js +78 -0
- package/dist/__tests__/bounties.test.js.map +1 -0
- package/dist/__tests__/externalMcpTools.test.d.ts +2 -0
- package/dist/__tests__/externalMcpTools.test.d.ts.map +1 -0
- package/dist/__tests__/externalMcpTools.test.js +94 -0
- package/dist/__tests__/externalMcpTools.test.js.map +1 -0
- package/dist/__tests__/pack.gating.test.d.ts +2 -0
- package/dist/__tests__/pack.gating.test.d.ts.map +1 -0
- package/dist/__tests__/pack.gating.test.js +134 -0
- package/dist/__tests__/pack.gating.test.js.map +1 -0
- package/dist/__tests__/pack.test.d.ts +2 -0
- package/dist/__tests__/pack.test.d.ts.map +1 -0
- package/dist/__tests__/pack.test.js +299 -0
- package/dist/__tests__/pack.test.js.map +1 -0
- package/dist/__tests__/packLoader.test.d.ts +2 -0
- package/dist/__tests__/packLoader.test.d.ts.map +1 -0
- package/dist/__tests__/packLoader.test.js +304 -0
- package/dist/__tests__/packLoader.test.js.map +1 -0
- package/dist/__tests__/presetLoader.test.js +59 -42
- package/dist/__tests__/presetLoader.test.js.map +1 -1
- package/dist/__tests__/runtimeConstructor.test.d.ts +18 -0
- package/dist/__tests__/runtimeConstructor.test.d.ts.map +1 -0
- package/dist/__tests__/runtimeConstructor.test.js +57 -0
- package/dist/__tests__/runtimeConstructor.test.js.map +1 -0
- package/dist/__tests__/sandbox.test.js +24 -24
- package/dist/actionCatalog.d.ts.map +1 -1
- package/dist/actionCatalog.generated.d.ts +1 -1
- package/dist/actionCatalog.generated.d.ts.map +1 -1
- package/dist/actionCatalog.generated.js +18 -8
- package/dist/actionCatalog.generated.js.map +1 -1
- package/dist/actionCatalog.js +4 -12
- package/dist/actionCatalog.js.map +1 -1
- package/dist/autonomous.d.ts +24 -1
- package/dist/autonomous.d.ts.map +1 -1
- package/dist/autonomous.js +76 -8
- package/dist/autonomous.js.map +1 -1
- package/dist/bounties.d.ts +21 -1
- package/dist/bounties.d.ts.map +1 -1
- package/dist/bounties.js +5 -1
- package/dist/bounties.js.map +1 -1
- package/dist/conversation/modelLimits.js +17 -17
- package/dist/goal/goalPrompts.js +27 -27
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/pack.d.ts +181 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +379 -0
- package/dist/pack.js.map +1 -0
- package/dist/packLoader.d.ts +109 -0
- package/dist/packLoader.d.ts.map +1 -0
- package/dist/packLoader.js +236 -0
- package/dist/packLoader.js.map +1 -0
- package/dist/presetLoader.d.ts +3 -1
- package/dist/presetLoader.d.ts.map +1 -1
- package/dist/presetLoader.js +7 -1
- package/dist/presetLoader.js.map +1 -1
- package/dist/signalActionMap.d.ts +17 -1
- package/dist/signalActionMap.d.ts.map +1 -1
- package/dist/signalActionMap.js +38 -2
- package/dist/signalActionMap.js.map +1 -1
- package/dist/tools.d.ts +23 -7
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +20 -6
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack schema + validator + preset conversion (ROADMAP_external-mcp-connectors
|
|
3
|
+
* Phase 3). Every schema field maps to a consumer; every reject path is
|
|
4
|
+
* exercised; preset → pack → preset is lossless.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import { validatePack, parsePack, presetToPack, packToPresetConfig, resolvePackActions, CONNECTOR_PLATFORM_MODES, CONNECTOR_PLATFORMS, PACK_SCHEMA_VERSION, } from "../pack.js";
|
|
8
|
+
import { ACTION_CATALOG } from "../actionCatalog.js";
|
|
9
|
+
const FULL_PACK = {
|
|
10
|
+
name: "notion-research-agent",
|
|
11
|
+
version: "1.0.0",
|
|
12
|
+
description: "Answers team questions from the shared Notion workspace.",
|
|
13
|
+
preset: {
|
|
14
|
+
id: "research-biology",
|
|
15
|
+
version: 2,
|
|
16
|
+
trustLevel: "verified",
|
|
17
|
+
failurePolicy: "continue",
|
|
18
|
+
maxCostNook: 500,
|
|
19
|
+
sources: [
|
|
20
|
+
{ type: "mining", label: "Bio traces", config: { domainTags: ["biology"] } },
|
|
21
|
+
{ type: "bundle", config: { bundleId: 42 } },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
tools: ["search_knowledge", "store_knowledge_item", "send_dm"],
|
|
25
|
+
mcpServers: [
|
|
26
|
+
{ name: "notion", url: "https://mcp.notion.com/mcp", connection: "notion" },
|
|
27
|
+
{ name: "open-docs", url: "https://docs.example.com/mcp" },
|
|
28
|
+
],
|
|
29
|
+
connections: [{ service: "notion", owner: "workspace" }],
|
|
30
|
+
connectors: [
|
|
31
|
+
{ platform: "discord", mode: "reactive" },
|
|
32
|
+
{ platform: "email", mode: "outbound" },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
describe("validatePack — happy path", () => {
|
|
36
|
+
it("accepts a full pack exercising every field", () => {
|
|
37
|
+
const result = validatePack(FULL_PACK);
|
|
38
|
+
expect(result.errors).toEqual([]);
|
|
39
|
+
expect(result.ok).toBe(true);
|
|
40
|
+
expect(result.pack).toEqual(FULL_PACK);
|
|
41
|
+
});
|
|
42
|
+
it("accepts a minimal pack (name + version only)", () => {
|
|
43
|
+
const result = validatePack({ name: "tiny", version: "0.1.0" });
|
|
44
|
+
expect(result.ok).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it("returns a deep copy — mutating the result does not touch the input", () => {
|
|
47
|
+
const input = JSON.parse(JSON.stringify(FULL_PACK));
|
|
48
|
+
const result = validatePack(input);
|
|
49
|
+
result.pack.tools.push("ignore");
|
|
50
|
+
expect(input.tools).toEqual(FULL_PACK.tools);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("validatePack — reject paths", () => {
|
|
54
|
+
const expectError = (input, fragment) => {
|
|
55
|
+
const result = validatePack(input);
|
|
56
|
+
expect(result.ok).toBe(false);
|
|
57
|
+
expect(result.pack).toBeNull();
|
|
58
|
+
expect(result.errors.some((e) => e.includes(fragment)), `expected an error containing "${fragment}", got: ${JSON.stringify(result.errors)}`).toBe(true);
|
|
59
|
+
};
|
|
60
|
+
it("rejects non-objects", () => {
|
|
61
|
+
expectError("nope", "must be an object");
|
|
62
|
+
expectError(null, "must be an object");
|
|
63
|
+
expectError([1], "must be an object");
|
|
64
|
+
});
|
|
65
|
+
it("rejects missing/bad name and version", () => {
|
|
66
|
+
expectError({ version: "1.0.0" }, "name:");
|
|
67
|
+
expectError({ name: "-bad-start", version: "1.0.0" }, "name:");
|
|
68
|
+
expectError({ name: "ok", version: "1.0" }, "version:");
|
|
69
|
+
expectError({ name: "ok" }, "version:");
|
|
70
|
+
});
|
|
71
|
+
it("rejects unknown top-level keys, with a hint for snake_case spellings", () => {
|
|
72
|
+
expectError({ name: "ok", version: "1.0.0", bogus: 1 }, "bogus: unknown key");
|
|
73
|
+
expectError({ name: "ok", version: "1.0.0", mcp_servers: [] }, 'did you mean "mcpServers"');
|
|
74
|
+
});
|
|
75
|
+
it("rejects an over-long description", () => {
|
|
76
|
+
expectError({ name: "ok", version: "1.0.0", description: "x".repeat(501) }, "description:");
|
|
77
|
+
});
|
|
78
|
+
it("rejects bad preset blocks", () => {
|
|
79
|
+
expectError({ name: "ok", version: "1.0.0", preset: { version: 1 } }, "preset.id");
|
|
80
|
+
expectError({ name: "ok", version: "1.0.0", preset: { id: "p", version: 0 } }, "preset.version");
|
|
81
|
+
expectError({ name: "ok", version: "1.0.0", preset: { id: "p", trustLevel: "wild" } }, "preset.trustLevel");
|
|
82
|
+
expectError({ name: "ok", version: "1.0.0", preset: { id: "p", failurePolicy: "explode" } }, "preset.failurePolicy");
|
|
83
|
+
expectError({ name: "ok", version: "1.0.0", preset: { id: "p", maxCostNook: -1 } }, "preset.maxCostNook");
|
|
84
|
+
expectError({ name: "ok", version: "1.0.0", preset: { id: "p", sources: [{ label: "no type" }] } }, "sources[0].type");
|
|
85
|
+
expectError({ name: "ok", version: "1.0.0", preset: { id: "p", sources: [{ type: "mining", config: "nope" }] } }, "sources[0].config");
|
|
86
|
+
});
|
|
87
|
+
it("rejects unknown, mcp-prefixed, and duplicate tools", () => {
|
|
88
|
+
expectError({ name: "ok", version: "1.0.0", tools: ["definitely_not_a_real_action"] }, "unknown action type");
|
|
89
|
+
expectError({ name: "ok", version: "1.0.0", tools: ["mcp__notion__search"] }, "declare the server under mcpServers");
|
|
90
|
+
expectError({ name: "ok", version: "1.0.0", tools: ["send_dm", "send_dm"] }, "duplicate action type");
|
|
91
|
+
expectError({ name: "ok", version: "1.0.0", tools: "send_dm" }, "tools: must be an array");
|
|
92
|
+
});
|
|
93
|
+
it("rejects bad mcpServers entries", () => {
|
|
94
|
+
expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "bad name!", url: "https://x.com" }] }, "mcpServers[0].name");
|
|
95
|
+
expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "ftp://x.com" }] }, "mcpServers[0].url");
|
|
96
|
+
expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "not a url" }] }, "mcpServers[0].url");
|
|
97
|
+
expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "https://x.com" }, { name: "a", url: "https://y.com" }] }, "duplicate server name");
|
|
98
|
+
expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "https://x.com", connection: "ghost" }] }, 'does not match any connections[].service');
|
|
99
|
+
expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "https://x.com", extra: 1 }] }, "mcpServers[0].extra: unknown key");
|
|
100
|
+
});
|
|
101
|
+
it("rejects bad connections and dead (unreferenced) connections", () => {
|
|
102
|
+
expectError({ name: "ok", version: "1.0.0", connections: [{ service: "bad service!", owner: "agent" }] }, "connections[0].service");
|
|
103
|
+
expectError({ name: "ok", version: "1.0.0", connections: [{ service: "notion", owner: "guild" }] }, "connections[0].owner");
|
|
104
|
+
expectError({
|
|
105
|
+
name: "ok", version: "1.0.0",
|
|
106
|
+
connections: [{ service: "notion", owner: "agent" }, { service: "notion", owner: "workspace" }],
|
|
107
|
+
mcpServers: [{ name: "n", url: "https://x.com", connection: "notion" }],
|
|
108
|
+
}, "duplicate service");
|
|
109
|
+
// Declared but never referenced by an mcpServer — dead declaration.
|
|
110
|
+
expectError({ name: "ok", version: "1.0.0", connections: [{ service: "notion", owner: "agent" }] }, "not referenced by any mcpServers[].connection");
|
|
111
|
+
});
|
|
112
|
+
it("rejects bad connectors — platform, per-platform mode, duplicates", () => {
|
|
113
|
+
expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "slack", mode: "reactive" }] }, "connectors[0].platform");
|
|
114
|
+
// Risk posture: chat connectors are reactive-only (no outbound/cold DMs).
|
|
115
|
+
expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "discord", mode: "outbound" }] }, "not allowed for discord");
|
|
116
|
+
expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "telegram", mode: "outbound" }] }, "not allowed for telegram");
|
|
117
|
+
expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "email", mode: "reactive" }] }, "not allowed for email");
|
|
118
|
+
expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "email", mode: "outbound" }, { platform: "email", mode: "outbound" }] }, "duplicate platform");
|
|
119
|
+
});
|
|
120
|
+
it("collects multiple errors in one pass", () => {
|
|
121
|
+
const result = validatePack({ name: "!", version: "x", tools: ["nope_action"] });
|
|
122
|
+
expect(result.ok).toBe(false);
|
|
123
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(3);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("schemaVersion (pack-format gate)", () => {
|
|
127
|
+
it("accepts an explicit current schemaVersion and round-trips it", () => {
|
|
128
|
+
const result = validatePack({ schemaVersion: PACK_SCHEMA_VERSION, name: "ok", version: "1.0.0" });
|
|
129
|
+
expect(result.errors).toEqual([]);
|
|
130
|
+
expect(result.ok).toBe(true);
|
|
131
|
+
expect(result.pack.schemaVersion).toBe(PACK_SCHEMA_VERSION);
|
|
132
|
+
});
|
|
133
|
+
it("treats an omitted schemaVersion as format 1 (every pre-existing pack)", () => {
|
|
134
|
+
const result = validatePack({ name: "ok", version: "1.0.0" });
|
|
135
|
+
expect(result.ok).toBe(true);
|
|
136
|
+
expect(result.pack.schemaVersion).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
it("fails closed on a NEWER format with ONE upgrade error — no unknown-key noise", () => {
|
|
139
|
+
const result = validatePack({
|
|
140
|
+
schemaVersion: PACK_SCHEMA_VERSION + 1,
|
|
141
|
+
name: "future", version: "1.0.0",
|
|
142
|
+
someFutureField: { anything: true },
|
|
143
|
+
});
|
|
144
|
+
expect(result.ok).toBe(false);
|
|
145
|
+
expect(result.errors).toHaveLength(1);
|
|
146
|
+
expect(result.errors[0]).toContain(`supports up to ${PACK_SCHEMA_VERSION}`);
|
|
147
|
+
expect(result.errors[0]).toContain("upgrade");
|
|
148
|
+
});
|
|
149
|
+
it("rejects non-positive-integer schemaVersion values", () => {
|
|
150
|
+
for (const bad of [0, -1, 1.5, "1", null]) {
|
|
151
|
+
const result = validatePack({ schemaVersion: bad, name: "ok", version: "1.0.0" });
|
|
152
|
+
expect(result.ok, `schemaVersion ${JSON.stringify(bad)} should be rejected`).toBe(false);
|
|
153
|
+
expect(result.errors[0]).toContain("schemaVersion: must be a positive integer");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
it("hints the camelCase key for the snake_case spelling", () => {
|
|
157
|
+
const result = validatePack({ schema_version: 1, name: "ok", version: "1.0.0" });
|
|
158
|
+
expect(result.ok).toBe(false);
|
|
159
|
+
expect(result.errors.some((e) => e.includes('did you mean "schemaVersion"'))).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("parsePack", () => {
|
|
163
|
+
it("parses a YAML pack document", () => {
|
|
164
|
+
const result = parsePack(`
|
|
165
|
+
name: notion-research-agent
|
|
166
|
+
version: 1.0.0
|
|
167
|
+
tools: [search_knowledge, send_dm]
|
|
168
|
+
mcpServers:
|
|
169
|
+
- name: notion
|
|
170
|
+
url: https://mcp.notion.com/mcp
|
|
171
|
+
connection: notion
|
|
172
|
+
connections:
|
|
173
|
+
- service: notion
|
|
174
|
+
owner: agent
|
|
175
|
+
connectors:
|
|
176
|
+
- platform: email
|
|
177
|
+
mode: outbound
|
|
178
|
+
`);
|
|
179
|
+
expect(result.errors).toEqual([]);
|
|
180
|
+
expect(result.ok).toBe(true);
|
|
181
|
+
expect(result.pack?.mcpServers?.[0]?.connection).toBe("notion");
|
|
182
|
+
});
|
|
183
|
+
it("reports YAML syntax errors instead of throwing", () => {
|
|
184
|
+
const result = parsePack("name: [unclosed");
|
|
185
|
+
expect(result.ok).toBe(false);
|
|
186
|
+
expect(result.errors[0]).toContain("YAML parse error");
|
|
187
|
+
});
|
|
188
|
+
it("validates parsed JSON too (JSON is YAML)", () => {
|
|
189
|
+
const result = parsePack(JSON.stringify({ name: "j", version: "1.0.0" }));
|
|
190
|
+
expect(result.ok).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe("preset ↔ pack conversion (lossless round-trip)", () => {
|
|
194
|
+
const roundTrip = (preset) => packToPresetConfig(presetToPack(preset));
|
|
195
|
+
it("round-trips a minimal preset", () => {
|
|
196
|
+
const preset = { id: "defi-trader-v2" };
|
|
197
|
+
expect(roundTrip(preset)).toEqual(preset);
|
|
198
|
+
});
|
|
199
|
+
it("round-trips a full preset with every field", () => {
|
|
200
|
+
const preset = {
|
|
201
|
+
id: "research-biology",
|
|
202
|
+
version: 3,
|
|
203
|
+
trustLevel: "scanned",
|
|
204
|
+
failurePolicy: "abort",
|
|
205
|
+
maxCostNook: 0,
|
|
206
|
+
sources: [
|
|
207
|
+
{ type: "mining", label: "Traces", config: { domainTags: ["bio"], minScore: 0.7 } },
|
|
208
|
+
{ type: "aggregate", config: { domainTags: ["bio"] } },
|
|
209
|
+
{ type: "composite", config: { nested: { deep: [1, 2, 3] } } },
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
expect(roundTrip(preset)).toEqual(preset);
|
|
213
|
+
});
|
|
214
|
+
it("defaults pack version from the preset version", () => {
|
|
215
|
+
expect(presetToPack({ id: "p", version: 4 }).version).toBe("4.0.0");
|
|
216
|
+
expect(presetToPack({ id: "p" }).version).toBe("1.0.0");
|
|
217
|
+
});
|
|
218
|
+
it("honors explicit meta overrides", () => {
|
|
219
|
+
const pack = presetToPack({ id: "p" }, { name: "my-pack", version: "2.1.0", description: "d" });
|
|
220
|
+
expect(pack.name).toBe("my-pack");
|
|
221
|
+
expect(pack.version).toBe("2.1.0");
|
|
222
|
+
expect(pack.description).toBe("d");
|
|
223
|
+
});
|
|
224
|
+
it("throws when the converted pack would be invalid", () => {
|
|
225
|
+
expect(() => presetToPack({ id: "spaces are not a valid pack name" })).toThrow(/presetToPack/);
|
|
226
|
+
});
|
|
227
|
+
it("packToPresetConfig returns null when no preset is embedded", () => {
|
|
228
|
+
expect(packToPresetConfig({ name: "x", version: "1.0.0" })).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
it("conversion deep-copies — mutating the pack does not touch the preset", () => {
|
|
231
|
+
const preset = { id: "p", sources: [{ type: "mining", config: { a: 1 } }] };
|
|
232
|
+
const pack = presetToPack(preset);
|
|
233
|
+
pack.preset.sources[0].config.a = 999;
|
|
234
|
+
expect(preset.sources[0].config.a).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe("resolvePackActions", () => {
|
|
238
|
+
it("returns the declared tools, sorted", () => {
|
|
239
|
+
const actions = resolvePackActions({ name: "x", version: "1.0.0", tools: ["send_dm", "search_knowledge"] });
|
|
240
|
+
expect(actions).toEqual(["search_knowledge", "send_dm"]);
|
|
241
|
+
});
|
|
242
|
+
it("an email connector contributes every catalog action in the email category", () => {
|
|
243
|
+
const emailActions = Object.entries(ACTION_CATALOG)
|
|
244
|
+
.filter(([, info]) => info.category === "email")
|
|
245
|
+
.map(([name]) => name);
|
|
246
|
+
expect(emailActions.length).toBeGreaterThan(0);
|
|
247
|
+
const actions = resolvePackActions({
|
|
248
|
+
name: "x", version: "1.0.0",
|
|
249
|
+
connectors: [{ platform: "email", mode: "outbound" }],
|
|
250
|
+
});
|
|
251
|
+
expect(actions).toEqual([...emailActions].sort());
|
|
252
|
+
expect(actions).toContain("send_email");
|
|
253
|
+
});
|
|
254
|
+
it("a discord connector contributes no catalog actions (dispatcher-driven)", () => {
|
|
255
|
+
const actions = resolvePackActions({
|
|
256
|
+
name: "x", version: "1.0.0",
|
|
257
|
+
connectors: [{ platform: "discord", mode: "reactive" }],
|
|
258
|
+
});
|
|
259
|
+
expect(actions).toEqual([]);
|
|
260
|
+
});
|
|
261
|
+
it("dedupes overlap between tools and connector categories", () => {
|
|
262
|
+
const actions = resolvePackActions({
|
|
263
|
+
name: "x", version: "1.0.0",
|
|
264
|
+
tools: ["send_email"],
|
|
265
|
+
connectors: [{ platform: "email", mode: "outbound" }],
|
|
266
|
+
});
|
|
267
|
+
expect(actions.filter((a) => a === "send_email")).toHaveLength(1);
|
|
268
|
+
});
|
|
269
|
+
it("returns [] for a pack with no tools or connectors", () => {
|
|
270
|
+
expect(resolvePackActions({ name: "x", version: "1.0.0" })).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe("CONNECTOR_PLATFORM_MODES (risk posture)", () => {
|
|
274
|
+
it("chat connectors are reactive-only; email is outbound-only", () => {
|
|
275
|
+
expect(CONNECTOR_PLATFORM_MODES.discord).toEqual(["reactive"]);
|
|
276
|
+
expect(CONNECTOR_PLATFORM_MODES.telegram).toEqual(["reactive"]);
|
|
277
|
+
expect(CONNECTOR_PLATFORM_MODES.email).toEqual(["outbound"]);
|
|
278
|
+
});
|
|
279
|
+
it("CONNECTOR_PLATFORMS is the single registration point — modes view derives from it, every descriptor is complete", () => {
|
|
280
|
+
for (const [platform, descriptor] of Object.entries(CONNECTOR_PLATFORMS)) {
|
|
281
|
+
expect(CONNECTOR_PLATFORM_MODES[platform]).toEqual(descriptor.modes);
|
|
282
|
+
expect(descriptor.label.length).toBeGreaterThan(0);
|
|
283
|
+
if (!descriptor.hasBindings) {
|
|
284
|
+
// PackLoader surfaces readyDetail as the whole status line for
|
|
285
|
+
// bindings-less platforms — it must exist.
|
|
286
|
+
expect(descriptor.readyDetail, `${platform}: hasBindings=false requires readyDetail`).toBeTruthy();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
it("accepts a telegram reactive connector (Phase 4b) with no catalog contribution", () => {
|
|
291
|
+
const result = validatePack({
|
|
292
|
+
name: "tg", version: "1.0.0",
|
|
293
|
+
connectors: [{ platform: "telegram", mode: "reactive" }],
|
|
294
|
+
});
|
|
295
|
+
expect(result.ok).toBe(true);
|
|
296
|
+
expect(resolvePackActions(result.pack)).toEqual([]);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
//# sourceMappingURL=pack.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack.test.js","sourceRoot":"","sources":["../../src/__tests__/pack.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packLoader.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/packLoader.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackLoader (ROADMAP_external-mcp-connectors Phase 3): pre-flight connection
|
|
3
|
+
* checks (agent credential → oauth → workspace), idempotent MCP mounts with
|
|
4
|
+
* the right auth mapping, connector status reporting, and verbatim preset
|
|
5
|
+
* delegation to PresetLoader.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { PackLoader } from "../packLoader.js";
|
|
10
|
+
import { resolvePackActions } from "../pack.js";
|
|
11
|
+
vi.mock("node:fs/promises", () => ({
|
|
12
|
+
readFile: vi.fn().mockRejectedValue(new Error("no file")),
|
|
13
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
}));
|
|
15
|
+
const mockReadFile = vi.mocked(readFile);
|
|
16
|
+
function makeRuntime(state = {}) {
|
|
17
|
+
const s = {
|
|
18
|
+
credentials: [],
|
|
19
|
+
oauthConnected: [],
|
|
20
|
+
workspaceConnections: [],
|
|
21
|
+
bindings: [],
|
|
22
|
+
...state,
|
|
23
|
+
};
|
|
24
|
+
const request = vi.fn(async (method, path, ..._rest) => {
|
|
25
|
+
if (method === "GET" && path === "/v1/agents/me/credentials") {
|
|
26
|
+
return { credentials: s.credentials.map((service) => ({ service, createdAt: "now" })) };
|
|
27
|
+
}
|
|
28
|
+
if (method === "GET" && /^\/v1\/oauth\/[^/]+\/status$/.test(path)) {
|
|
29
|
+
const provider = decodeURIComponent(path.split("/")[3]);
|
|
30
|
+
return { connected: s.oauthConnected.includes(provider) };
|
|
31
|
+
}
|
|
32
|
+
if (method === "GET" && /^\/v1\/workspaces\/[^/]+\/connections$/.test(path)) {
|
|
33
|
+
return { connections: s.workspaceConnections.map((service) => ({ service })) };
|
|
34
|
+
}
|
|
35
|
+
if (method === "GET" && path === "/v1/discord/bindings") {
|
|
36
|
+
return { bindings: s.bindings };
|
|
37
|
+
}
|
|
38
|
+
if (method === "POST" && path === "/v1/forge/data/fetch") {
|
|
39
|
+
return {
|
|
40
|
+
sources: [{
|
|
41
|
+
source: "mining",
|
|
42
|
+
items: [{ content: "A useful biology fact about CRISPR editing.", metadata: {}, sourceTag: "t", contentHash: "" }],
|
|
43
|
+
itemCount: 1,
|
|
44
|
+
costNook: 0,
|
|
45
|
+
pricingContext: "test",
|
|
46
|
+
}],
|
|
47
|
+
totalCostNook: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// RAG / knowledge-graph probes → unavailable (memory fallback path).
|
|
51
|
+
throw Object.assign(new Error("not found"), { status: 404 });
|
|
52
|
+
});
|
|
53
|
+
const connectMcpServer = vi.fn().mockResolvedValue({ id: "srv_1", toolCount: 7, status: "connected" });
|
|
54
|
+
const storeMemory = vi.fn().mockResolvedValue({ id: "mem_1" });
|
|
55
|
+
const runtime = {
|
|
56
|
+
connection: { request, address: "0xAGENT", apiKey: "nk_test" },
|
|
57
|
+
tools: { connectMcpServer },
|
|
58
|
+
memory: { storeMemory },
|
|
59
|
+
};
|
|
60
|
+
return { runtime, request, connectMcpServer, storeMemory };
|
|
61
|
+
}
|
|
62
|
+
const NOTION_PACK = {
|
|
63
|
+
name: "notion-research-agent",
|
|
64
|
+
version: "1.0.0",
|
|
65
|
+
tools: ["search_knowledge"],
|
|
66
|
+
mcpServers: [{ name: "notion", url: "https://mcp.notion.com/mcp", connection: "notion" }],
|
|
67
|
+
connections: [{ service: "notion", owner: "agent" }],
|
|
68
|
+
};
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
vi.clearAllMocks();
|
|
71
|
+
mockReadFile.mockRejectedValue(new Error("no file"));
|
|
72
|
+
});
|
|
73
|
+
describe("connection pre-flight + auth mapping", () => {
|
|
74
|
+
it("agent credential satisfies the connection and mounts with bearer_credential", async () => {
|
|
75
|
+
const { runtime, connectMcpServer } = makeRuntime({ credentials: ["notion"] });
|
|
76
|
+
const report = await new PackLoader(runtime, { pack: NOTION_PACK }).load();
|
|
77
|
+
expect(report.connections).toEqual([
|
|
78
|
+
{ service: "notion", owner: "agent", status: "satisfied", authType: "bearer_credential" },
|
|
79
|
+
]);
|
|
80
|
+
expect(connectMcpServer).toHaveBeenCalledWith("https://mcp.notion.com/mcp", "notion", {
|
|
81
|
+
authType: "bearer_credential", credentialService: "notion",
|
|
82
|
+
});
|
|
83
|
+
expect(report.mcpServers).toEqual([
|
|
84
|
+
{ name: "notion", url: "https://mcp.notion.com/mcp", status: "mounted", toolCount: 7 },
|
|
85
|
+
]);
|
|
86
|
+
expect(report.ok).toBe(true);
|
|
87
|
+
expect(report.actions).toEqual(resolvePackActions(NOTION_PACK));
|
|
88
|
+
});
|
|
89
|
+
it("falls back to oauth when no credential is stored", async () => {
|
|
90
|
+
const { runtime, connectMcpServer } = makeRuntime({ oauthConnected: ["notion"] });
|
|
91
|
+
const report = await new PackLoader(runtime, { pack: NOTION_PACK }).load();
|
|
92
|
+
expect(report.connections[0]).toMatchObject({ status: "satisfied", authType: "oauth" });
|
|
93
|
+
expect(connectMcpServer).toHaveBeenCalledWith("https://mcp.notion.com/mcp", "notion", {
|
|
94
|
+
authType: "oauth", oauthProvider: "notion",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
it("reports missing connections with an actionable hint and skips the mount", async () => {
|
|
98
|
+
const { runtime, connectMcpServer } = makeRuntime();
|
|
99
|
+
const report = await new PackLoader(runtime, { pack: NOTION_PACK }).load();
|
|
100
|
+
expect(report.connections[0].status).toBe("missing");
|
|
101
|
+
expect(report.connections[0].hint).toContain("storeCredential");
|
|
102
|
+
expect(report.connections[0].hint).toContain("/v1/oauth/notion/connect");
|
|
103
|
+
expect(connectMcpServer).not.toHaveBeenCalled();
|
|
104
|
+
expect(report.mcpServers[0].status).toBe("skipped");
|
|
105
|
+
expect(report.mcpServers[0].reason).toContain('connection "notion" not satisfied');
|
|
106
|
+
expect(report.ok).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it("workspace-owned: requires a workspaceId, then mounts with workspace auth", async () => {
|
|
109
|
+
const teamPack = {
|
|
110
|
+
...NOTION_PACK,
|
|
111
|
+
connections: [{ service: "notion", owner: "workspace" }],
|
|
112
|
+
};
|
|
113
|
+
const ws = "11111111-2222-3333-4444-555555555555";
|
|
114
|
+
// No workspaceId → missing with a pointed hint.
|
|
115
|
+
const a = makeRuntime({ workspaceConnections: ["notion"] });
|
|
116
|
+
const noWs = await new PackLoader(a.runtime, { pack: teamPack }).load();
|
|
117
|
+
expect(noWs.connections[0].status).toBe("missing");
|
|
118
|
+
expect(noWs.connections[0].hint).toContain("workspaceId");
|
|
119
|
+
// With workspaceId + the workspace has the connection → workspace auth.
|
|
120
|
+
const b = makeRuntime({ workspaceConnections: ["notion"] });
|
|
121
|
+
const report = await new PackLoader(b.runtime, { pack: teamPack, workspaceId: ws }).load();
|
|
122
|
+
expect(report.connections[0]).toMatchObject({ status: "satisfied", authType: "workspace" });
|
|
123
|
+
expect(b.connectMcpServer).toHaveBeenCalledWith("https://mcp.notion.com/mcp", "notion", {
|
|
124
|
+
authType: "workspace", credentialService: "notion", workspaceId: ws,
|
|
125
|
+
});
|
|
126
|
+
// Workspace lacks the connection → admin hint.
|
|
127
|
+
const c = makeRuntime({ workspaceConnections: [] });
|
|
128
|
+
const missing = await new PackLoader(c.runtime, { pack: teamPack, workspaceId: ws }).load();
|
|
129
|
+
expect(missing.connections[0].status).toBe("missing");
|
|
130
|
+
expect(missing.connections[0].hint).toContain("workspace admin");
|
|
131
|
+
});
|
|
132
|
+
it("fetches the agent credential list once for multiple agent connections", async () => {
|
|
133
|
+
const pack = {
|
|
134
|
+
name: "multi", version: "1.0.0",
|
|
135
|
+
mcpServers: [
|
|
136
|
+
{ name: "notion", url: "https://mcp.notion.com/mcp", connection: "notion" },
|
|
137
|
+
{ name: "linear", url: "https://mcp.linear.app/mcp", connection: "linear" },
|
|
138
|
+
],
|
|
139
|
+
connections: [
|
|
140
|
+
{ service: "notion", owner: "agent" },
|
|
141
|
+
{ service: "linear", owner: "agent" },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
const { runtime, request } = makeRuntime({ credentials: ["notion", "linear"] });
|
|
145
|
+
await new PackLoader(runtime, { pack }).load();
|
|
146
|
+
const credCalls = request.mock.calls.filter(([, p]) => p === "/v1/agents/me/credentials");
|
|
147
|
+
expect(credCalls).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe("mount failures and unauthenticated servers", () => {
|
|
151
|
+
it("captures a mount failure and continues with the remaining servers", async () => {
|
|
152
|
+
const pack = {
|
|
153
|
+
name: "mixed", version: "1.0.0",
|
|
154
|
+
mcpServers: [
|
|
155
|
+
{ name: "notion", url: "https://mcp.notion.com/mcp", connection: "notion" },
|
|
156
|
+
{ name: "open-docs", url: "https://docs.example.com/mcp" },
|
|
157
|
+
],
|
|
158
|
+
connections: [{ service: "notion", owner: "agent" }],
|
|
159
|
+
};
|
|
160
|
+
const { runtime, connectMcpServer } = makeRuntime({ credentials: ["notion"] });
|
|
161
|
+
connectMcpServer
|
|
162
|
+
.mockRejectedValueOnce(new Error("502: dial failed"))
|
|
163
|
+
.mockResolvedValueOnce({ id: "srv_2", toolCount: 3 });
|
|
164
|
+
const report = await new PackLoader(runtime, { pack }).load();
|
|
165
|
+
expect(report.mcpServers[0]).toMatchObject({ name: "notion", status: "failed", error: "502: dial failed" });
|
|
166
|
+
expect(report.mcpServers[1]).toMatchObject({ name: "open-docs", status: "mounted", toolCount: 3 });
|
|
167
|
+
// Unauthenticated server mounts with no auth argument.
|
|
168
|
+
expect(connectMcpServer).toHaveBeenLastCalledWith("https://docs.example.com/mcp", "open-docs", undefined);
|
|
169
|
+
expect(report.ok).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe("connector status", () => {
|
|
173
|
+
const connectorPack = {
|
|
174
|
+
name: "chatty", version: "1.0.0",
|
|
175
|
+
connectors: [
|
|
176
|
+
{ platform: "discord", mode: "reactive" },
|
|
177
|
+
{ platform: "email", mode: "outbound" },
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
it("discord reports bound when an active binding exists", async () => {
|
|
181
|
+
const { runtime } = makeRuntime({ bindings: [{ status: "active" }, { status: "paused" }] });
|
|
182
|
+
const report = await new PackLoader(runtime, { pack: connectorPack }).load();
|
|
183
|
+
expect(report.connectors[0]).toMatchObject({ platform: "discord", status: "bound", detail: "1 active binding(s)" });
|
|
184
|
+
expect(report.connectors[1]).toMatchObject({ platform: "email", status: "ready" });
|
|
185
|
+
});
|
|
186
|
+
it("discord reports unbound with guidance when no bindings exist", async () => {
|
|
187
|
+
const { runtime } = makeRuntime();
|
|
188
|
+
const report = await new PackLoader(runtime, { pack: connectorPack }).load();
|
|
189
|
+
expect(report.connectors[0].status).toBe("unbound");
|
|
190
|
+
expect(report.connectors[0].detail).toContain("Discord install flow");
|
|
191
|
+
});
|
|
192
|
+
it("telegram connectors check /v1/telegram/bindings on the shared core (Phase 4b)", async () => {
|
|
193
|
+
const { runtime, request } = makeRuntime();
|
|
194
|
+
const base = request.getMockImplementation();
|
|
195
|
+
request.mockImplementation(async (method, path, ...rest) => {
|
|
196
|
+
if (method === "GET" && path === "/v1/telegram/bindings")
|
|
197
|
+
return { bindings: [{ status: "active" }] };
|
|
198
|
+
return base(method, path, ...rest);
|
|
199
|
+
});
|
|
200
|
+
const report = await new PackLoader(runtime, {
|
|
201
|
+
pack: { name: "tg", version: "1.0.0", connectors: [{ platform: "telegram", mode: "reactive" }] },
|
|
202
|
+
}).load();
|
|
203
|
+
expect(report.connectors[0]).toMatchObject({ platform: "telegram", status: "bound", detail: "1 active binding(s)" });
|
|
204
|
+
});
|
|
205
|
+
it("email connector actions surface in the pack action set", async () => {
|
|
206
|
+
const { runtime } = makeRuntime();
|
|
207
|
+
const report = await new PackLoader(runtime, { pack: connectorPack }).load();
|
|
208
|
+
expect(report.actions).toContain("send_email");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe("preset delegation", () => {
|
|
212
|
+
it("loads the embedded preset block verbatim through PresetLoader", async () => {
|
|
213
|
+
const pack = {
|
|
214
|
+
name: "with-knowledge", version: "1.0.0",
|
|
215
|
+
preset: {
|
|
216
|
+
id: "research-biology",
|
|
217
|
+
sources: [{ type: "mining", label: "Bio", config: { domainTags: ["biology"] } }],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
const { runtime, request, storeMemory } = makeRuntime();
|
|
221
|
+
const report = await new PackLoader(runtime, { pack }).load();
|
|
222
|
+
const fetchCall = request.mock.calls.find(([m, p]) => m === "POST" && p === "/v1/forge/data/fetch");
|
|
223
|
+
expect(fetchCall?.[2]).toMatchObject({ presetId: "research-biology" });
|
|
224
|
+
expect(report.preset?.totalItems).toBe(1);
|
|
225
|
+
expect(storeMemory).toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
it("omits the preset result when the pack has no preset block", async () => {
|
|
228
|
+
const { runtime, request } = makeRuntime();
|
|
229
|
+
const report = await new PackLoader(runtime, { pack: { name: "min", version: "1.0.0" } }).load();
|
|
230
|
+
expect(report.preset).toBeUndefined();
|
|
231
|
+
expect(request.mock.calls.some(([, p]) => p === "/v1/forge/data/fetch")).toBe(false);
|
|
232
|
+
expect(report.ok).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
describe("pack file loading", () => {
|
|
236
|
+
it("reads + parses the pack from packPath", async () => {
|
|
237
|
+
mockReadFile.mockResolvedValueOnce(`
|
|
238
|
+
name: from-file
|
|
239
|
+
version: 1.0.0
|
|
240
|
+
tools: [search_knowledge]
|
|
241
|
+
`);
|
|
242
|
+
const { runtime } = makeRuntime();
|
|
243
|
+
const report = await new PackLoader(runtime, { packPath: "./my.pack.yaml" }).load();
|
|
244
|
+
expect(mockReadFile).toHaveBeenCalledWith("./my.pack.yaml", "utf-8");
|
|
245
|
+
expect(report.name).toBe("from-file");
|
|
246
|
+
expect(report.source).toBe("file");
|
|
247
|
+
expect(report.actions).toEqual(["search_knowledge"]);
|
|
248
|
+
});
|
|
249
|
+
it("throws with the collected validation errors for an invalid pack file", async () => {
|
|
250
|
+
mockReadFile.mockResolvedValueOnce("name: '!bad'\nversion: nope\n");
|
|
251
|
+
const { runtime } = makeRuntime();
|
|
252
|
+
await expect(new PackLoader(runtime, { packPath: "./bad.pack.yaml" }).load())
|
|
253
|
+
.rejects.toThrow(/is invalid[\s\S]*version/);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe("registry reference loading (packRef)", () => {
|
|
257
|
+
const REGISTRY_PACK = {
|
|
258
|
+
name: "notion-research-agent",
|
|
259
|
+
version: "1.2.0",
|
|
260
|
+
content: "name: notion-research-agent\nversion: 1.2.0\ntools: [search_knowledge]\n",
|
|
261
|
+
contentHash: "c".repeat(64),
|
|
262
|
+
};
|
|
263
|
+
function withRegistry(state = {}, pack = REGISTRY_PACK) {
|
|
264
|
+
const made = makeRuntime(state);
|
|
265
|
+
const base = made.request.getMockImplementation();
|
|
266
|
+
made.request.mockImplementation(async (method, path, ...rest) => {
|
|
267
|
+
if (method === "POST" && path === "/v1/agents/me/pack/load")
|
|
268
|
+
return { pack };
|
|
269
|
+
return base(method, path, ...rest);
|
|
270
|
+
});
|
|
271
|
+
return made;
|
|
272
|
+
}
|
|
273
|
+
it("fetches by name@version, validates, and reports source + hash", async () => {
|
|
274
|
+
const { runtime, request } = withRegistry();
|
|
275
|
+
const report = await new PackLoader(runtime, { packRef: "notion-research-agent@1.2.0" }).load();
|
|
276
|
+
const loadCall = request.mock.calls.find(([m, p]) => m === "POST" && p === "/v1/agents/me/pack/load");
|
|
277
|
+
expect(loadCall?.[2]).toEqual({ name: "notion-research-agent", version: "1.2.0" });
|
|
278
|
+
expect(report.source).toBe("registry");
|
|
279
|
+
expect(report.contentHash).toBe(REGISTRY_PACK.contentHash);
|
|
280
|
+
expect(report.actions).toEqual(["search_knowledge"]);
|
|
281
|
+
});
|
|
282
|
+
it("omits version for a bare name ref (registry resolves latest)", async () => {
|
|
283
|
+
const { runtime, request } = withRegistry();
|
|
284
|
+
await new PackLoader(runtime, { packRef: "notion-research-agent" }).load();
|
|
285
|
+
const loadCall = request.mock.calls.find(([m, p]) => m === "POST" && p === "/v1/agents/me/pack/load");
|
|
286
|
+
expect(loadCall?.[2]).toEqual({ name: "notion-research-agent" });
|
|
287
|
+
});
|
|
288
|
+
it("re-validates registry content and rejects invalid packs", async () => {
|
|
289
|
+
const { runtime } = withRegistry({}, { ...REGISTRY_PACK, content: "name: '!bad'\nversion: nope\n" });
|
|
290
|
+
await expect(new PackLoader(runtime, { packRef: "notion-research-agent" }).load())
|
|
291
|
+
.rejects.toThrow(/failed validation/);
|
|
292
|
+
});
|
|
293
|
+
it("inline pack takes precedence over packRef", async () => {
|
|
294
|
+
const { runtime, request } = withRegistry();
|
|
295
|
+
const report = await new PackLoader(runtime, {
|
|
296
|
+
pack: { name: "inline-pack", version: "1.0.0" },
|
|
297
|
+
packRef: "notion-research-agent",
|
|
298
|
+
}).load();
|
|
299
|
+
expect(report.name).toBe("inline-pack");
|
|
300
|
+
expect(report.source).toBe("inline");
|
|
301
|
+
expect(request.mock.calls.some(([, p]) => p === "/v1/agents/me/pack/load")).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
//# sourceMappingURL=packLoader.test.js.map
|