@app-vitals/agent-admin 0.16.2 → 1.0.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/e2e/agents-page.e2e.ts +2 -2
- package/openapi.json +3 -3
- package/openapi.platform.json +3 -3
- package/package.json +3 -5
- package/src/account-agent-create.ts +1 -1
- package/src/admin-boot.ts +6 -6
- package/src/admin-extension.ts +16 -15
- package/src/admin-extension.unit.test.ts +2 -2
- package/src/admin-local-agent.smoke.test.ts +8 -5
- package/src/admin-ui-pages.ts +4 -4
- package/src/admin-ui.smoke.test.ts +27 -39
- package/src/admin-ui.ts +12 -12
- package/src/agent-chat-tokens.smoke.test.ts +2 -2
- package/src/agent-cron-jobs.integration.test.ts +23 -21
- package/src/agent-cron-jobs.ts +30 -40
- package/src/agent-cron-jobs.unit.test.ts +51 -39
- package/src/agent-cron-run-stats.smoke.test.ts +2 -2
- package/src/agent-manifest.ts +2 -6
- package/src/agent-patch-author-allowlist.integration.test.ts +16 -27
- package/src/agents-api.integration.test.ts +20 -31
- package/src/agents-api.sentry.smoke.test.ts +2 -2
- package/src/agents-api.smoke.test.ts +17 -30
- package/src/agents-api.ts +13 -13
- package/src/agents.integration.test.ts +21 -31
- package/src/agents.ts +27 -34
- package/src/agents.unit.test.ts +35 -55
- package/src/api.smoke.test.ts +2 -2
- package/src/chat-service-provisioning-client.integration.test.ts +1 -1
- package/src/server.smoke.test.ts +2 -2
- package/src/specialty-defaults.ts +4 -4
- package/src/specialty-types.ts +92 -0
- package/src/specialty-types.unit.test.ts +67 -0
- package/e2e/fixtures/specialties/coding/manifest.yaml +0 -33
- package/src/coding-task-store-provisioning-client.integration.test.ts +0 -372
- package/src/coding-task-store-provisioning-client.ts +0 -156
- package/src/fixtures/task-store-provisioning-cassette.json +0 -85
- package/src/specialty-manifest-loader.fixture.integration.test.ts +0 -67
- package/src/specialty-manifest-loader.ts +0 -285
- package/src/specialty-manifest-loader.unit.test.ts +0 -364
- package/src/specialty-registry.schema.unit.test.ts +0 -35
- package/src/specialty-registry.ts +0 -236
- package/src/specialty-registry.unit.test.ts +0 -272
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* admin/src/specialty-registry.unit.test.ts
|
|
3
|
-
* Unit tests for the pure Specialty manifest schema + parser. No I/O, no
|
|
4
|
-
* network, no fs — parseSpecialtyManifest takes a YAML string in memory and
|
|
5
|
-
* returns/throws in-process.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, expect, it } from "bun:test";
|
|
9
|
-
import { stringify as stringifyYaml } from "yaml";
|
|
10
|
-
import {
|
|
11
|
-
SpecialtyManifestSchema,
|
|
12
|
-
parseSpecialtyManifest,
|
|
13
|
-
} from "./specialty-registry.ts";
|
|
14
|
-
|
|
15
|
-
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
/** A minimal valid manifest object. Pass overrides for the fields a test cares about. */
|
|
18
|
-
function baseManifest(overrides: Record<string, unknown> = {}) {
|
|
19
|
-
return {
|
|
20
|
-
apiVersion: "shipwright.dev/v1alpha1",
|
|
21
|
-
kind: "Specialty",
|
|
22
|
-
metadata: {
|
|
23
|
-
name: "dude-agent",
|
|
24
|
-
displayName: "The Dude",
|
|
25
|
-
description: "A laid-back team member agent.",
|
|
26
|
-
version: "1.0.0",
|
|
27
|
-
skills: [],
|
|
28
|
-
},
|
|
29
|
-
identity: { templatesDir: "templates/dude" },
|
|
30
|
-
crons: [],
|
|
31
|
-
plugins: [],
|
|
32
|
-
tools: [],
|
|
33
|
-
env: { required: [], optional: [] },
|
|
34
|
-
members: [],
|
|
35
|
-
repos: [],
|
|
36
|
-
chat: true,
|
|
37
|
-
voice: false,
|
|
38
|
-
...overrides,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Round-trips a manifest object through YAML so tests exercise the real parse path. */
|
|
43
|
-
function parseManifestYaml(manifest: Record<string, unknown>) {
|
|
44
|
-
return parseSpecialtyManifest(stringifyYaml(manifest));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function expectThrowsWithMessage(manifest: Record<string, unknown>, ...fragments: string[]) {
|
|
48
|
-
expect(() => parseManifestYaml(manifest)).toThrow();
|
|
49
|
-
try {
|
|
50
|
-
parseManifestYaml(manifest);
|
|
51
|
-
throw new Error("expected parseSpecialtyManifest to throw");
|
|
52
|
-
} catch (err) {
|
|
53
|
-
const message = (err as Error).message;
|
|
54
|
-
for (const fragment of fragments) {
|
|
55
|
-
expect(message).toContain(fragment);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─── Valid manifest ─────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
describe("parseSpecialtyManifest — valid manifest", () => {
|
|
63
|
-
it("parses a valid minimal manifest successfully", () => {
|
|
64
|
-
const manifest = parseManifestYaml(
|
|
65
|
-
baseManifest({
|
|
66
|
-
metadata: {
|
|
67
|
-
name: "dude-agent",
|
|
68
|
-
displayName: "The Dude",
|
|
69
|
-
description: "A laid-back team member agent.",
|
|
70
|
-
version: "1.0.0",
|
|
71
|
-
skills: [
|
|
72
|
-
{
|
|
73
|
-
id: "chill",
|
|
74
|
-
name: "Stay Chill",
|
|
75
|
-
description: "Keeps things mellow.",
|
|
76
|
-
tags: ["vibe"],
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
crons: [
|
|
81
|
-
{
|
|
82
|
-
name: "morning-brief",
|
|
83
|
-
schedule: "0 9 * * 1-5",
|
|
84
|
-
prompt: "Run the morning brief.",
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
plugins: ["@shipwright/plugin"],
|
|
88
|
-
tools: ["Read", "Write"],
|
|
89
|
-
env: {
|
|
90
|
-
required: [
|
|
91
|
-
{
|
|
92
|
-
key: "SHIPWRIGHT_API_URL",
|
|
93
|
-
description: "Base URL of the admin API.",
|
|
94
|
-
secret: false,
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
optional: [
|
|
98
|
-
{
|
|
99
|
-
key: "GROQ_API_KEY",
|
|
100
|
-
description: "Optional Groq API key for voice.",
|
|
101
|
-
secret: true,
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
members: ["alice"],
|
|
106
|
-
repos: ["my-org/my-repo"],
|
|
107
|
-
}),
|
|
108
|
-
);
|
|
109
|
-
expect(manifest.apiVersion).toBe("shipwright.dev/v1alpha1");
|
|
110
|
-
expect(manifest.kind).toBe("Specialty");
|
|
111
|
-
expect(manifest.metadata.name).toBe("dude-agent");
|
|
112
|
-
expect(manifest.identity.templatesDir).toBe("templates/dude");
|
|
113
|
-
expect(manifest.crons).toHaveLength(1);
|
|
114
|
-
expect(manifest.chat).toBe(true);
|
|
115
|
-
expect(manifest.voice).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("accepts a cron whose parentCron resolves to a sibling cron name", () => {
|
|
119
|
-
const manifest = parseManifestYaml(
|
|
120
|
-
baseManifest({
|
|
121
|
-
crons: [
|
|
122
|
-
{
|
|
123
|
-
name: "parent-cron",
|
|
124
|
-
schedule: "0 9 * * 1-5",
|
|
125
|
-
prompt: "Parent prompt.",
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: "child-cron",
|
|
129
|
-
schedule: "0 10 * * 1-5",
|
|
130
|
-
prompt: "Child prompt.",
|
|
131
|
-
parentCron: "parent-cron",
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
136
|
-
expect(manifest.crons[1].parentCron).toBe("parent-cron");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ─── Acceptance criterion 1 — failure cases ────────────────────────────────
|
|
141
|
-
|
|
142
|
-
describe("parseSpecialtyManifest — invalid manifests fail with field-path errors", () => {
|
|
143
|
-
it("rejects a cron missing its required schedule field", () => {
|
|
144
|
-
expectThrowsWithMessage(
|
|
145
|
-
baseManifest({
|
|
146
|
-
crons: [{ name: "bad-cron", prompt: "Missing schedule." }],
|
|
147
|
-
}),
|
|
148
|
-
"crons",
|
|
149
|
-
"schedule",
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("rejects an unknown top-level field", () => {
|
|
154
|
-
expectThrowsWithMessage(
|
|
155
|
-
baseManifest({ unknownField: "surprise" }),
|
|
156
|
-
"unknownField",
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("rejects an env entry carrying a value key (trust boundary)", () => {
|
|
161
|
-
expectThrowsWithMessage(
|
|
162
|
-
baseManifest({
|
|
163
|
-
env: {
|
|
164
|
-
required: [
|
|
165
|
-
{
|
|
166
|
-
key: "SOME_KEY",
|
|
167
|
-
description: "has a value, which is forbidden",
|
|
168
|
-
secret: false,
|
|
169
|
-
value: "should not be allowed",
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
optional: [],
|
|
173
|
-
},
|
|
174
|
-
}),
|
|
175
|
-
"env",
|
|
176
|
-
"value",
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("rejects a dangling parentCron reference", () => {
|
|
181
|
-
expectThrowsWithMessage(
|
|
182
|
-
baseManifest({
|
|
183
|
-
crons: [
|
|
184
|
-
{
|
|
185
|
-
name: "only-cron",
|
|
186
|
-
schedule: "0 9 * * 1-5",
|
|
187
|
-
prompt: "Prompt.",
|
|
188
|
-
parentCron: "does-not-exist",
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
}),
|
|
192
|
-
"parentCron",
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("rejects a non-RFC1123 metadata.name", () => {
|
|
197
|
-
expectThrowsWithMessage(
|
|
198
|
-
baseManifest({
|
|
199
|
-
metadata: {
|
|
200
|
-
name: "Dude_Agent!",
|
|
201
|
-
displayName: "The Dude",
|
|
202
|
-
description: "desc",
|
|
203
|
-
version: "1.0.0",
|
|
204
|
-
skills: [],
|
|
205
|
-
},
|
|
206
|
-
}),
|
|
207
|
-
"metadata",
|
|
208
|
-
"name",
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ─── Schema-level checks (direct safeParse, matching repo convention) ──────
|
|
214
|
-
|
|
215
|
-
describe("SpecialtyManifestSchema.safeParse", () => {
|
|
216
|
-
it("rejects a self-referencing parentCron", () => {
|
|
217
|
-
const result = SpecialtyManifestSchema.safeParse(
|
|
218
|
-
baseManifest({
|
|
219
|
-
crons: [
|
|
220
|
-
{
|
|
221
|
-
name: "self-cron",
|
|
222
|
-
schedule: "0 9 * * 1-5",
|
|
223
|
-
prompt: "Prompt.",
|
|
224
|
-
parentCron: "self-cron",
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
}),
|
|
228
|
-
);
|
|
229
|
-
expect(result.success).toBe(false);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("defaults silent/enabled on a cron entry when omitted", () => {
|
|
233
|
-
const result = SpecialtyManifestSchema.safeParse(
|
|
234
|
-
baseManifest({
|
|
235
|
-
crons: [
|
|
236
|
-
{ name: "morning-brief", schedule: "0 9 * * 1-5", prompt: "Prompt." },
|
|
237
|
-
],
|
|
238
|
-
}),
|
|
239
|
-
);
|
|
240
|
-
expect(result.success).toBe(true);
|
|
241
|
-
if (result.success) {
|
|
242
|
-
expect(result.data.crons[0].silent).toBe(false);
|
|
243
|
-
expect(result.data.crons[0].enabled).toBe(true);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("rejects a bad apiVersion literal", () => {
|
|
248
|
-
const result = SpecialtyManifestSchema.safeParse(
|
|
249
|
-
baseManifest({ apiVersion: "shipwright.dev/v1" }),
|
|
250
|
-
);
|
|
251
|
-
expect(result.success).toBe(false);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("rejects a repos entry not in org/repo format", () => {
|
|
255
|
-
const result = SpecialtyManifestSchema.safeParse(
|
|
256
|
-
baseManifest({ repos: ["not-a-valid-repo"] }),
|
|
257
|
-
);
|
|
258
|
-
expect(result.success).toBe(false);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("accepts an optional resources override", () => {
|
|
262
|
-
const result = SpecialtyManifestSchema.safeParse(
|
|
263
|
-
baseManifest({
|
|
264
|
-
resources: {
|
|
265
|
-
requests: { cpu: "500m", memory: "2Gi", "ephemeral-storage": "1Gi" },
|
|
266
|
-
limits: { memory: "8Gi", "ephemeral-storage": "1Gi" },
|
|
267
|
-
},
|
|
268
|
-
}),
|
|
269
|
-
);
|
|
270
|
-
expect(result.success).toBe(true);
|
|
271
|
-
});
|
|
272
|
-
});
|