@app-vitals/agent-admin 0.16.0 → 0.16.2

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * admin/src/specialty-manifest-loader.unit.test.ts
3
3
  * Unit tests for SpecialtyRegistry — the disk-backed resolver from an agent's
4
- * specialty to its parsed manifest, with the unknown-type "coding" fallback.
4
+ * specialty to its parsed manifest, with the unknown-type default-type fallback.
5
5
  *
6
6
  * Pure logic against an injected reader/warner — no filesystem, no DB. See
7
7
  * docs/testing.md for the unit-layer contract.
@@ -9,10 +9,10 @@
9
9
 
10
10
  import { describe, expect, it } from "bun:test";
11
11
  import {
12
- SpecialtyRegistry,
13
12
  DEFAULT_SPECIALTY_NAME,
14
- resolveSpecialtiesDir,
15
13
  resolveDefaultSpecialtyName,
14
+ resolveSpecialtiesDir,
15
+ SpecialtyRegistry,
16
16
  } from "./specialty-manifest-loader.ts";
17
17
 
18
18
  /** A minimal valid Specialty manifest YAML for a given type name. */
@@ -49,17 +49,17 @@ describe("SpecialtyRegistry", () => {
49
49
  defaultTypeName: DEFAULT_SPECIALTY_NAME,
50
50
  read: (t) => manifestYaml(t),
51
51
  });
52
- const manifest = registry.getManifest("coding");
53
- expect(manifest.metadata.name).toBe("coding");
54
- expect(manifest.crons[0]?.name).toBe("coding-cron");
52
+ const manifest = registry.getManifest("specialist");
53
+ expect(manifest.metadata.name).toBe("specialist");
54
+ expect(manifest.crons[0]?.name).toBe("specialist-cron");
55
55
  });
56
56
 
