@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.
- package/cli/mcp-server-stigmer.d.ts.map +1 -1
- package/cli/mcp-server-stigmer.js +1 -1
- package/cli/mcp-server-stigmer.js.map +1 -1
- package/config.d.ts.map +1 -1
- package/config.js.map +1 -1
- package/domains/agentexecutions/approve.d.ts.map +1 -1
- package/domains/agentexecutions/approve.js.map +1 -1
- package/domains/agentexecutions/fetch.d.ts.map +1 -1
- package/domains/agentexecutions/fetch.js.map +1 -1
- package/domains/agentexecutions/run.d.ts.map +1 -1
- package/domains/agentexecutions/run.js.map +1 -1
- package/domains/agentexecutions/tools.d.ts.map +1 -1
- package/domains/agentexecutions/tools.js.map +1 -1
- package/domains/agents/apply.d.ts.map +1 -1
- package/domains/agents/apply.js.map +1 -1
- package/domains/agents/delete.d.ts.map +1 -1
- package/domains/agents/delete.js.map +1 -1
- package/domains/agents/fetch.d.ts.map +1 -1
- package/domains/agents/fetch.js.map +1 -1
- package/domains/agents/resources.d.ts.map +1 -1
- package/domains/agents/resources.js.map +1 -1
- package/domains/agents/tools.d.ts.map +1 -1
- package/domains/agents/tools.js.map +1 -1
- package/domains/apply-visibility.d.ts.map +1 -1
- package/domains/apply-visibility.js.map +1 -1
- package/domains/channels/calls.d.ts.map +1 -1
- package/domains/channels/calls.js.map +1 -1
- package/domains/channels/errors.d.ts.map +1 -1
- package/domains/channels/errors.js.map +1 -1
- package/domains/channels/tools.d.ts.map +1 -1
- package/domains/channels/tools.js.map +1 -1
- package/domains/client.d.ts.map +1 -1
- package/domains/client.js.map +1 -1
- package/domains/conversation/calls.d.ts.map +1 -1
- package/domains/conversation/calls.js.map +1 -1
- package/domains/conversation/errors.d.ts.map +1 -1
- package/domains/conversation/errors.js.map +1 -1
- package/domains/conversation/tools.d.ts.map +1 -1
- package/domains/conversation/tools.js.map +1 -1
- package/domains/environments/apply.d.ts.map +1 -1
- package/domains/environments/apply.js.map +1 -1
- package/domains/environments/delete.d.ts.map +1 -1
- package/domains/environments/delete.js.map +1 -1
- package/domains/environments/fetch.d.ts.map +1 -1
- package/domains/environments/fetch.js.map +1 -1
- package/domains/environments/resources.d.ts.map +1 -1
- package/domains/environments/resources.js.map +1 -1
- package/domains/environments/tools.d.ts.map +1 -1
- package/domains/environments/tools.js.map +1 -1
- package/domains/executions/cancel.d.ts.map +1 -1
- package/domains/executions/cancel.js.map +1 -1
- package/domains/executions/tools.d.ts.map +1 -1
- package/domains/executions/tools.js.map +1 -1
- package/domains/marshal.d.ts.map +1 -1
- package/domains/marshal.js.map +1 -1
- package/domains/mcpservers/apply.d.ts.map +1 -1
- package/domains/mcpservers/apply.js.map +1 -1
- package/domains/mcpservers/delete.d.ts.map +1 -1
- package/domains/mcpservers/delete.js.map +1 -1
- package/domains/mcpservers/fetch.d.ts.map +1 -1
- package/domains/mcpservers/fetch.js.map +1 -1
- package/domains/mcpservers/resources.d.ts.map +1 -1
- package/domains/mcpservers/resources.js.map +1 -1
- package/domains/mcpservers/tools.d.ts.map +1 -1
- package/domains/mcpservers/tools.js.map +1 -1
- package/domains/memory/calls.d.ts.map +1 -1
- package/domains/memory/calls.js.map +1 -1
- package/domains/memory/context.d.ts.map +1 -1
- package/domains/memory/context.js.map +1 -1
- package/domains/memory/errors.d.ts.map +1 -1
- package/domains/memory/errors.js.map +1 -1
- package/domains/memory/tools.d.ts.map +1 -1
- package/domains/memory/tools.js.map +1 -1
- package/domains/resourcehandler.d.ts.map +1 -1
- package/domains/resourcehandler.js.map +1 -1
- package/domains/resourceuri.d.ts.map +1 -1
- package/domains/resourceuri.js.map +1 -1
- package/domains/rpcerr.d.ts.map +1 -1
- package/domains/rpcerr.js.map +1 -1
- package/domains/search/tools.d.ts.map +1 -1
- package/domains/search/tools.js.map +1 -1
- package/domains/skills/delete.d.ts.map +1 -1
- package/domains/skills/delete.js.map +1 -1
- package/domains/skills/fetch.d.ts.map +1 -1
- package/domains/skills/fetch.js.map +1 -1
- package/domains/skills/resources.d.ts.map +1 -1
- package/domains/skills/resources.js.map +1 -1
- package/domains/skills/tools.d.ts.map +1 -1
- package/domains/skills/tools.js.map +1 -1
- package/domains/skills/versions.d.ts.map +1 -1
- package/domains/skills/versions.js.map +1 -1
- package/domains/toolresult.d.ts.map +1 -1
- package/domains/toolresult.js.map +1 -1
- package/domains/workflowexecutions/approvals.d.ts.map +1 -1
- package/domains/workflowexecutions/approvals.js.map +1 -1
- package/domains/workflowexecutions/run.d.ts.map +1 -1
- package/domains/workflowexecutions/run.js.map +1 -1
- package/domains/workflowexecutions/tools.d.ts.map +1 -1
- package/domains/workflowexecutions/tools.js.map +1 -1
- package/domains/workflows/apply.d.ts.map +1 -1
- package/domains/workflows/apply.js.map +1 -1
- package/domains/workflows/delete.d.ts.map +1 -1
- package/domains/workflows/delete.js.map +1 -1
- package/domains/workflows/fetch.d.ts.map +1 -1
- package/domains/workflows/fetch.js.map +1 -1
- package/domains/workflows/resources.d.ts.map +1 -1
- package/domains/workflows/resources.js.map +1 -1
- package/domains/workflows/taskkinds.d.ts.map +1 -1
- package/domains/workflows/taskkinds.js.map +1 -1
- package/domains/workflows/tools.d.ts.map +1 -1
- package/domains/workflows/tools.js.map +1 -1
- package/domains/workflows/validate.d.ts.map +1 -1
- package/domains/workflows/validate.js.map +1 -1
- package/domains/workflows/versions.d.ts.map +1 -1
- package/domains/workflows/versions.js.map +1 -1
- package/gen/agent.d.ts.map +1 -1
- package/gen/agent.js.map +1 -1
- package/gen/apply-runtime.d.ts.map +1 -1
- package/gen/apply-runtime.js.map +1 -1
- package/gen/environment.d.ts.map +1 -1
- package/gen/environment.js.map +1 -1
- package/gen/mcpserver.d.ts.map +1 -1
- package/gen/mcpserver.js.map +1 -1
- package/gen/workflow.d.ts.map +1 -1
- package/gen/workflow.js.map +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/logger.d.ts.map +1 -1
- package/logger.js.map +1 -1
- package/package.json +3 -3
- package/readiness.d.ts.map +1 -1
- package/readiness.js.map +1 -1
- package/server.d.ts.map +1 -1
- package/server.js.map +1 -1
- package/src/config.test.ts +0 -88
- package/src/domains/apply.integration.test.ts +0 -313
- package/src/domains/channels/channels.integration.test.ts +0 -272
- package/src/domains/channels/errors.test.ts +0 -52
- package/src/domains/conversation/conversation.integration.test.ts +0 -233
- package/src/domains/deletes.integration.test.ts +0 -147
- package/src/domains/executions.integration.test.ts +0 -376
- package/src/domains/memory/context.test.ts +0 -95
- package/src/domains/memory/errors.test.ts +0 -54
- package/src/domains/memory/memory.integration.test.ts +0 -253
- package/src/domains/reads.integration.test.ts +0 -163
- package/src/domains/resources.integration.test.ts +0 -157
- package/src/domains/resourceuri.test.ts +0 -104
- package/src/domains/rpcerr.test.ts +0 -62
- package/src/domains/search/search.integration.test.ts +0 -141
- package/src/domains/versions.integration.test.ts +0 -212
- package/src/domains/workflows/validate.integration.test.ts +0 -117
- package/src/domains/workflows/workflow-tools.integration.test.ts +0 -148
- package/src/http.integration.test.ts +0 -235
- package/src/readiness.test.ts +0 -90
- 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
|
-
});
|