@stigmer/mcp-server 3.35.0 → 3.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/cli/mcp-server-stigmer.d.ts.map +1 -1
  2. package/cli/mcp-server-stigmer.js +1 -1
  3. package/cli/mcp-server-stigmer.js.map +1 -1
  4. package/config.d.ts.map +1 -1
  5. package/config.js.map +1 -1
  6. package/domains/agentexecutions/approve.d.ts.map +1 -1
  7. package/domains/agentexecutions/approve.js.map +1 -1
  8. package/domains/agentexecutions/fetch.d.ts.map +1 -1
  9. package/domains/agentexecutions/fetch.js.map +1 -1
  10. package/domains/agentexecutions/run.d.ts.map +1 -1
  11. package/domains/agentexecutions/run.js.map +1 -1
  12. package/domains/agentexecutions/tools.d.ts.map +1 -1
  13. package/domains/agentexecutions/tools.js.map +1 -1
  14. package/domains/agents/apply.d.ts.map +1 -1
  15. package/domains/agents/apply.js.map +1 -1
  16. package/domains/agents/delete.d.ts.map +1 -1
  17. package/domains/agents/delete.js.map +1 -1
  18. package/domains/agents/fetch.d.ts.map +1 -1
  19. package/domains/agents/fetch.js.map +1 -1
  20. package/domains/agents/resources.d.ts.map +1 -1
  21. package/domains/agents/resources.js.map +1 -1
  22. package/domains/agents/tools.d.ts.map +1 -1
  23. package/domains/agents/tools.js.map +1 -1
  24. package/domains/apply-visibility.d.ts.map +1 -1
  25. package/domains/apply-visibility.js.map +1 -1
  26. package/domains/channels/calls.d.ts.map +1 -1
  27. package/domains/channels/calls.js.map +1 -1
  28. package/domains/channels/errors.d.ts.map +1 -1
  29. package/domains/channels/errors.js.map +1 -1
  30. package/domains/channels/tools.d.ts.map +1 -1
  31. package/domains/channels/tools.js.map +1 -1
  32. package/domains/client.d.ts.map +1 -1
  33. package/domains/client.js.map +1 -1
  34. package/domains/conversation/calls.d.ts.map +1 -1
  35. package/domains/conversation/calls.js.map +1 -1
  36. package/domains/conversation/errors.d.ts.map +1 -1
  37. package/domains/conversation/errors.js.map +1 -1
  38. package/domains/conversation/tools.d.ts.map +1 -1
  39. package/domains/conversation/tools.js.map +1 -1
  40. package/domains/environments/apply.d.ts.map +1 -1
  41. package/domains/environments/apply.js.map +1 -1
  42. package/domains/environments/delete.d.ts.map +1 -1
  43. package/domains/environments/delete.js.map +1 -1
  44. package/domains/environments/fetch.d.ts.map +1 -1
  45. package/domains/environments/fetch.js.map +1 -1
  46. package/domains/environments/resources.d.ts.map +1 -1
  47. package/domains/environments/resources.js.map +1 -1
  48. package/domains/environments/tools.d.ts.map +1 -1
  49. package/domains/environments/tools.js.map +1 -1
  50. package/domains/executions/cancel.d.ts.map +1 -1
  51. package/domains/executions/cancel.js.map +1 -1
  52. package/domains/executions/tools.d.ts.map +1 -1
  53. package/domains/executions/tools.js.map +1 -1
  54. package/domains/marshal.d.ts.map +1 -1
  55. package/domains/marshal.js.map +1 -1
  56. package/domains/mcpservers/apply.d.ts.map +1 -1
  57. package/domains/mcpservers/apply.js.map +1 -1
  58. package/domains/mcpservers/delete.d.ts.map +1 -1
  59. package/domains/mcpservers/delete.js.map +1 -1
  60. package/domains/mcpservers/fetch.d.ts.map +1 -1
  61. package/domains/mcpservers/fetch.js.map +1 -1
  62. package/domains/mcpservers/resources.d.ts.map +1 -1
  63. package/domains/mcpservers/resources.js.map +1 -1
  64. package/domains/mcpservers/tools.d.ts.map +1 -1
  65. package/domains/mcpservers/tools.js.map +1 -1
  66. package/domains/memory/calls.d.ts.map +1 -1
  67. package/domains/memory/calls.js.map +1 -1
  68. package/domains/memory/context.d.ts.map +1 -1
  69. package/domains/memory/context.js.map +1 -1
  70. package/domains/memory/errors.d.ts.map +1 -1
  71. package/domains/memory/errors.js.map +1 -1
  72. package/domains/memory/tools.d.ts.map +1 -1
  73. package/domains/memory/tools.js.map +1 -1
  74. package/domains/resourcehandler.d.ts.map +1 -1
  75. package/domains/resourcehandler.js.map +1 -1
  76. package/domains/resourceuri.d.ts.map +1 -1
  77. package/domains/resourceuri.js.map +1 -1
  78. package/domains/rpcerr.d.ts.map +1 -1
  79. package/domains/rpcerr.js.map +1 -1
  80. package/domains/search/tools.d.ts.map +1 -1
  81. package/domains/search/tools.js.map +1 -1
  82. package/domains/skills/delete.d.ts.map +1 -1
  83. package/domains/skills/delete.js.map +1 -1
  84. package/domains/skills/fetch.d.ts.map +1 -1
  85. package/domains/skills/fetch.js.map +1 -1
  86. package/domains/skills/resources.d.ts.map +1 -1
  87. package/domains/skills/resources.js.map +1 -1
  88. package/domains/skills/tools.d.ts.map +1 -1
  89. package/domains/skills/tools.js.map +1 -1
  90. package/domains/skills/versions.d.ts.map +1 -1
  91. package/domains/skills/versions.js.map +1 -1
  92. package/domains/toolresult.d.ts.map +1 -1
  93. package/domains/toolresult.js.map +1 -1
  94. package/domains/workflowexecutions/approvals.d.ts.map +1 -1
  95. package/domains/workflowexecutions/approvals.js.map +1 -1
  96. package/domains/workflowexecutions/run.d.ts.map +1 -1
  97. package/domains/workflowexecutions/run.js.map +1 -1
  98. package/domains/workflowexecutions/tools.d.ts.map +1 -1
  99. package/domains/workflowexecutions/tools.js.map +1 -1
  100. package/domains/workflows/apply.d.ts.map +1 -1
  101. package/domains/workflows/apply.js.map +1 -1
  102. package/domains/workflows/delete.d.ts.map +1 -1
  103. package/domains/workflows/delete.js.map +1 -1
  104. package/domains/workflows/fetch.d.ts.map +1 -1
  105. package/domains/workflows/fetch.js.map +1 -1
  106. package/domains/workflows/resources.d.ts.map +1 -1
  107. package/domains/workflows/resources.js.map +1 -1
  108. package/domains/workflows/taskkinds.d.ts.map +1 -1
  109. package/domains/workflows/taskkinds.js.map +1 -1
  110. package/domains/workflows/tools.d.ts.map +1 -1
  111. package/domains/workflows/tools.js.map +1 -1
  112. package/domains/workflows/validate.d.ts.map +1 -1
  113. package/domains/workflows/validate.js.map +1 -1
  114. package/domains/workflows/versions.d.ts.map +1 -1
  115. package/domains/workflows/versions.js.map +1 -1
  116. package/gen/agent.d.ts.map +1 -1
  117. package/gen/agent.js.map +1 -1
  118. package/gen/apply-runtime.d.ts.map +1 -1
  119. package/gen/apply-runtime.js.map +1 -1
  120. package/gen/environment.d.ts.map +1 -1
  121. package/gen/environment.js.map +1 -1
  122. package/gen/mcpserver.d.ts.map +1 -1
  123. package/gen/mcpserver.js.map +1 -1
  124. package/gen/workflow.d.ts.map +1 -1
  125. package/gen/workflow.js.map +1 -1
  126. package/index.d.ts.map +1 -1
  127. package/index.js.map +1 -1
  128. package/logger.d.ts.map +1 -1
  129. package/logger.js.map +1 -1
  130. package/package.json +3 -3
  131. package/readiness.d.ts.map +1 -1
  132. package/readiness.js.map +1 -1
  133. package/server.d.ts.map +1 -1
  134. package/server.js.map +1 -1
  135. package/src/config.test.ts +0 -88
  136. package/src/domains/apply.integration.test.ts +0 -313
  137. package/src/domains/channels/channels.integration.test.ts +0 -272
  138. package/src/domains/channels/errors.test.ts +0 -52
  139. package/src/domains/conversation/conversation.integration.test.ts +0 -233
  140. package/src/domains/deletes.integration.test.ts +0 -147
  141. package/src/domains/executions.integration.test.ts +0 -376
  142. package/src/domains/memory/context.test.ts +0 -95
  143. package/src/domains/memory/errors.test.ts +0 -54
  144. package/src/domains/memory/memory.integration.test.ts +0 -253
  145. package/src/domains/reads.integration.test.ts +0 -163
  146. package/src/domains/resources.integration.test.ts +0 -157
  147. package/src/domains/resourceuri.test.ts +0 -104
  148. package/src/domains/rpcerr.test.ts +0 -62
  149. package/src/domains/search/search.integration.test.ts +0 -141
  150. package/src/domains/versions.integration.test.ts +0 -212
  151. package/src/domains/workflows/validate.integration.test.ts +0 -117
  152. package/src/domains/workflows/workflow-tools.integration.test.ts +0 -148
  153. package/src/http.integration.test.ts +0 -235
  154. package/src/readiness.test.ts +0 -90
  155. package/src/server.integration.test.ts +0 -82
