@stigmer/cli 3.3.0 → 3.4.1

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 (55) hide show
  1. package/commands/delete.js +5 -2
  2. package/commands/delete.js.map +1 -1
  3. package/commands/setup.d.ts.map +1 -1
  4. package/commands/setup.js +11 -16
  5. package/commands/setup.js.map +1 -1
  6. package/local/daemon/components.d.ts.map +1 -1
  7. package/local/daemon/components.js +3 -2
  8. package/local/daemon/components.js.map +1 -1
  9. package/local/daemon/env.d.ts +0 -3
  10. package/local/daemon/env.d.ts.map +1 -1
  11. package/local/daemon/env.js +4 -8
  12. package/local/daemon/env.js.map +1 -1
  13. package/local/daemon/launch.d.ts.map +1 -1
  14. package/local/daemon/launch.js +9 -17
  15. package/local/daemon/launch.js.map +1 -1
  16. package/local/llm-config.d.ts +7 -7
  17. package/local/llm-config.d.ts.map +1 -1
  18. package/local/llm-config.js +29 -42
  19. package/local/llm-config.js.map +1 -1
  20. package/local/setup/wizard.d.ts +16 -7
  21. package/local/setup/wizard.d.ts.map +1 -1
  22. package/local/setup/wizard.js +41 -44
  23. package/local/setup/wizard.js.map +1 -1
  24. package/local/status.d.ts.map +1 -1
  25. package/local/status.js +16 -15
  26. package/local/status.js.map +1 -1
  27. package/package.json +5 -5
  28. package/resources/delete.d.ts +5 -1
  29. package/resources/delete.d.ts.map +1 -1
  30. package/resources/delete.js +40 -8
  31. package/resources/delete.js.map +1 -1
  32. package/resources/get-bindings.d.ts.map +1 -1
  33. package/resources/get-bindings.js +8 -0
  34. package/resources/get-bindings.js.map +1 -1
  35. package/resources/list.d.ts.map +1 -1
  36. package/resources/list.js +70 -4
  37. package/resources/list.js.map +1 -1
  38. package/src/commands/delete.ts +5 -2
  39. package/src/commands/setup.ts +13 -20
  40. package/src/local/daemon/components.test.ts +8 -2
  41. package/src/local/daemon/components.ts +3 -1
  42. package/src/local/daemon/env.test.ts +13 -6
  43. package/src/local/daemon/env.ts +10 -13
  44. package/src/local/daemon/launch.ts +9 -17
  45. package/src/local/llm-config.test.ts +16 -27
  46. package/src/local/llm-config.ts +27 -41
  47. package/src/local/setup/wizard.test.ts +19 -11
  48. package/src/local/setup/wizard.ts +42 -47
  49. package/src/local/status.test.ts +20 -0
  50. package/src/local/status.ts +17 -15
  51. package/src/resources/delete.ts +48 -9
  52. package/src/resources/get-bindings.ts +8 -0
  53. package/src/resources/list.ts +76 -4
  54. package/src/resources/mutate-verbs.integration.test.ts +178 -1
  55. package/src/resources/read-verbs.integration.test.ts +236 -2
@@ -16,9 +16,21 @@ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentex
16
16
  import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
17
17
  import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