57
- it("falls back to the coding manifest with a warning for an unknown specialty", () => {
57
+ it("falls back to the default (specialist) manifest with a warning for an unknown specialty", () => {
58
58
  const warnings: string[] = [];
59
59
  const registry = new SpecialtyRegistry({
60
60
  defaultTypeName: DEFAULT_SPECIALTY_NAME,
61
61
  read: (t) => {
62
- if (t === DEFAULT_SPECIALTY_NAME) return manifestYaml("coding");
62
+ if (t === DEFAULT_SPECIALTY_NAME) return manifestYaml("specialist");
63
63
  throw new Error(`ENOENT: no manifest for ${t}`);
64
64
  },
65
65
  warn: (m) => warnings.push(m),
@@ -67,51 +67,49 @@ describe("SpecialtyRegistry", () => {
67
67
 
68
68
  const manifest = registry.getManifest("renamed-type");
69
69
 
70
- expect(manifest.metadata.name).toBe("coding");
70
+ expect(manifest.metadata.name).toBe("specialist");
71
71
  expect(warnings).toHaveLength(1);
72
72
  expect(warnings[0]).toContain("renamed-type");
73
- expect(warnings[0]).toContain("coding");
73
+ expect(warnings[0]).toContain("specialist");
74
74
  });
75
75
 
76
- it("throws (does not silently no-op) when the coding fallback itself cannot load", () => {
77
- const registry = new SpecialtyRegistry({
78
- defaultTypeName: DEFAULT_SPECIALTY_NAME,
79
- read: () => {
80
- throw new Error("ENOENT: coding manifest missing");
81
- },
82
- warn: () => {},
83
- });
84
-
85
- // Requesting the default type when it can't load must surface a hard error
86
- // rather than degrade into an empty cron set that would strip system crons.
87
- expect(() => registry.getManifest("coding")).toThrow();
88
- });
89
-
90
- it("names SHIPWRIGHT_SPECIALTIES_DIR and SHIPWRIGHT_DEFAULT_AGENT_TYPE when the default specialty is missing", () => {
76
+ it("never throws: an unloadable default degrades to an empty-crons manifest with warnings naming the env knobs", () => {
77
+ const warnings: string[] = [];
91
78
  const registry = new SpecialtyRegistry({
92
79
  read: () => {
93
80
  throw new Error("ENOENT: no manifest");
94
81
  },
95
- warn: () => {},
82
+ warn: (m) => warnings.push(m),
96
83
  typesDir: "/nonexistent/specialties",
97
84
  defaultTypeName: "absent",
98
85
  });
99
86
 
100
87
  for (const specialty of ["absent", "some-other-type"]) {
101
- let message = "";
102
- try {
103
- registry.getManifest(specialty);
104
- } catch (err) {
105
- message = (err as Error).message;
106
- }
107
- expect(message).toContain('default specialty "absent"');
108
- expect(message).toContain("/nonexistent/specialties");
109
- expect(message).toContain("SHIPWRIGHT_SPECIALTIES_DIR");
110
- expect(message).toContain("SHIPWRIGHT_DEFAULT_AGENT_TYPE");
111
- expect(message).not.toContain("startup-time bug");
88
+ warnings.length = 0;
89
+ const manifest = registry.getManifest(specialty);
90
+ expect(manifest.crons).toEqual([]);
91
+ expect(manifest.plugins).toEqual([]);
92
+ const all = warnings.join("\n");
93
+ expect(all).toContain('default specialty "absent"');
94
+ expect(all).toContain("/nonexistent/specialties");
95
+ expect(all).toContain("SHIPWRIGHT_SPECIALTIES_DIR");
96
+ expect(all).toContain("SHIPWRIGHT_DEFAULT_AGENT_TYPE");
112
97
  }
113
98
  });
114
99
 
100
+ it("uses the default manifest silently when the default type itself is requested and loads", () => {
101
+ const warnings: string[] = [];
102
+ const registry = new SpecialtyRegistry({
103
+ defaultTypeName: DEFAULT_SPECIALTY_NAME,
104
+ read: (t) => manifestYaml(t),
105
+ warn: (m) => warnings.push(m),
106
+ });
107
+ expect(registry.getManifest(DEFAULT_SPECIALTY_NAME).metadata.name).toBe(
108
+ "specialist",
109
+ );
110
+ expect(warnings).toEqual([]);
111
+ });
112
+
115
113
  it("caches a resolved manifest — the reader is called once per type", () => {
116
114
  let reads = 0;
117
115
  const registry = new SpecialtyRegistry({
@@ -122,15 +120,15 @@ describe("SpecialtyRegistry", () => {
122
120
  },
123
121
  });
124
122
 
125
- registry.getManifest("coding");
126
- registry.getManifest("coding");
123
+ registry.getManifest("specialist");
124
+ registry.getManifest("specialist");
127
125
 
128
126
  expect(reads).toBe(1);
129
127
  });
130
128
 
131
129
  // ─── tryGetManifest — non-fallback lookup ────────────────────────────────
132
130
  //
133
- // Unlike getManifest(), tryGetManifest() must NOT fall back to "coding" for
131
+ // Unlike getManifest(), tryGetManifest() must NOT fall back to the default type for
134
132
  // an unknown specialty — it returns undefined instead. This is what lets
135
133
  // POST /agents distinguish "resolved to the exact requested type" from
136
134
  // "silently degraded", which the cron-reconcile boot path (getManifest())
@@ -141,8 +139,8 @@ describe("SpecialtyRegistry", () => {
141
139
  defaultTypeName: DEFAULT_SPECIALTY_NAME,
142
140
  read: (t) => manifestYaml(t),
143
141
  });
144
- const manifest = registry.tryGetManifest("coding");
145
- expect(manifest?.metadata.name).toBe("coding");
142
+ const manifest = registry.tryGetManifest("specialist");
143
+ expect(manifest?.metadata.name).toBe("specialist");
146
144
  });
147
145
 
148
146
  it("tryGetManifest returns undefined for an unknown specialty — no fallback, no warning", () => {
@@ -150,7 +148,7 @@ describe("SpecialtyRegistry", () => {
150
148
  const registry = new SpecialtyRegistry({
151
149
  defaultTypeName: DEFAULT_SPECIALTY_NAME,
152
150
  read: (t) => {
153
- if (t === DEFAULT_SPECIALTY_NAME) return manifestYaml("coding");
151
+ if (t === DEFAULT_SPECIALTY_NAME) return manifestYaml("specialist");
154
152
  throw new Error(`ENOENT: no manifest for ${t}`);
155
153
  },
156
154
  warn: (m) => warnings.push(m),
@@ -162,6 +160,38 @@ describe("SpecialtyRegistry", () => {
162
160
  expect(warnings).toHaveLength(0);
163
161
  });
164
162
 
163
+ it("tryGetManifest resolves the built-in default type when no specialty is discoverable", () => {
164
+ const registry = new SpecialtyRegistry({
165
+ defaultTypeName: DEFAULT_SPECIALTY_NAME,
166
+ list: () => {
167
+ throw new Error("ENOENT: no such file or directory");
168
+ },
169
+ read: (t) => {
170
+ throw new Error(`ENOENT: no manifest for ${t}`);
171
+ },
172
+ warn: () => {},
173
+ });
174
+
175
+ expect(registry.tryGetManifest(DEFAULT_SPECIALTY_NAME)?.metadata.name).toBe(
176
+ DEFAULT_SPECIALTY_NAME,
177
+ );
178
+ expect(registry.tryGetManifest("nonexistent-type")).toBeUndefined();
179
+ });
180
+
181
+ it("tryGetManifest does not synthesize the default type when other specialties are discoverable", () => {
182
+ const registry = new SpecialtyRegistry({
183
+ defaultTypeName: DEFAULT_SPECIALTY_NAME,
184
+ list: () => ["coding"],
185
+ read: (t) => {
186
+ if (t === "coding") return manifestYaml("coding");
187
+ throw new Error(`ENOENT: no manifest for ${t}`);
188
+ },
189
+ warn: () => {},
190
+ });
191
+
192
+ expect(registry.tryGetManifest(DEFAULT_SPECIALTY_NAME)).toBeUndefined();
193
+ });
194
+
165
195
  it("tryGetManifest shares the cache with getManifest — reader called once per type", () => {
166
196
  let reads = 0;
167
197
  const registry = new SpecialtyRegistry({
@@ -172,8 +202,8 @@ describe("SpecialtyRegistry", () => {
172
202
  },
173
203
  });
174
204
 
175
- registry.getManifest("coding");
176
- registry.tryGetManifest("coding");
205
+ registry.getManifest("specialist");
206
+ registry.tryGetManifest("specialist");
177
207
 
178
208
  expect(reads).toBe(1);
179
209
  });
@@ -261,21 +291,54 @@ voice: true
261
291
  expect(types).toEqual([{ name: "coding", displayName: "coding" }]);
262
292
  });
263
293
 
264
- it("listTypes returns an empty array when no type directories are discovered", () => {
294
+ it("listTypes falls back to the built-in default option when no type directories are discovered", () => {
265
295
  const registry = new SpecialtyRegistry({
266
296
  defaultTypeName: DEFAULT_SPECIALTY_NAME,
267
297
  list: () => [],
268
298
  read: (t) => manifestYaml(t),
269
299
  });
270
300
 
271
- expect(registry.listTypes()).toEqual([]);
301
+ expect(registry.listTypes()).toEqual([
302
+ { name: DEFAULT_SPECIALTY_NAME, displayName: "Specialist Agent" },
303
+ ]);
304
+ });
305
+
306
+ it("listTypes warns and returns the built-in default when the list fn throws (missing dir)", () => {
307
+ const warnings: string[] = [];
308
+ const registry = new SpecialtyRegistry({
309
+ defaultTypeName: DEFAULT_SPECIALTY_NAME,
310
+ warn: (m) => warnings.push(m),
311
+ list: () => {
312
+ throw new Error("ENOENT: no such file or directory");
313
+ },
314
+ read: (t) => manifestYaml(t),
315
+ });
316
+
317
+ expect(registry.listTypes()).toEqual([
318
+ { name: DEFAULT_SPECIALTY_NAME, displayName: "Specialist Agent" },
319
+ ]);
320
+ expect(warnings).toHaveLength(1);
321
+ expect(warnings[0]).toContain("ENOENT");
322
+ });
323
+
324
+ it("listTypes does not throw on a nonexistent typesDir with the real lister", () => {
325
+ const warnings: string[] = [];
326
+ const registry = new SpecialtyRegistry({
327
+ typesDir: "/nonexistent/agent-types",
328
+ warn: (m) => warnings.push(m),
329
+ });
330
+
331
+ expect(registry.listTypes()).toEqual([
332
+ { name: DEFAULT_SPECIALTY_NAME, displayName: "Specialist Agent" },
333
+ ]);
334
+ expect(warnings.length).toBeGreaterThan(0);
272
335
  });
273
336
  });
274
337
 
275
338
  describe("specialties option resolution", () => {
276
- it("defaults to coding and the repo-root specialties/ with no env set", () => {
277
- expect(resolveDefaultSpecialtyName({})).toBe("coding");
278
- expect(resolveSpecialtiesDir({})).toMatch(/specialties$/);
339
+ it("defaults to specialist and the shipped agent-types/ with no env set", () => {
340
+ expect(resolveDefaultSpecialtyName({})).toBe("specialist");
341
+ expect(resolveSpecialtiesDir({})).toMatch(/agent-types$/);
279
342
  });
280
343
 
281
344
  it("honours SHIPWRIGHT_DEFAULT_AGENT_TYPE and SHIPWRIGHT_SPECIALTIES_DIR", () => {