@@ -1,313 +0,0 @@
1
- // In-process integration test for the apply tools. Drives apply_agent,
2
- // apply_mcp_server, apply_workflow, and apply_environment through the
3
- // full MCP boundary and asserts the codegen projection (src/gen/*)
4
- // reconstitutes the proto correctly: metadata hoist + slug generation,
5
- // enum-string conversion,
6
- // ApiResourceReference kind injection, the stdio/http oneof, the environment
7
- // data map with secret flags, and the recursive workflow task_config expansion
8
- // (http_call leaf, fork/for_each nesting).
9
-
10
- import { clone } from "@bufbuild/protobuf";
11
- import type { ConnectRouter } from "@connectrpc/connect";
12
- import { connectNodeAdapter } from "@connectrpc/connect-node";
13
- import {
14
- createServer as createHttp2Server,
15
- type Http2Server,
16
- type ServerHttp2Session,
17
- } from "node:http2";
18
- import type { AddressInfo } from "node:net";
19
-
20
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
21
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
22
- import { type Agent, AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
23
- import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
24
- import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
25
- import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
26
- import type { McpServer as McpServerProto } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
27
- import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
28
- import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
29
- import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
30
- import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
31
- import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
32
- import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
33
- import type { UpdateVisibilityInput } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
34
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
35
-
36
- import { configureLogger } from "../logger";
37
- import { createServer } from "../server";
38
-
39
- configureLogger({ level: "error", format: "text" });
40
-
41
- let backend: Http2Server;
42
- let client: Client;
43
- let appliedAgent: Agent | undefined;
44
- let appliedMcpServer: McpServerProto | undefined;
45
- let appliedWorkflow: Workflow | undefined;
46
- let appliedEnvironment: Environment | undefined;
47
- const visibilityUpdates: UpdateVisibilityInput[] = [];
48
-
49
- // The single-door contract (oss#573): a plain update preserves the STORED
50
- // visibility, so the apply response can disagree with the request. This slug
51
- // makes the mock backend simulate that — apply echoes back a pre-existing
52
- // resource whose stored level is org, regardless of the requested one.
53
- const STORED_ORG_SLUG = "stored-org-agent";
54
- const openSessions = new Set<ServerHttp2Session>();
55
-
56
- interface ToolResult {
57
- content: Array<{ type: string; text?: string }>;
58
- isError?: boolean;
59
- }
60
-
61
- async function callTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {
62
- return (await client.callTool({ name, arguments: args })) as ToolResult;
63
- }
64
-
65
- beforeAll(async () => {
66
- const routes = (router: ConnectRouter) => {
67
- router.service(AgentCommandController, {
68
- apply: (req) => {
69
- appliedAgent = req;
70
- if (req.metadata?.slug === STORED_ORG_SLUG) {
71
- const stored = clone(AgentSchema, req);
72
- stored.metadata!.id = "agent-stored-1";
73
- stored.metadata!.visibility = ApiResourceVisibility.visibility_org;
74
- return stored;
75
- }
76
- return req;
77
- },
78
- updateVisibility: (input) => {
79
- visibilityUpdates.push(input);
80
- const updated = clone(AgentSchema, appliedAgent!);
81
- updated.metadata!.id = input.resourceId;
82
- updated.metadata!.visibility = input.visibility;
83
- return updated;
84
- },
85
- });
86
- router.service(McpServerCommandController, {
87
- apply: (req) => {
88
- appliedMcpServer = req;
89
- return req;
90
- },
91
- });
92
- router.service(WorkflowCommandController, {
93
- apply: (req) => {
94
- appliedWorkflow = req;
95
- return req;
96
- },
97
- });
98
- router.service(EnvironmentCommandController, {
99
- apply: (req) => {
100
- appliedEnvironment = req;
101
- return req;
102
- },
103
- });
104
- };
105
- backend = createHttp2Server(connectNodeAdapter({ routes }));
106
- backend.on("session", (session) => {
107
- openSessions.add(session);
108
- session.on("close", () => openSessions.delete(session));
109
- });
110
- await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
111
- const port = (backend.address() as AddressInfo).port;
112
-
113
- const mcp = createServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
114
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
115
- client = new Client({ name: "apply-integration", version: "test" });
116
- await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
117
- });
118
-
119
- afterAll(async () => {
120
- await client?.close();
121
- for (const session of openSessions) session.destroy();
122
- await new Promise<void>((resolve) => backend.close(() => resolve()));
123
- });
124
-
125
- describe("apply tools integration", () => {
126
- it("advertises every apply tool", async () => {
127
- const { tools } = await client.listTools();
128
- expect(tools.map((t) => t.name)).toEqual(
129
- expect.arrayContaining([
130
- "apply_agent",
131
- "apply_mcp_server",
132
- "apply_workflow",
133
- "apply_environment",
134
- ]),
135
- );
136
- });
137
-
138
- it("apply_agent hoists metadata, generates slug, and injects reference kinds", async () => {
139
- const result = await callTool("apply_agent", {
140
- name: "Code Reviewer",
141
- org: "acme",
142
- visibility: "visibility_platform",
143
- instructions: "Review code carefully.",
144
- skill_refs: [{ slug: "web-search", version: "stable" }],
145
- mcp_server_usages: [{ mcp_server_ref: { slug: "github" }, enabled_tools: ["create_pr"] }],
146
- });
147
- expect(result.isError).toBeFalsy();
148
-
149
- const agent = appliedAgent;
150
- expect(agent?.apiVersion).toBe("agentic.stigmer.ai/v1");
151
- expect(agent?.kind).toBe("Agent");
152
- expect(agent?.metadata?.name).toBe("Code Reviewer");
153
- expect(agent?.metadata?.slug).toBe("code-reviewer"); // auto-generated from name
154
- expect(agent?.metadata?.org).toBe("acme");
155
- expect(agent?.metadata?.visibility).toBe(ApiResourceVisibility.visibility_platform);
156
- expect(agent?.spec?.instructions).toBe("Review code carefully.");
157
-
158
- const skillRef = agent?.spec?.skillRefs?.[0];
159
- expect(skillRef?.slug).toBe("web-search");
160
- expect(skillRef?.kind).toBe(ApiResourceKind.skill);
161
- expect(skillRef?.version).toBe("stable"); // version kept for versioned kind
162
-
163
- const usage = agent?.spec?.mcpServerUsages?.[0];
164
- expect(usage?.mcpServerRef?.slug).toBe("github");
165
- expect(usage?.mcpServerRef?.kind).toBe(ApiResourceKind.mcp_server);
166
- expect(usage?.enabledTools).toEqual(["create_pr"]);
167
- });
168
-
169
- it("apply_mcp_server rebuilds the stdio oneof", async () => {
170
- const result = await callTool("apply_mcp_server", {
171
- name: "GitHub",
172
- org: "acme",
173
- stdio: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
174
- });
175
- expect(result.isError).toBeFalsy();
176
- expect(appliedMcpServer?.spec?.serverType?.case).toBe("stdio");
177
- expect(appliedMcpServer?.spec?.serverType?.value).toMatchObject({ command: "npx" });
178
- });
179
-
180
- it("apply_mcp_server rebuilds the http oneof", async () => {
181
- const result = await callTool("apply_mcp_server", {
182
- name: "Remote",
183
- org: "acme",
184
- http: { url: "https://mcp.example.com/v1" },
185
- });
186
- expect(result.isError).toBeFalsy();
187
- expect(appliedMcpServer?.spec?.serverType?.case).toBe("http");
188
- expect(appliedMcpServer?.spec?.serverType?.value).toMatchObject({
189
- url: "https://mcp.example.com/v1",
190
- });
191
- });
192
-
193
- it("apply_workflow expands typed task_config and nests recursive tasks", async () => {
194
- const result = await callTool("apply_workflow", {
195
- name: "Triage",
196
- org: "acme",
197
- document: { namespace: "support", name: "triage", version: "1.0.0" },
198
- tasks: [
199
- {
200
- name: "fetch",
201
- kind: "http_call",
202
- http_call: { method: "POST", endpoint: { uri: "https://api.example.com" } },
203
- },
204
- {
205
- name: "parallel",
206
- kind: "fork",
207
- fork: {
208
- branches: [
209
- {
210
- name: "b1",
211
- do: [{ name: "set_x", kind: "set_vars", set_vars: { variables: { x: "1" } } }],
212
- },
213
- ],
214
- },
215
- },
216
- {
217
- name: "loop",
218
- kind: "for_each",
219
- for_each: {
220
- each: "item",
221
- in: "${ $data.items }",
222
- do: [
223
- {
224
- name: "call",
225
- kind: "http_call",
226
- http_call: { method: "GET", endpoint: { uri: "https://api.example.com/item" } },
227
- },
228
- ],
229
- },
230
- },
231
- ],
232
- });
233
- expect(result.isError).toBeFalsy();
234
-
235
- const wf = appliedWorkflow;
236
- expect(wf?.apiVersion).toBe("agentic.stigmer.ai/v1");
237
- expect(wf?.metadata?.slug).toBe("triage");
238
- expect(wf?.spec?.document?.namespace).toBe("support");
239
-
240
- const tasks = wf?.spec?.tasks ?? [];
241
- expect(tasks).toHaveLength(3);
242
-
243
- // http_call leaf: task_config Struct carries the serialized config.
244
- expect(tasks[0]?.kind).toBe(WorkflowTaskKind.http_call);
245
- expect(tasks[0]?.taskConfig).toMatchObject({ method: "POST" });
246
-
247
- // fork: recursive nesting — branches[].do[] are full tasks.
248
- expect(tasks[1]?.kind).toBe(WorkflowTaskKind.fork);
249
- const forkCfg = tasks[1]?.taskConfig as { branches?: Array<{ do?: unknown[] }> };
250
- expect(forkCfg.branches?.[0]?.do).toHaveLength(1);
251
-
252
- // for_each: recursive nesting via z.lazy.
253
- expect(tasks[2]?.kind).toBe(WorkflowTaskKind.for_each);
254
- const forCfg = tasks[2]?.taskConfig as { each?: string; do?: unknown[] };
255
- expect(forCfg.each).toBe("item");
256
- expect(forCfg.do).toHaveLength(1);
257
- });
258
-
259
- it("apply_agent lands a declared visibility the update preserved away via UpdateVisibility (oss#573)", async () => {
260
- const result = await callTool("apply_agent", {
261
- name: "Stored Org Agent",
262
- org: "acme",
263
- visibility: "visibility_platform",
264
- instructions: "i",
265
- });
266
- expect(result.isError).toBeFalsy();
267
-
268
- // Server "preserved" stored org; the tool must follow up once through
269
- // the guarded door and return the landed level, not the stale one.
270
- expect(visibilityUpdates).toHaveLength(1);
271
- expect(visibilityUpdates[0].resourceId).toBe("agent-stored-1");
272
- expect(visibilityUpdates[0].visibility).toBe(ApiResourceVisibility.visibility_platform);
273
- const text = result.content.find((c) => c.type === "text")?.text ?? "";
274
- expect(JSON.parse(text).metadata.visibility).toBe("visibility_platform");
275
- });
276
-
277
- it("apply_agent skips the follow-up when the server already matches", async () => {
278
- visibilityUpdates.length = 0;
279
- const result = await callTool("apply_agent", {
280
- name: "Plain Agent",
281
- org: "acme",
282
- visibility: "visibility_platform",
283
- instructions: "i",
284
- });
285
- expect(result.isError).toBeFalsy();
286
- // The echo backend returns the requested level (and no id) — no diff,
287
- // no follow-up.
288
- expect(visibilityUpdates).toHaveLength(0);
289
- });
290
-
291
- it("apply_environment rebuilds the data map with secret flags", async () => {
292
- const result = await callTool("apply_environment", {
293
- name: "GitHub Creds",
294
- org: "acme",
295
- data: {
296
- API_KEY: { value: "sk-real-value", is_secret: true, description: "GitHub PAT" },
297
- REGION: { value: "us-east-1" },
298
- },
299
- });
300
- expect(result.isError).toBeFalsy();
301
-
302
- const env = appliedEnvironment;
303
- expect(env?.apiVersion).toBe("agentic.stigmer.ai/v1");
304
- expect(env?.kind).toBe("Environment");
305
- expect(env?.metadata?.slug).toBe("github-creds"); // auto-generated from name
306
- expect(env?.spec?.data?.API_KEY).toMatchObject({
307
- value: "sk-real-value",
308
- isSecret: true,
309
- description: "GitHub PAT",
310
- });
311
- expect(env?.spec?.data?.REGION?.isSecret).toBe(false);
312
- });
313
- });
@@ -1,272 +0,0 @@
1
- // In-process integration test for the channels roster (the
2
- // records.integration.test.ts pattern: real Connect backend with a
3
- // stubbed messaging service, real MCP client over an in-memory
4
- // transport).
5
- //
6
- // Verifies the DD-006 D5/D8 contract surface:
7
- // - the channels-only roster is exactly send_channel_message with NO
8
- // org argument (agent audience);
9
- // - argument → request mapping mirrors the ChannelOutboundPayload
10
- // oneof (text | template, exactly one, checked before any RPC);
11
- // - typed outcomes (accepted/queued/refused) are ANSWERS — success
12
- // results the model adapts to, never isError;
13
- // - the channels-own error mapper passes domain messages verbatim as
14
- // {error, code, reason} JSON; transport errors delegate to the
15
- // shared classifier.
16
-
17
- import { create } from "@bufbuild/protobuf";
18
- import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
19
- import { connectNodeAdapter } from "@connectrpc/connect-node";
20
- import {
21
- createServer as createHttp2Server,
22
- type Http2Server,
23
- type ServerHttp2Session,
24
- } from "node:http2";
25
- import type { AddressInfo } from "node:net";
26
-
27
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
28
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
29
- import { ChannelMessageCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_command_pb";
30
- import {
31
- ChannelSendOutcome,
32
- SendChannelMessageOutputSchema,
33
- type SendChannelMessageInput,
34
- } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
35
- import { ErrorInfoSchema } from "@stigmer/protos/google/rpc/error_details_pb";
36
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
37
-
38
- import { configureLogger } from "../../logger";
39
- import { CHANNELS_ROUTE, createChannelsServer } from "../../server";
40
-
41
- configureLogger({ level: "error", format: "text" });
42
-
43
- let backend: Http2Server;
44
- let client: Client;
45
- const openSessions = new Set<ServerHttp2Session>();
46
-
47
- /** The next stubbed outcome; tests set this. */
48
- let sendResponse: () => ReturnType<typeof create<typeof SendChannelMessageOutputSchema>>;
49
-
50
- /** Requests the stub captured (object property for closure narrowing). */
51
- const captured: { send?: SendChannelMessageInput } = {};
52
-
53
- interface ToolResult {
54
- content: Array<{ type: string; text?: string }>;
55
- isError?: boolean;
56
- }
57
-
58
- async function send(args: Record<string, unknown>): Promise<ToolResult> {
59
- return (await client.callTool({
60
- name: "send_channel_message",
61
- arguments: args,
62
- })) as ToolResult;
63
- }
64
-
65
- beforeAll(async () => {
66
- const routes = (router: ConnectRouter) => {
67
- router.service(ChannelMessageCommandController, {
68
- sendMessage: (req) => {
69
- captured.send = req;
70
- return sendResponse();
71
- },
72
- });
73
- };
74
- backend = createHttp2Server(connectNodeAdapter({ routes }));
75
- backend.on("session", (session) => {
76
- openSessions.add(session);
77
- session.on("close", () => openSessions.delete(session));
78
- });
79
- await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
80
- const port = (backend.address() as AddressInfo).port;
81
-
82
- const mcp = createChannelsServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
83
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
84
- client = new Client({ name: "channels-integration", version: "test" });
85
- await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
86
- });
87
-
88
- afterAll(async () => {
89
- await client?.close();
90
- for (const session of openSessions) session.destroy();
91
- await new Promise<void>((resolve) => backend.close(() => resolve()));
92
- });
93
-
94
- describe("channels roster (DD-006 D8)", () => {
95
- it("exposes exactly send_channel_message with no org argument", async () => {
96
- const { tools } = await client.listTools();
97
- expect(tools.map((t) => t.name)).toEqual(["send_channel_message"]);
98
-
99
- // The agent audience gets no org argument — a session-bound
100
- // caller's org is server-derived, and offering the argument would
101
- // only invite INVALID_ARGUMENT rejections (the records T05 R3 rule).
102
- const properties = (tools[0].inputSchema as { properties?: Record<string, unknown> })
103
- .properties;
104
- expect(Object.keys(properties ?? {}).sort()).toEqual([
105
- "channel",
106
- "recipient",
107
- "template",
108
- "text",
109
- ]);
110
- });
111
-
112
- it("pins the cross-repo attachment strings (the TOOL_CALL_LIMIT precedent)", () => {
113
- // The runner's channel-attachment.ts builds these independently
114
- // (shared/channel-attachment.ts and its mirror guard); a drift here
115
- // strands every synthesized attachment on a 404 or a full roster.
116
- expect(CHANNELS_ROUTE).toBe("/channels");
117
- });
118
- });
119
-
120
- describe("argument → request mapping (the ChannelOutboundPayload oneof)", () => {
121
- it("maps a text send; org never travels, channel defaults empty", async () => {
122
- sendResponse = () =>
123
- create(SendChannelMessageOutputSchema, {
124
- outcome: ChannelSendOutcome.accepted,
125
- outboundMessageId: "chom_1",
126
- providerMessageId: "wamid.abc",
127
- });
128
- captured.send = undefined;
129
-
130
- const result = await send({ recipient: "919000000001", text: "see you at 6" });
131
-
132
- expect(result.isError).toBeFalsy();
133
- const req = captured.send as SendChannelMessageInput | undefined;
134
- expect(req?.recipient).toBe("919000000001");
135
- expect(req?.org).toBe(""); // agent audience: org never travels
136
- expect(req?.channel).toBe("");
137
- expect(req?.payload?.kind.case).toBe("text");
138
- expect(req?.payload?.kind.case === "text" && req.payload.kind.value.body).toBe("see you at 6");
139
-
140
- // toProtoJson emits proto (snake_case) field names — the Go parity rule.
141
- const body = JSON.parse(result.content[0]?.text ?? "{}") as Record<string, string>;
142
- expect(body.outcome).toBe("accepted");
143
- expect(body.provider_message_id).toBe("wamid.abc");
144
- });
145
-
146
- it("maps a template send with language, parameters, header link, and a named channel", async () => {
147
- sendResponse = () =>
148
- create(SendChannelMessageOutputSchema, { outcome: ChannelSendOutcome.accepted });
149
- captured.send = undefined;
150
-
151
- const result = await send({
152
- recipient: "919000000001",
153
- channel: "isc-whatsapp",
154
- template: {
155
- name: "invoice_qr",
156
- language: "en",
157
- parameters: { member_name: "Asha", amount: "1500" },
158
- header_image_link: "https://isc.example/qr.png",
159
- },
160
- });
161
-
162
- expect(result.isError).toBeFalsy();
163
- const req = captured.send as SendChannelMessageInput | undefined;
164
- expect(req?.channel).toBe("isc-whatsapp");
165
- expect(req?.payload?.kind.case).toBe("template");
166
- if (req?.payload?.kind.case === "template") {
167
- expect(req.payload.kind.value.name).toBe("invoice_qr");
168
- expect(req.payload.kind.value.language).toBe("en");
169
- expect(req.payload.kind.value.parameters).toMatchObject({
170
- member_name: "Asha",
171
- amount: "1500",
172
- });
173
- expect(req.payload.kind.value.headerImageLink).toBe("https://isc.example/qr.png");
174
- }
175
- });
176
-
177
- it("refuses both/neither arms with corrective copy, before any RPC", async () => {
178
- captured.send = undefined;
179
-
180
- const both = await send({
181
- recipient: "919000000001",
182
- text: "hi",
183
- template: { name: "fee_reminder" },
184
- });
185
- expect(both.isError).toBe(true);
186
- expect(both.content[0]?.text).toContain("exactly one of text | template");
187
-
188
- const neither = await send({ recipient: "919000000001" });
189
- expect(neither.isError).toBe(true);
190
- expect(neither.content[0]?.text).toContain("exactly one of text | template");
191
-
192
- expect(captured.send).toBeUndefined();
193
- });
194
- });
195
-
196
- describe("typed outcomes are answers, not errors (DD-002 D4)", () => {
197
- it("a refused outcome returns as a SUCCESS result carrying the detail", async () => {
198
- sendResponse = () =>
199
- create(SendChannelMessageOutputSchema, {
200
- outcome: ChannelSendOutcome.refused,
201
- outboundMessageId: "",
202
- detail:
203
- "recipient has not messaged this channel — proactive sends from a channel"
204
- + " conversation are limited to known senders",
205
- });
206
-
207
- const result = await send({ recipient: "919000000002", text: "hello" });
208
-
209
- expect(result.isError).toBeFalsy();
210
- const body = JSON.parse(result.content[0]?.text ?? "{}") as Record<string, string>;
211
- expect(body.outcome).toBe("refused");
212
- expect(body.detail).toContain("known senders");
213
- });
214
- });
215
-
216
- describe("channels error mapper", () => {
217
- it("passes a reach denial through verbatim — never the shared rewrite", async () => {
218
- sendResponse = () => {
219
- throw new ConnectError(
220
- "this agent has no proactive-messaging channel it can use",
221
- Code.PermissionDenied,
222
- );
223
- };
224
-
225
- const result = await send({ recipient: "919000000001", text: "hello" });
226
-
227
- expect(result.isError).toBe(true);
228
- expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual({
229
- error: "this agent has no proactive-messaging channel it can use",
230
- code: "PERMISSION_DENIED",
231
- });
232
- });
233
-
234
- it("carries the ErrorInfo reason on operator-actionable preconditions (DD-005 D8)", async () => {
235
- sendResponse = () => {
236
- throw new ConnectError(
237
- "proactive channel messaging requires Stigmer Cloud",
238
- Code.FailedPrecondition,
239
- undefined,
240
- [
241
- {
242
- desc: ErrorInfoSchema,
243
- value: create(ErrorInfoSchema, {
244
- reason: "WHATSAPP_MANAGEMENT_SCOPE_MISSING",
245
- domain: "stigmer.ai",
246
- }),
247
- },
248
- ],
249
- );
250
- };
251
-
252
- const result = await send({ recipient: "919000000001", text: "hello" });
253
-
254
- expect(result.isError).toBe(true);
255
- const body = JSON.parse(result.content[0]?.text ?? "{}") as Record<string, string>;
256
- expect(body.code).toBe("FAILED_PRECONDITION");
257
- expect(body.reason).toBe("WHATSAPP_MANAGEMENT_SCOPE_MISSING");
258
- });
259
-
260
- it("delegates transport codes to the shared classifier", async () => {
261
- sendResponse = () => {
262
- throw new ConnectError("upstream down", Code.Unavailable);
263
- };
264
-
265
- const result = await send({ recipient: "919000000001", text: "hello" });
266
-
267
- expect(result.isError).toBe(true);
268
- expect(result.content[0]?.text).toBe(
269
- "Stigmer server is unavailable. Ensure it is running and reachable.",
270
- );
271
- });
272
- });
@@ -1,52 +0,0 @@
1
- // Unit tests for the channels-domain error projection: the verbatim
2
- // message and status name always; the google.rpc.ErrorInfo reason only
3
- // when the server attached one with a non-empty reason (DD-005 D8 —
4
- // absence means the message alone carries the contract).
5
-
6
- import { Code, ConnectError } from "@connectrpc/connect";
7
- import { create } from "@bufbuild/protobuf";
8
- import { describe, expect, it } from "vitest";
9
- import { ErrorInfoSchema } from "@stigmer/protos/google/rpc/error_details_pb";
10
-
11
- import { domainError } from "./errors";
12
-
13
- function preconditionWith(reason: string): ConnectError {
14
- return new ConnectError(
15
- "registry misconfigured",
16
- Code.FailedPrecondition,
17
- undefined,
18
- [
19
- {
20
- desc: ErrorInfoSchema,
21
- value: create(ErrorInfoSchema, { reason, domain: "stigmer.ai" }),
22
- },
23
- ],
24
- );
25
- }
26
-
27
- describe("domainError", () => {
28
- it("carries the ErrorInfo reason when the server attached one", () => {
29
- expect(
30
- domainError(preconditionWith("WHATSAPP_MANAGEMENT_SCOPE_MISSING")),
31
- ).toEqual({
32
- error: "registry misconfigured",
33
- code: "FAILED_PRECONDITION",
34
- reason: "WHATSAPP_MANAGEMENT_SCOPE_MISSING",
35
- });
36
- });
37
-
38
- it("omits the reason key when the attached ErrorInfo has an empty reason", () => {
39
- expect(domainError(preconditionWith(""))).toEqual({
40
- error: "registry misconfigured",
41
- code: "FAILED_PRECONDITION",
42
- });
43
- });
44
-
45
- it("omits the reason key when no ErrorInfo detail is attached", () => {
46
- const ce = new ConnectError("channel is required", Code.InvalidArgument);
47
- expect(domainError(ce)).toEqual({
48
- error: "channel is required",
49
- code: "INVALID_ARGUMENT",
50
- });
51
- });
52
- });