18
18
  import { AgentExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/query_pb";
19
+ import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
20
+ import { AgentChannelCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/command_pb";
21
+ import { AgentChannelQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/query_pb";
19
22
  import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
20
23
  import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
21
24
  import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/query_pb";
25
+ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
26
+ import { ChannelAppCommandController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/command_pb";
27
+ import { ChannelAppQueryController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/query_pb";
28
+ import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
29
+ import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
30
+ import { DatastoreQueryController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/query_pb";
31
+ import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
32
+ import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
33
+ import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
22
34
  import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
23
35
  import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
24
36
  import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
@@ -51,6 +63,28 @@ const knownWorkflow = create(WorkflowSchema, {
51
63
  metadata: { name: "Deploy", slug: "deploy", org: "acme", id: "wfl_1" },
52
64
  });
53
65
 
66
+ // The mock treats this datastore as NON-EMPTY: delete refuses without the
67
+ // force acknowledgment, mirroring the server's GuardNonEmptyStep contract.
68
+ const knownDatastore = create(DatastoreSchema, {
69
+ metadata: { name: "clinic-records", slug: "clinic-records", org: "acme", id: "dst_1" },
70
+ });
71
+
72
+ // Byte-shaped like the Go server's GuardNonEmptyStep refusal (delete_guards.go).
73
+ const nonEmptyGuardMessage =
74
+ 'datastore "clinic-records" holds 12 records across 3 collections; deleting destroys them — pass force to acknowledge';
75
+
76
+ const knownEnvironment = create(EnvironmentSchema, {
77
+ metadata: { name: "clinic-patient-db", slug: "clinic-patient-db", org: "acme", id: "env_1" },
78
+ });
79
+
80
+ const knownChannel = create(AgentChannelSchema, {
81
+ metadata: { name: "clinic-patient-whatsapp", slug: "clinic-patient-whatsapp", org: "acme", id: "ach_1" },
82
+ });
83
+
84
+ const knownChannelApp = create(ChannelAppSchema, {
85
+ metadata: { name: "clinic-meta-app", slug: "clinic-meta-app", org: "acme", id: "chapp_1" },
86
+ });
87
+
54
88
  // Pending execution (cancellable) vs. an already-terminal one.
55
89
  const pendingExecution = create(AgentExecutionSchema, {
56
90
  metadata: { id: "aex_run" },
@@ -72,10 +106,20 @@ const openSessions = new Set<ServerHttp2Session>();
72
106
  // Spies for the command-side calls, reset per test.
73
107
  let cancelCalls: string[] = [];
74
108
  let tagCalls: { workflowId: string; versionHash: string; tag: string }[] = [];
109
+ // Force-carrying deletes record what actually rode the RPC.
110
+ let datastoreDeletes: { resourceId: string; force: boolean }[] = [];
111
+ let environmentDeletes: { resourceId: string; force: boolean }[] = [];
112
+ let channelAppDeletes: { resourceId: string; force: boolean }[] = [];
113
+ // agent_channel's delete is a typed ID — no force field exists on the wire.
114
+ let channelDeleteIds: string[] = [];
75
115
 
76
116
  beforeEach(() => {
77
117
  cancelCalls = [];
78
118
  tagCalls = [];
119
+ datastoreDeletes = [];
120
+ environmentDeletes = [];
121
+ channelAppDeletes = [];
122
+ channelDeleteIds = [];
79
123
  });
80
124
 
81
125
  beforeAll(async () => {
@@ -118,6 +162,60 @@ beforeAll(async () => {
118
162
  },
119
163
  });
120
164
 
165
+ router.service(DatastoreQueryController, {
166
+ getByReference: (req) => {
167
+ if (req.slug !== "clinic-records") throw new ConnectError("datastore not found", Code.NotFound);
168
+ return knownDatastore;
169
+ },
170
+ });
171
+ router.service(DatastoreCommandController, {
172
+ delete: (req) => {
173
+ datastoreDeletes.push({ resourceId: req.resourceId, force: req.force });
174
+ // Mirrors GuardNonEmptyStep: a non-empty datastore refuses without force.
175
+ if (!req.force) throw new ConnectError(nonEmptyGuardMessage, Code.FailedPrecondition);
176
+ return knownDatastore;
177
+ },
178
+ });
179
+
180
+ router.service(EnvironmentQueryController, {
181
+ getByReference: (req) => {
182
+ if (req.slug !== "clinic-patient-db") throw new ConnectError("environment not found", Code.NotFound);
183
+ return knownEnvironment;
184
+ },
185
+ });
186
+ router.service(EnvironmentCommandController, {
187
+ delete: (req) => {
188
+ environmentDeletes.push({ resourceId: req.resourceId, force: req.force });
189
+ return knownEnvironment;
190
+ },
191
+ });
192
+
193
+ router.service(AgentChannelQueryController, {
194
+ getByReference: (req) => {
195
+ if (req.slug !== "clinic-patient-whatsapp") throw new ConnectError("agent channel not found", Code.NotFound);
196
+ return knownChannel;
197
+ },
198
+ });
199
+ router.service(AgentChannelCommandController, {
200
+ delete: (req) => {
201
+ channelDeleteIds.push(req.value);
202
+ return knownChannel;
203
+ },
204
+ });
205
+
206
+ router.service(ChannelAppQueryController, {
207
+ getByReference: (req) => {
208
+ if (req.slug !== "clinic-meta-app") throw new ConnectError("channel app not found", Code.NotFound);
209
+ return knownChannelApp;
210
+ },
211
+ });
212
+ router.service(ChannelAppCommandController, {
213
+ delete: (req) => {
214
+ channelAppDeletes.push({ resourceId: req.resourceId, force: req.force });
215
+ return knownChannelApp;
216
+ },
217
+ });
218
+
121
219
  router.service(WorkflowQueryController, {
122
220
  getByReference: (req) => {
123
221
  if (req.slug !== "deploy") throw new ConnectError("workflow not found", Code.NotFound);
@@ -208,9 +306,17 @@ describe("delete (standard kinds)", () => {
208
306
  expect(classify(err)?.exitCode).toBe(ExitCode.NotFound);
209
307
  });
210
308
 
211
- it("rejects an unknown type with a usage error", async () => {
309
+ it("rejects an unknown type and lists exactly the wired types", async () => {
212
310
  const err = await planDelete(client, "bogus", "x", "acme").catch((e) => e);
213
311
  expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
312
+ // The list is derived from DELETE_HANDLERS (+ the two special cases), so
313
+ // it must name the wired kinds and never the declared-but-unwired ones.
314
+ const message = String(err.message);
315
+ for (const wired of ["datastore", "environment", "agentchannel", "channelapp", "execution", "organization"]) {
316
+ expect(message).toContain(wired);
317
+ }
318
+ expect(message).not.toContain("session");
319
+ expect(message).not.toContain("oauthapp");
214
320
  });
215
321
 
216
322
  it("rejects a kind that has no delete handler with a usage error", async () => {
@@ -219,6 +325,77 @@ describe("delete (standard kinds)", () => {
219
325
  });
220
326
  });
221
327
 
328
+ describe("delete (force acknowledgment)", () => {
329
+ it("threads --force to the datastore delete RPC as the destroy-records ack", async () => {
330
+ const plan = await planDelete(client, "datastore", "clinic-records", "acme", true);
331
+ const result = await plan.perform();
332
+
333
+ expect(result.status).toBe("success");
334
+ expect(result.message).toBe("Datastore deleted successfully");
335
+ expect(datastoreDeletes).toEqual([{ resourceId: "dst_1", force: true }]);
336
+ });
337
+
338
+ it("surfaces the non-empty guard refusal verbatim when force is absent", async () => {
339
+ const plan = await planDelete(client, "datastore", "clinic-records", "acme");
340
+ const err = await plan.perform().catch((e) => e);
341
+
342
+ // A confirmed-but-unforced delete must NOT bypass the server guard: the
343
+ // RPC carries force=false and the refusal reaches the user verbatim.
344
+ expect(datastoreDeletes).toEqual([{ resourceId: "dst_1", force: false }]);
345
+ const classified = classify(err);
346
+ expect(classified?.exitCode).toBe(ExitCode.Usage);
347
+ expect(classified?.message).toBe(`Precondition failed: ${nonEmptyGuardMessage}`);
348
+ });
349
+
350
+ it("warns up front that datastore records are destroyed and force-guarded", async () => {
351
+ const plan = await planDelete(client, "datastore", "clinic-records", "acme");
352
+ expect(plan.warning.hints).toContain("All records held by this datastore will be destroyed.");
353
+ expect(plan.warning.hints).toContain(
354
+ "A datastore holding records is refused unless --force acknowledges destroying them.",
355
+ );
356
+ });
357
+ });
358
+
359
+ describe("delete (cutover kinds: environment, agent channel, channel app)", () => {
360
+ it("deletes an environment via the DeleteResourceInput shape", async () => {
361
+ const plan = await planDelete(client, "environment", "clinic-patient-db", "acme");
362
+ const result = await plan.perform();
363
+
364
+ expect(result.status).toBe("success");
365
+ expect(result.message).toBe("Environment deleted successfully");
366
+ expect(environmentDeletes).toEqual([{ resourceId: "env_1", force: false }]);
367
+ });
368
+
369
+ it("threads --force through the environment delete (generic ack carrier)", async () => {
370
+ const plan = await planDelete(client, "environment", "clinic-patient-db", "acme", true);
371
+ await plan.perform();
372
+ expect(environmentDeletes).toEqual([{ resourceId: "env_1", force: true }]);
373
+ });
374
+
375
+ it("deletes an agent channel via its typed-ID contract, ignoring force gracefully", async () => {
376
+ const plan = await planDelete(client, "agent-channel", "clinic-patient-whatsapp", "acme", true);
377
+
378
+ // The teardown-vs-pause distinction rides the warning (server doc contract).
379
+ expect(plan.warning.hints).toContain(
380
+ "Delete is the connection's full teardown; to pause instead, apply with spec.enabled: false.",
381
+ );
382
+
383
+ const result = await plan.perform();
384
+ expect(result.status).toBe("success");
385
+ expect(result.message).toBe("Agent Channel deleted successfully");
386
+ expect(channelDeleteIds).toEqual(["ach_1"]);
387
+ });
388
+
389
+ it("deletes a channel app via the DeleteResourceInput shape", async () => {
390
+ const plan = await planDelete(client, "channel-app", "clinic-meta-app", "acme");
391
+ const result = await plan.perform();
392
+
393
+ expect(result.status).toBe("success");
394
+ expect(result.message).toBe("Channel App deleted successfully");
395
+ expect(channelAppDeletes).toEqual([{ resourceId: "chapp_1", force: false }]);
396
+ });
397
+ });
398
+
222
399
  describe("delete execution (cancel special case)", () => {
223
400
  it("cancels a non-terminal execution", async () => {
224
401
  const plan = await planDelete(client, "execution", "aex_run", "");
@@ -7,12 +7,20 @@
7
7
  // to the right CLI exit code via classify().
8
8
 
9
9
  import { create, toJson } from "@bufbuild/protobuf";
10
+ import { timestampFromDate } from "@bufbuild/protobuf/wkt";
10
11
  import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
11
12
  import { connectNodeAdapter } from "@connectrpc/connect-node";
12
13
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
13
14
  import { AgentQueryController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/query_pb";
15
+ import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
16
+ import { AgentChannelQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/query_pb";
17
+ import { AgentChannelInstallState } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/status_pb";
14
18
  import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
15
19
  import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/query_pb";
20
+ import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
21
+ import { ChannelAppQueryController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/query_pb";
22
+ import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
23
+ import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
16
24
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
17
25
  import { ApiKeySchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
18
26
  import { ApiKeyQueryController } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/query_pb";
@@ -67,6 +75,73 @@ const knownSearchResult = create(SearchResultSchema, {
67
75
  description: "reviews code",
68
76
  });
69
77
 
78
+ // The three T07 cutover kinds — wired into get-bindings and list alongside
79
+ // this test. environment lists via the SearchService (it is search-indexed);
80
+ // the two channel kinds list via their dedicated query RPCs.
81
+ const knownEnvironment = create(EnvironmentSchema, {
82
+ apiVersion: "agentic.stigmer.ai/v1",
83
+ kind: "Environment",
84
+ metadata: { name: "clinic-patient-db", slug: "clinic-patient-db", org: "acme", id: "env_1" },
85
+ });
86
+
87
+ const knownEnvironmentSearchResult = create(SearchResultSchema, {
88
+ kind: ApiResourceKind.environment,
89
+ id: "env_1",
90
+ name: "clinic-patient-db",
91
+ slug: "clinic-patient-db",
92
+ qualifiedSlug: "acme/clinic-patient-db",
93
+ org: "acme",
94
+ description: "clinic patient database credentials",
95
+ });
96
+
97
+ // Spec/status populated to exercise every AGENT_CHANNEL_TABLE column:
98
+ // agent_ref, the provider oneof, install_state, and enabled.
99
+ const knownChannel = create(AgentChannelSchema, {
100
+ apiVersion: "agentic.stigmer.ai/v1",
101
+ kind: "AgentChannel",
102
+ metadata: { name: "clinic-patient-whatsapp", slug: "clinic-patient-whatsapp", org: "acme", id: "ach_1" },
103
+ spec: {
104
+ enabled: true,
105
+ agentRef: { kind: ApiResourceKind.agent, org: "acme", slug: "clinic-assistant" },
106
+ providerConfig: { case: "whatsapp", value: { phoneNumberId: "106540352242922" } },
107
+ },
108
+ status: { installState: AgentChannelInstallState.installed },
109
+ });
110
+
111
+ // Secret fields carry the redaction marker, matching what the server's
112
+ // RedactChannelApp pipeline returns on every list/get response.
113
+ const knownChannelApp = create(ChannelAppSchema, {
114
+ apiVersion: "agentic.stigmer.ai/v1",
115
+ kind: "ChannelApp",
116
+ metadata: { name: "clinic-meta-app", slug: "clinic-meta-app", org: "acme", id: "chapp_1" },
117
+ spec: {
118
+ providerConfig: {
119
+ case: "whatsapp",
120
+ value: {
121
+ appId: "108954",
122
+ appSecret: "***REDACTED***",
123
+ accessToken: "***REDACTED***",
124
+ verifyToken: "***REDACTED***",
125
+ },
126
+ },
127
+ },
128
+ status: { audit: { specAudit: { createdAt: timestampFromDate(new Date("2026-07-20T10:00:00Z")) } } },
129
+ });
130
+
131
+ // Second entry (slack arm) so the list tests cover both provider derivations
132
+ // and the client-side --limit slice (listByOrg has no pagination).
133
+ const secondChannelApp = create(ChannelAppSchema, {
134
+ apiVersion: "agentic.stigmer.ai/v1",
135
+ kind: "ChannelApp",
136
+ metadata: { name: "clinic-slack-app", slug: "clinic-slack-app", org: "acme", id: "chapp_2" },
137
+ spec: {
138
+ providerConfig: {
139
+ case: "slack",
140
+ value: { clientId: "12.34", clientSecret: "***REDACTED***", signingSecret: "***REDACTED***" },
141
+ },
142
+ },
143
+ });
144
+
70
145
  let backend: Http2Server;
71
146
  let client: Stigmer;
72
147
  const openSessions = new Set<ServerHttp2Session>();
@@ -98,7 +173,14 @@ beforeAll(async () => {
98
173
  },
99
174
  });
100
175
  router.service(SearchService, {
101
- search: () => ({ entries: [knownSearchResult], totalCount: 1, totalPages: 1 }),
176
+ // Kind-aware: search-backed list sends a single-kind query, so the
177
+ // environment arm proves the kind actually rode the request.
178
+ search: (req) => {
179
+ if (req.kinds.length === 1 && req.kinds[0] === ApiResourceKind.environment) {
180
+ return { entries: [knownEnvironmentSearchResult], totalCount: 1, totalPages: 1 };
181
+ }
182
+ return { entries: [knownSearchResult], totalCount: 1, totalPages: 1 };
183
+ },
102
184
  });
103
185
  router.service(OrganizationQueryController, {
104
186
  findMyOrganizations: () => ({ entries: [knownOrg] }),
@@ -106,6 +188,44 @@ beforeAll(async () => {
106
188
  router.service(ApiKeyQueryController, {
107
189
  findAll: () => ({ entries: [knownApiKey] }),
108
190
  });
191
+ router.service(EnvironmentQueryController, {
192
+ get: (req) => {
193
+ if (req.value !== "env_1") throw new ConnectError("environment not found", Code.NotFound);
194
+ return knownEnvironment;
195
+ },
196
+ getByReference: (req) => {
197
+ if (req.slug !== "clinic-patient-db") throw new ConnectError("environment not found", Code.NotFound);
198
+ return knownEnvironment;
199
+ },
200
+ });
201
+ router.service(AgentChannelQueryController, {
202
+ get: (req) => {
203
+ if (req.value !== "ach_1") throw new ConnectError("agent channel not found", Code.NotFound);
204
+ return knownChannel;
205
+ },
206
+ getByReference: (req) => {
207
+ if (req.slug !== "clinic-patient-whatsapp") throw new ConnectError("agent channel not found", Code.NotFound);
208
+ return knownChannel;
209
+ },
210
+ list: (req) => {
211
+ if (req.org !== "acme") throw new ConnectError("org is required", Code.InvalidArgument);
212
+ return { totalCount: 1, items: [knownChannel] };
213
+ },
214
+ });
215
+ router.service(ChannelAppQueryController, {
216
+ get: (req) => {
217
+ if (req.value !== "chapp_1") throw new ConnectError("channel app not found", Code.NotFound);
218
+ return knownChannelApp;
219
+ },
220
+ getByReference: (req) => {
221
+ if (req.slug !== "clinic-meta-app") throw new ConnectError("channel app not found", Code.NotFound);
222
+ return knownChannelApp;
223
+ },
224
+ listByOrg: (req) => {
225
+ if (req.org !== "acme") throw new ConnectError("org is required", Code.InvalidArgument);
226
+ return { entries: [knownChannelApp, secondChannelApp] };
227
+ },
228
+ });
109
229
  };
110
230
  backend = createHttp2Server(connectNodeAdapter({ routes }));
111
231
  backend.on("session", (session) => {
@@ -169,6 +289,58 @@ describe("get integration", () => {
169
289
  });
170
290
  });
171
291
 
292
+ it("fetches an environment by org/slug and renders backend protojson", async () => {
293
+ const { schema, message } = await fetchResource(client, ApiResourceKind.environment, {
294
+ kind: "ref",
295
+ org: "acme",
296
+ slug: "clinic-patient-db",
297
+ });
298
+ const rendered = JSON.parse(renderResource(schema, message, "json"));
299
+ expect(rendered).toEqual(toJson(EnvironmentSchema, knownEnvironment, { useProtoFieldName: true }));
300
+ });
301
+
302
+ it("fetches an environment by ID", async () => {
303
+ const { message } = await fetchResource(client, ApiResourceKind.environment, { kind: "id", id: "env_1" });
304
+ expect(JSON.parse(renderResource(EnvironmentSchema, message, "json"))).toMatchObject({
305
+ metadata: { id: "env_1" },
306
+ });
307
+ });
308
+
309
+ it("fetches an agent channel by org/slug and renders backend protojson", async () => {
310
+ const { schema, message } = await fetchResource(client, ApiResourceKind.agent_channel, {
311
+ kind: "ref",
312
+ org: "acme",
313
+ slug: "clinic-patient-whatsapp",
314
+ });
315
+ const rendered = JSON.parse(renderResource(schema, message, "json"));
316
+ expect(rendered).toEqual(toJson(AgentChannelSchema, knownChannel, { useProtoFieldName: true }));
317
+ });
318
+
319
+ it("fetches an agent channel by ID", async () => {
320
+ const { message } = await fetchResource(client, ApiResourceKind.agent_channel, { kind: "id", id: "ach_1" });
321
+ expect(JSON.parse(renderResource(AgentChannelSchema, message, "json"))).toMatchObject({
322
+ metadata: { id: "ach_1" },
323
+ spec: { enabled: true },
324
+ });
325
+ });
326
+
327
+ it("fetches a channel app by org/slug and renders backend protojson", async () => {
328
+ const { schema, message } = await fetchResource(client, ApiResourceKind.channel_app, {
329
+ kind: "ref",
330
+ org: "acme",
331
+ slug: "clinic-meta-app",
332
+ });
333
+ const rendered = JSON.parse(renderResource(schema, message, "json"));
334
+ expect(rendered).toEqual(toJson(ChannelAppSchema, knownChannelApp, { useProtoFieldName: true }));
335
+ });
336
+
337
+ it("fetches a channel app by ID", async () => {
338
+ const { message } = await fetchResource(client, ApiResourceKind.channel_app, { kind: "id", id: "chapp_1" });
339
+ expect(JSON.parse(renderResource(ChannelAppSchema, message, "json"))).toMatchObject({
340
+ metadata: { id: "chapp_1" },
341
+ });
342
+ });
343
+
172
344
  it("maps a NotFound backend error to ExitCode.NotFound", async () => {
173
345
  const err = await fetchResource(client, ApiResourceKind.agent, { kind: "id", id: "missing" }).catch((e) => e);
174
346
  const classified = classify(err);
@@ -176,7 +348,9 @@ describe("get integration", () => {
176
348
  });
177
349
 
178
350
  it("rejects an unsupported kind with a usage error", async () => {
179
- const err = await fetchResource(client, ApiResourceKind.environment, { kind: "id", id: "x" }).catch((e) => e);
351
+ // session declares Get in the registry but has no get binding — the
352
+ // canonical still-unwired kind (environment gained a binding).
353
+ const err = await fetchResource(client, ApiResourceKind.session, { kind: "id", id: "x" }).catch((e) => e);
180
354
  expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
181
355
  });
182
356
  });
@@ -214,4 +388,64 @@ describe("list integration", () => {
214
388
  expect(out).toContain("reviewer-default");
215
389
  expect(out).toContain("agt_1");
216
390
  });
391
+
392
+ it("lists environments via the search service as JSON", async () => {
393
+ const out = await listResources(client, ApiResourceKind.environment, "acme", 50, "json");
394
+ expect(JSON.parse(out)).toEqual([
395
+ toJson(SearchResultSchema, knownEnvironmentSearchResult, { useProtoFieldName: true }),
396
+ ]);
397
+ });
398
+
399
+ it("renders a human table for environment lists", async () => {
400
+ const out = await listResources(client, ApiResourceKind.environment, "acme", 50, "table");
401
+ expect(out).toContain("NAME");
402
+ expect(out).toContain("acme/clinic-patient-db");
403
+ });
404
+
405
+ it("lists agent channels via the dedicated list RPC as JSON", async () => {
406
+ const out = await listResources(client, ApiResourceKind.agent_channel, "acme", 50, "json");
407
+ expect(JSON.parse(out)).toEqual([toJson(AgentChannelSchema, knownChannel, { useProtoFieldName: true })]);
408
+ });
409
+
410
+ it("renders a human table for agent channels with the serving signals", async () => {
411
+ const out = await listResources(client, ApiResourceKind.agent_channel, "acme", 50, "table");
412
+ expect(out).toContain("PROVIDER");
413
+ expect(out).toContain("STATE");
414
+ expect(out).toContain("ENABLED");
415
+ expect(out).toContain("acme/clinic-assistant"); // agent_ref as org/slug
416
+ expect(out).toContain("whatsapp"); // derived from the provider oneof
417
+ expect(out).toContain("installed"); // status.install_state
418
+ expect(out).toContain("true"); // spec.enabled
419
+ });
420
+
421
+ it("lists channel apps via listByOrg as JSON", async () => {
422
+ const out = await listResources(client, ApiResourceKind.channel_app, "acme", 50, "json");
423
+ expect(JSON.parse(out)).toEqual([
424
+ toJson(ChannelAppSchema, knownChannelApp, { useProtoFieldName: true }),
425
+ toJson(ChannelAppSchema, secondChannelApp, { useProtoFieldName: true }),
426
+ ]);
427
+ });
428
+
429
+ it("renders a human table for channel apps with provider and created date", async () => {
430
+ const out = await listResources(client, ApiResourceKind.channel_app, "acme", 50, "table");
431
+ expect(out).toContain("PROVIDER");
432
+ expect(out).toContain("whatsapp");
433
+ expect(out).toContain("slack");
434
+ expect(out).toContain("2026-07-20"); // status.audit.spec_audit.created_at
435
+ // Secrets never reach the table (redaction markers stay in json/yaml only).
436
+ expect(out).not.toContain("***REDACTED***");
437
+ });
438
+
439
+ it("applies --limit client-side for channel apps (listByOrg has no pagination)", async () => {
440
+ const out = await listResources(client, ApiResourceKind.channel_app, "acme", 1, "json");
441
+ expect(JSON.parse(out)).toEqual([toJson(ChannelAppSchema, knownChannelApp, { useProtoFieldName: true })]);
442
+ });
443
+
444
+ it("rejects a kind whose list is unwired with a usage error", async () => {
445
+ // agent_share declares List in the registry but has no list branch —
446
+ // the canonical still-unwired list kind (the three cutover kinds and
447
+ // environment gained branches).
448
+ const err = await listResources(client, ApiResourceKind.agent_share, "acme", 50, "json").catch((e) => e);
449
+ expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
450
+ });
217
451
  });