@stigmer/cli 3.5.2 → 3.6.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/commands/list.js +8 -5
- package/commands/list.js.map +1 -1
- package/commands/resume.js +5 -1
- package/commands/resume.js.map +1 -1
- package/commands/schedule.d.ts +3 -0
- package/commands/schedule.d.ts.map +1 -0
- package/commands/schedule.js +57 -0
- package/commands/schedule.js.map +1 -0
- package/package.json +5 -5
- package/program.d.ts.map +1 -1
- package/program.js +2 -0
- package/program.js.map +1 -1
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +2 -0
- package/registry/metadata.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +29 -21
- package/registry/verb-support.js.map +1 -1
- package/registry/verbs.d.ts.map +1 -1
- package/registry/verbs.js +0 -1
- package/registry/verbs.js.map +1 -1
- package/resources/apply/apply.d.ts +6 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +14 -24
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/handlers.d.ts +9 -1
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +50 -148
- package/resources/apply/handlers.js.map +1 -1
- package/resources/delete.d.ts +12 -0
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +8 -4
- package/resources/delete.js.map +1 -1
- package/resources/execution.d.ts +14 -4
- package/resources/execution.d.ts.map +1 -1
- package/resources/execution.js +16 -6
- package/resources/execution.js.map +1 -1
- package/resources/get-bindings.d.ts +1 -0
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +8 -1
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts +4 -0
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +95 -27
- package/resources/list.js.map +1 -1
- package/resources/reference.d.ts +2 -0
- package/resources/reference.d.ts.map +1 -1
- package/resources/reference.js +4 -0
- package/resources/reference.js.map +1 -1
- package/resources/schedule.d.ts +19 -0
- package/resources/schedule.d.ts.map +1 -0
- package/resources/schedule.js +96 -0
- package/resources/schedule.js.map +1 -0
- package/src/commands/list.ts +8 -5
- package/src/commands/resume.ts +8 -1
- package/src/commands/schedule.ts +70 -0
- package/src/program.ts +2 -0
- package/src/registry/metadata.ts +2 -0
- package/src/registry/registry.test.ts +113 -8
- package/src/registry/verb-support.ts +29 -21
- package/src/registry/verbs.ts +0 -1
- package/src/resources/apply/apply.integration.test.ts +132 -0
- package/src/resources/apply/apply.ts +16 -25
- package/src/resources/apply/handlers.ts +60 -150
- package/src/resources/delete.ts +8 -4
- package/src/resources/execution.ts +20 -6
- package/src/resources/get-bindings.ts +8 -1
- package/src/resources/list.ts +107 -31
- package/src/resources/mutate-verbs.integration.test.ts +78 -4
- package/src/resources/read-verbs.integration.test.ts +124 -5
- package/src/resources/reference.ts +5 -0
- package/src/resources/schedule.ts +115 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import { APPLY_HANDLERS } from "../resources/apply/handlers.js";
|
|
4
|
+
import { DELETE_HANDLERS } from "../resources/delete.js";
|
|
5
|
+
import { GET_BINDINGS } from "../resources/get-bindings.js";
|
|
6
|
+
import { LIST_HANDLERS, SEARCH_KINDS } from "../resources/list.js";
|
|
4
7
|
import { defaultRegistry } from "./registry.js";
|
|
5
8
|
import { Verb } from "./verbs.js";
|
|
6
9
|
|
|
@@ -72,7 +75,7 @@ describe("registry — verb support matrix", () => {
|
|
|
72
75
|
expect(registry.supportsVerb(ApiResourceKind.workflow_instance, Verb.List)).toBe(false);
|
|
73
76
|
});
|
|
74
77
|
|
|
75
|
-
it("agent_channel supports the declarative
|
|
78
|
+
it("agent_channel supports the full declarative verb set", () => {
|
|
76
79
|
for (const v of [Verb.Apply, Verb.Get, Verb.List, Verb.Delete]) {
|
|
77
80
|
expect(registry.supportsVerb(ApiResourceKind.agent_channel, v)).toBe(true);
|
|
78
81
|
}
|
|
@@ -81,6 +84,23 @@ describe("registry — verb support matrix", () => {
|
|
|
81
84
|
expect(registry.supportsVerb(ApiResourceKind.agent_channel, Verb.Search)).toBe(false);
|
|
82
85
|
});
|
|
83
86
|
|
|
87
|
+
it("schedule supports the full declarative verb set", () => {
|
|
88
|
+
for (const v of [Verb.Apply, Verb.Get, Verb.List, Verb.Delete]) {
|
|
89
|
+
expect(registry.supportsVerb(ApiResourceKind.schedule, v)).toBe(true);
|
|
90
|
+
}
|
|
91
|
+
// Firing rides `stigmer schedule trigger`, never the generic run verb.
|
|
92
|
+
expect(registry.supportsVerb(ApiResourceKind.schedule, Verb.Run)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("session and agent_share promise only apply (narrowed — stigmer/stigmer#354)", () => {
|
|
96
|
+
for (const kind of [ApiResourceKind.session, ApiResourceKind.agent_share]) {
|
|
97
|
+
expect(registry.supportsVerb(kind, Verb.Apply)).toBe(true);
|
|
98
|
+
for (const v of [Verb.Get, Verb.List, Verb.Delete]) {
|
|
99
|
+
expect(registry.supportsVerb(kind, v)).toBe(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
84
104
|
it("typesForVerb(get) includes the core read kinds", () => {
|
|
85
105
|
const kinds = new Set(registry.typesForVerb(Verb.Get).map((t) => t.kind));
|
|
86
106
|
expect(kinds).toContain(ApiResourceKind.agent);
|
|
@@ -98,13 +118,98 @@ describe("registry — completeness", () => {
|
|
|
98
118
|
}
|
|
99
119
|
});
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// The verb matrix is a PROMISE (`list types` prints it; the command layer
|
|
124
|
+
// gates on it), but each verb actually runs through its own dispatch
|
|
125
|
+
// registry. A pair present in the matrix but absent from its registry fails
|
|
126
|
+
// at the point of use with "not implemented" — the stigmer/stigmer#353 bug
|
|
127
|
+
// class. This suite holds the two to strict bidirectional equality so the
|
|
128
|
+
// drift is a red test instead of a runtime surprise. Verbs deliberately
|
|
129
|
+
// withheld are narrowed out of the matrix (recorded in stigmer/stigmer#354),
|
|
130
|
+
// never exempted here — an exemption category would legitimize exactly the
|
|
131
|
+
// state this suite exists to reject.
|
|
132
|
+
describe("registry — verb/dispatch conformance", () => {
|
|
133
|
+
// Special cases are kind/verb pairs that genuinely work through bespoke
|
|
134
|
+
// code instead of a registry entry; each names its dispatch site.
|
|
135
|
+
const DISPATCH: ReadonlyArray<{
|
|
136
|
+
label: string;
|
|
137
|
+
verb: Verb;
|
|
138
|
+
wired: ReadonlySet<ApiResourceKind>;
|
|
139
|
+
specialCases: ReadonlyMap<ApiResourceKind, string>;
|
|
140
|
+
}> = [
|
|
141
|
+
{
|
|
142
|
+
label: "apply",
|
|
143
|
+
verb: Verb.Apply,
|
|
144
|
+
wired: new Set(APPLY_HANDLERS.keys()),
|
|
145
|
+
specialCases: new Map([
|
|
146
|
+
[
|
|
147
|
+
ApiResourceKind.project,
|
|
148
|
+
"the stigmer.yaml declarative/synthesis track (resources/apply/declarative.ts); " +
|
|
149
|
+
"file mode refuses with a pointer there (resolveHandlerForKind)",
|
|
150
|
+
],
|
|
151
|
+
]),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
label: "get",
|
|
155
|
+
verb: Verb.Get,
|
|
156
|
+
wired: new Set(GET_BINDINGS.keys()),
|
|
157
|
+
specialCases: new Map([
|
|
158
|
+
[
|
|
159
|
+
ApiResourceKind.organization,
|
|
160
|
+
"fetchOrganization in resources/get.ts (not org-scoped; a slug resolves via findMyOrganizations)",
|
|
161
|
+
],
|
|
162
|
+
]),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: "list",
|
|
166
|
+
verb: Verb.List,
|
|
167
|
+
wired: new Set([...LIST_HANDLERS.keys(), ...SEARCH_KINDS]),
|
|
168
|
+
specialCases: new Map(),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
label: "delete",
|
|
172
|
+
verb: Verb.Delete,
|
|
173
|
+
wired: new Set(DELETE_HANDLERS.keys()),
|
|
174
|
+
specialCases: new Map([
|
|
175
|
+
[
|
|
176
|
+
ApiResourceKind.organization,
|
|
177
|
+
"planOrganizationDelete in resources/delete.ts (not org-scoped; resolved via memberships)",
|
|
178
|
+
],
|
|
179
|
+
]),
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
it.each(DISPATCH)("every kind promising '$label' has a dispatch entry", ({ verb, wired, specialCases }) => {
|
|
184
|
+
for (const info of registry.all()) {
|
|
185
|
+
if (!info.supportedVerbs.has(verb)) continue;
|
|
186
|
+
if (specialCases.has(info.kind)) continue;
|
|
187
|
+
expect(
|
|
188
|
+
wired.has(info.kind),
|
|
189
|
+
`${info.name} promises '${verb}' in the verb matrix but has no dispatch entry — ` +
|
|
190
|
+
"wire it or narrow the matrix (stigmer/stigmer#353, #354)",
|
|
191
|
+
).toBe(true);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it.each(DISPATCH)("every '$label' dispatch entry is promised in the matrix", ({ verb, wired }) => {
|
|
196
|
+
for (const kind of wired) {
|
|
197
|
+
expect(
|
|
198
|
+
registry.supportsVerb(kind, verb),
|
|
199
|
+
`${ApiResourceKind[kind]} is wired for '${verb}' but the verb matrix does not promise it — ` +
|
|
200
|
+
"dead dispatch the command-layer gate blocks",
|
|
201
|
+
).toBe(true);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("special cases do not shadow a real dispatch entry", () => {
|
|
206
|
+
for (const { label, wired, specialCases } of DISPATCH) {
|
|
207
|
+
for (const kind of specialCases.keys()) {
|
|
208
|
+
expect(
|
|
209
|
+
wired.has(kind),
|
|
210
|
+
`${ApiResourceKind[kind]} '${label}' is documented as a special case yet also has a registry entry — drop one`,
|
|
211
|
+
).toBe(false);
|
|
212
|
+
}
|
|
108
213
|
}
|
|
109
214
|
});
|
|
110
215
|
});
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
// Verb-support matrix — which verbs each resource kind supports.
|
|
2
2
|
//
|
|
3
|
-
// This is CLI-specific policy (not stored in proto),
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// | McpServer | Y | Y | Y | Y | Y | - | - | - | - |
|
|
13
|
-
// | Project | Y* | Y | Y | Y | Y | - | - | - | - |
|
|
14
|
-
// | Execution | - | - | Y | Y | Y** | - | - | - | Y |
|
|
15
|
-
//
|
|
16
|
-
// * Project "apply" triggers SDK synthesis mode.
|
|
17
|
-
// ** Execution "delete" maps to a cancel operation.
|
|
3
|
+
// This is CLI-specific policy (not stored in proto), and it is a PROMISE:
|
|
4
|
+
// `stigmer list types` prints it, the docs type tables are written from it,
|
|
5
|
+
// and the command layer gates on it before dispatch. The conformance test in
|
|
6
|
+
// registry.test.ts holds every promised apply/get/list/delete verb to an
|
|
7
|
+
// actual dispatch entry — and every dispatch entry back to a promise — so a
|
|
8
|
+
// verb listed here either works or fails CI. "Advertised but not
|
|
9
|
+
// implemented" (stigmer/stigmer#353) is a rejected state, not a backlog;
|
|
10
|
+
// verbs deliberately withheld pending a real ops story are recorded in
|
|
11
|
+
// stigmer/stigmer#354, not here.
|
|
18
12
|
|
|
19
13
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
20
14
|
import { Verb } from "./verbs.js";
|
|
@@ -31,17 +25,24 @@ export const VERB_SUPPORT: ReadonlyMap<ApiResourceKind, ReadonlySet<Verb>> = new
|
|
|
31
25
|
],
|
|
32
26
|
[ApiResourceKind.skill, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete, Verb.Push])],
|
|
33
27
|
[ApiResourceKind.mcp_server, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
|
|
28
|
+
// Project apply is the stigmer.yaml declarative/synthesis track, not
|
|
29
|
+
// `apply -f project.yaml` — resolveHandlerForKind carries the teaching
|
|
30
|
+
// refusal, and the conformance test documents it as a special case.
|
|
34
31
|
[ApiResourceKind.project, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
|
|
35
32
|
[ApiResourceKind.api_key, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete])],
|
|
36
33
|
// agent_execution is special — uses dedicated AgentExecutionQueryController
|
|
37
34
|
// RPCs, not the unified SearchService. delete maps to cancel.
|
|
38
35
|
[ApiResourceKind.agent_execution, new Set<Verb>([Verb.Get, Verb.List, Verb.Delete, Verb.Download])],
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// IAM apps are configured declaratively; read/ops verbs are deliberately
|
|
37
|
+
// not promised (never wired, no demand — stigmer/stigmer#354). To add one:
|
|
38
|
+
// dispatch entry + this line, in the same change.
|
|
39
|
+
[ApiResourceKind.identity_provider, new Set<Verb>([Verb.Apply])],
|
|
40
|
+
[ApiResourceKind.oauth_app, new Set<Verb>([Verb.Apply])],
|
|
41
41
|
[ApiResourceKind.environment, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
|
|
42
|
-
// Enable/disable flows through `stigmer share agent
|
|
43
|
-
//
|
|
44
|
-
|
|
42
|
+
// Enable/disable flows through `stigmer share agent`, which is the whole
|
|
43
|
+
// read/ops surface today; only the declarative apply path is promised
|
|
44
|
+
// (stigmer/stigmer#354 records the narrowing).
|
|
45
|
+
[ApiResourceKind.agent_share, new Set<Verb>([Verb.Apply])],
|
|
45
46
|
// The provider install flow is console-driven and cloud-only; the generic
|
|
46
47
|
// verbs cover the declarative path (apply a manifest, inspect, tear down).
|
|
47
48
|
[ApiResourceKind.agent_channel, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
|
|
@@ -52,10 +53,17 @@ export const VERB_SUPPORT: ReadonlyMap<ApiResourceKind, ReadonlySet<Verb>> = new
|
|
|
52
53
|
// generic verbs cover the declarative path (apply a manifest, inspect,
|
|
53
54
|
// tear down — delete is guarded server-side for non-empty datastores).
|
|
54
55
|
[ApiResourceKind.datastore, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
|
|
56
|
+
// Firing is the platform's job (Temporal Schedules); the generic verbs
|
|
57
|
+
// cover the declarative path, and get/list surface the state the dedicated
|
|
58
|
+
// `stigmer schedule` commands act on — spec.enabled (the owner's switch)
|
|
59
|
+
// versus status.paused_reason (the platform's failure latch).
|
|
60
|
+
[ApiResourceKind.schedule, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
|
|
55
61
|
[ApiResourceKind.agent_instance, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
|
|
56
62
|
// No list: proto exposes getByWorkflow (requires workflow_id), not a generic list.
|
|
57
63
|
[ApiResourceKind.workflow_instance, new Set<Verb>([Verb.Apply, Verb.Get, Verb.Delete])],
|
|
58
|
-
|
|
64
|
+
// Sessions are runtime conversation state with no CLI read/ops story yet;
|
|
65
|
+
// get/list/delete are deliberately not promised (stigmer/stigmer#354).
|
|
66
|
+
[ApiResourceKind.session, new Set<Verb>([Verb.Apply])],
|
|
59
67
|
]);
|
|
60
68
|
|
|
61
69
|
export function verbsForKind(kind: ApiResourceKind): ReadonlySet<Verb> {
|
package/src/registry/verbs.ts
CHANGED
|
@@ -17,6 +17,8 @@ import { type ConnectRouter, createClient } from "@connectrpc/connect";
|
|
|
17
17
|
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
18
18
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
19
19
|
import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
|
|
20
|
+
import type { ChannelApp } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
|
|
21
|
+
import { ChannelAppCommandController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/command_pb";
|
|
20
22
|
import type { Datastore } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
21
23
|
import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
|
|
22
24
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
@@ -33,11 +35,13 @@ const openSessions = new Set<ServerHttp2Session>();
|
|
|
33
35
|
let appliedAgents: Agent[] = [];
|
|
34
36
|
let appliedMcps: McpServer[] = [];
|
|
35
37
|
let appliedDatastores: Datastore[] = [];
|
|
38
|
+
let appliedChannelApps: ChannelApp[] = [];
|
|
36
39
|
|
|
37
40
|
beforeEach(() => {
|
|
38
41
|
appliedAgents = [];
|
|
39
42
|
appliedMcps = [];
|
|
40
43
|
appliedDatastores = [];
|
|
44
|
+
appliedChannelApps = [];
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
function writeYaml(dir: string, name: string, body: string): string {
|
|
@@ -66,6 +70,12 @@ beforeAll(async () => {
|
|
|
66
70
|
return req;
|
|
67
71
|
},
|
|
68
72
|
});
|
|
73
|
+
router.service(ChannelAppCommandController, {
|
|
74
|
+
apply: (req) => {
|
|
75
|
+
appliedChannelApps.push(req);
|
|
76
|
+
return req;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
69
79
|
};
|
|
70
80
|
|
|
71
81
|
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
@@ -215,6 +225,128 @@ describe("file-mode apply — datastore", () => {
|
|
|
215
225
|
});
|
|
216
226
|
});
|
|
217
227
|
|
|
228
|
+
// The stigmer/stigmer#353 regression block: ChannelApp apply used to fail
|
|
229
|
+
// with "apply not implemented for Channel App" because the CLI's handler
|
|
230
|
+
// table was a drifted copy of the SDK manifest registry. The handlers now
|
|
231
|
+
// COME FROM that registry, so this suite pins the whole class: the kind
|
|
232
|
+
// applies, secrets round-trip verbatim, and the ordering the registry
|
|
233
|
+
// encodes (app before the channel that references it) is honored.
|
|
234
|
+
const CHANNEL_APP_YAML = [
|
|
235
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
236
|
+
"kind: ChannelApp",
|
|
237
|
+
"metadata:",
|
|
238
|
+
" name: demo-whatsapp",
|
|
239
|
+
" slug: demo-whatsapp",
|
|
240
|
+
"spec:",
|
|
241
|
+
" whatsapp:",
|
|
242
|
+
" app_id: '108954'",
|
|
243
|
+
" app_secret: '***REDACTED***'",
|
|
244
|
+
" access_token: '***REDACTED***'",
|
|
245
|
+
" verify_token: '***REDACTED***'",
|
|
246
|
+
"",
|
|
247
|
+
].join("\n");
|
|
248
|
+
|
|
249
|
+
const AGENT_CHANNEL_YAML = [
|
|
250
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
251
|
+
"kind: AgentChannel",
|
|
252
|
+
"metadata:",
|
|
253
|
+
" name: isc-whatsapp",
|
|
254
|
+
" slug: isc-whatsapp",
|
|
255
|
+
"spec:",
|
|
256
|
+
" enabled: true",
|
|
257
|
+
" agent_ref: { kind: 40, org: acme, slug: clinic-assistant }",
|
|
258
|
+
" whatsapp: { phone_number_id: '106540352242922' }",
|
|
259
|
+
"",
|
|
260
|
+
].join("\n");
|
|
261
|
+
|
|
262
|
+
const SCHEDULE_YAML = [
|
|
263
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
264
|
+
"kind: Schedule",
|
|
265
|
+
"metadata:",
|
|
266
|
+
" name: daily-fee-reminders",
|
|
267
|
+
" slug: daily-fee-reminders",
|
|
268
|
+
"spec:",
|
|
269
|
+
" cron: '0 9 * * *'",
|
|
270
|
+
" time_zone: Asia/Kolkata",
|
|
271
|
+
" enabled: false",
|
|
272
|
+
" agent:",
|
|
273
|
+
" agent_ref: { kind: 40, org: acme, slug: clinic-assistant }",
|
|
274
|
+
" message: run the reminders",
|
|
275
|
+
"",
|
|
276
|
+
].join("\n");
|
|
277
|
+
|
|
278
|
+
describe("file-mode apply — channel app (#353)", () => {
|
|
279
|
+
it("applies a ChannelApp through the raw controller with the redaction markers intact", async () => {
|
|
280
|
+
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
281
|
+
try {
|
|
282
|
+
writeYaml(dir, "chapp.yaml", CHANNEL_APP_YAML);
|
|
283
|
+
const items = resolveApplyItems(dir);
|
|
284
|
+
expect(items).toHaveLength(1);
|
|
285
|
+
|
|
286
|
+
const outcome = await applyItem(controllerFn, items[0], "acme", false);
|
|
287
|
+
expect(outcome.result.message).toBe("Channel App created successfully");
|
|
288
|
+
|
|
289
|
+
expect(appliedChannelApps).toHaveLength(1);
|
|
290
|
+
const spec = appliedChannelApps[0].spec;
|
|
291
|
+
expect(spec?.providerConfig.case).toBe("whatsapp");
|
|
292
|
+
if (spec?.providerConfig.case !== "whatsapp") throw new Error("unreachable");
|
|
293
|
+
expect(spec.providerConfig.value.appId).toBe("108954");
|
|
294
|
+
// The redaction markers must reach the server verbatim — that is the
|
|
295
|
+
// convention by which applying a fetched manifest preserves stored
|
|
296
|
+
// secrets (verb-support's OAuthApp marker note).
|
|
297
|
+
expect(spec.providerConfig.value.appSecret).toBe("***REDACTED***");
|
|
298
|
+
expect(spec.providerConfig.value.accessToken).toBe("***REDACTED***");
|
|
299
|
+
} finally {
|
|
300
|
+
rmSync(dir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("treats a ChannelApp carrying metadata.id as an update", async () => {
|
|
305
|
+
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
306
|
+
try {
|
|
307
|
+
writeYaml(
|
|
308
|
+
dir,
|
|
309
|
+
"chapp.yaml",
|
|
310
|
+
CHANNEL_APP_YAML.replace("metadata:", "metadata:\n id: chapp_existing"),
|
|
311
|
+
);
|
|
312
|
+
const items = resolveApplyItems(dir);
|
|
313
|
+
const outcome = await applyItem(controllerFn, items[0], "acme", false);
|
|
314
|
+
expect(outcome.result.message).toBe("Channel App updated successfully");
|
|
315
|
+
expect(appliedChannelApps[0].metadata?.id).toBe("chapp_existing");
|
|
316
|
+
} finally {
|
|
317
|
+
rmSync(dir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("orders one bundle app → channel → schedule regardless of file order", () => {
|
|
322
|
+
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
323
|
+
try {
|
|
324
|
+
// File names chosen so lexical file order is exactly WRONG — before
|
|
325
|
+
// the registry unification all three kinds fell to a shared default
|
|
326
|
+
// priority and applied in this (broken) file order.
|
|
327
|
+
writeYaml(dir, "a-schedule.yaml", SCHEDULE_YAML);
|
|
328
|
+
writeYaml(dir, "b-channel.yaml", AGENT_CHANNEL_YAML);
|
|
329
|
+
writeYaml(dir, "c-chapp.yaml", CHANNEL_APP_YAML);
|
|
330
|
+
const items = resolveApplyItems(dir);
|
|
331
|
+
expect(items.map((i) => i.handler.displayName)).toEqual(["Channel App", "Agent Channel", "Schedule"]);
|
|
332
|
+
} finally {
|
|
333
|
+
rmSync(dir, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("file-mode apply — project special case", () => {
|
|
339
|
+
it("refuses a Project manifest with a pointer to the stigmer.yaml flow", () => {
|
|
340
|
+
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
341
|
+
try {
|
|
342
|
+
writeYaml(dir, "project.yaml", ["kind: Project", "metadata:", " name: Demo", " slug: demo", ""].join("\n"));
|
|
343
|
+
expect(() => resolveApplyItems(dir)).toThrow(/stigmer\.yaml/);
|
|
344
|
+
} finally {
|
|
345
|
+
rmSync(dir, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
218
350
|
describe("file-mode apply — orchestration", () => {
|
|
219
351
|
it("sorts items into dependency order (mcp_server before agent)", () => {
|
|
220
352
|
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
@@ -39,26 +39,6 @@ export interface ApplyOutcome {
|
|
|
39
39
|
readonly applied?: Message;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Dependency apply order (ascending priority); kinds absent get 99 (applied
|
|
43
|
-
// last). Mirrors Go's applier.DefaultApplyOrder.
|
|
44
|
-
const APPLY_ORDER: ReadonlyMap<ApiResourceKind, number> = new Map([
|
|
45
|
-
[ApiResourceKind.organization, 0],
|
|
46
|
-
[ApiResourceKind.mcp_server, 1],
|
|
47
|
-
// Before agent: an Agent references a Datastore via datastore_usages
|
|
48
|
-
// (the McpServer-before-Agent pattern; mirrors the SDK manifest registry).
|
|
49
|
-
[ApiResourceKind.datastore, 2],
|
|
50
|
-
[ApiResourceKind.agent, 3],
|
|
51
|
-
[ApiResourceKind.workflow, 4],
|
|
52
|
-
[ApiResourceKind.environment, 5],
|
|
53
|
-
[ApiResourceKind.identity_provider, 6],
|
|
54
|
-
[ApiResourceKind.oauth_app, 7],
|
|
55
|
-
[ApiResourceKind.agent_instance, 8],
|
|
56
|
-
[ApiResourceKind.workflow_instance, 9],
|
|
57
|
-
[ApiResourceKind.session, 10],
|
|
58
|
-
// After agent and environment: a share references both.
|
|
59
|
-
[ApiResourceKind.agent_share, 11],
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
42
|
/** Expand a path into ordered, kind-resolved apply items (strict YAML parse). */
|
|
63
43
|
export function resolveApplyItems(path: string): ApplyItem[] {
|
|
64
44
|
const files = resolveYamlFiles(path);
|
|
@@ -83,14 +63,28 @@ export function resolveHandlerForKind(kind: string, where: string): ApplyHandler
|
|
|
83
63
|
const info = defaultRegistry().getByYamlKind(kind);
|
|
84
64
|
if (info === undefined) throw new UsageError(`unknown resource kind '${kind}' in ${where}`);
|
|
85
65
|
if (!info.supportedVerbs.has(Verb.Apply)) throw new UsageError(`${info.displayName} does not support 'apply'`);
|
|
66
|
+
if (info.kind === ApiResourceKind.project) {
|
|
67
|
+
// Project's Apply verb is the stigmer.yaml track, deliberately not a
|
|
68
|
+
// manifest handler: a file-mode apply would create a second, member-less
|
|
69
|
+
// way to apply a project, bypassing the membership reconciler.
|
|
70
|
+
throw new UsageError(
|
|
71
|
+
`a Project is not applied from a resource manifest (${where}): ` +
|
|
72
|
+
"run `stigmer apply` in the directory containing stigmer.yaml and the project is reconciled from its members",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
86
75
|
const handler = APPLY_HANDLERS.get(info.kind);
|
|
87
76
|
if (handler === undefined) throw new UsageError(`apply not implemented for ${info.displayName}`);
|
|
88
77
|
return handler;
|
|
89
78
|
}
|
|
90
79
|
|
|
91
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Stable sort by dependency priority (Array.prototype.sort is stable in V8).
|
|
82
|
+
* The priority rides each handler (`applyOrder`, sourced from the SDK
|
|
83
|
+
* manifest registry) — there is deliberately no local ordering table to
|
|
84
|
+
* drift from it.
|
|
85
|
+
*/
|
|
92
86
|
export function sortApplyItems(items: ApplyItem[]): ApplyItem[] {
|
|
93
|
-
items.sort((a, b) =>
|
|
87
|
+
items.sort((a, b) => a.handler.applyOrder - b.handler.applyOrder);
|
|
94
88
|
return items;
|
|
95
89
|
}
|
|
96
90
|
|
|
@@ -202,6 +196,3 @@ function buildDryRunResult(handler: ApplyHandler, message: Message): CommandResu
|
|
|
202
196
|
return result;
|
|
203
197
|
}
|
|
204
198
|
|
|
205
|
-
function priorityOf(kind: ApiResourceKind): number {
|
|
206
|
-
return APPLY_ORDER.get(kind) ?? 99;
|
|
207
|
-
}
|