@app-vitals/agent-admin 0.15.0 → 0.16.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.
- package/package.json +2 -2
- package/src/account-agent-create.ts +4 -4
- package/src/account-agent-create.unit.test.ts +2 -2
- package/src/admin-boot.ts +12 -1
- package/src/admin-extension.ts +29 -0
- package/src/admin-extension.unit.test.ts +19 -0
- package/src/admin-local-agent.smoke.test.ts +1 -1
- package/src/admin-ui-pages.ts +5 -4
- package/src/admin-ui-pages.unit.test.ts +34 -6
- package/src/admin-ui.ts +6 -1
- package/src/agent-cron-jobs.integration.test.ts +94 -0
- package/src/agent-cron-jobs.ts +23 -25
- package/src/agent-cron-jobs.unit.test.ts +126 -18
- package/src/agents-api.ts +6 -1
- package/src/agents.ts +13 -3
- package/src/agents.unit.test.ts +70 -0
- package/src/specialty-defaults.ts +35 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +7 -3
- package/src/specialty-manifest-loader.ts +80 -90
- package/src/specialty-manifest-loader.unit.test.ts +46 -48
|
@@ -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
|
|
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("
|
|
53
|
-
expect(manifest.metadata.name).toBe("
|
|
54
|
-
expect(manifest.crons[0]?.name).toBe("
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
74
|
-
});
|
|
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();
|
|
73
|
+
expect(warnings[0]).toContain("specialist");
|
|
88
74
|
});
|
|
89
75
|
|
|
90
|
-
it("
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
expect(
|
|
108
|
-
expect(
|
|
109
|
-
expect(
|
|
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("
|
|
126
|
-
registry.getManifest("
|
|
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
|
|
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("
|
|
145
|
-
expect(manifest?.metadata.name).toBe("
|
|
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("
|
|
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),
|
|
@@ -172,8 +170,8 @@ describe("SpecialtyRegistry", () => {
|
|
|
172
170
|
},
|
|
173
171
|
});
|
|
174
172
|
|
|
175
|
-
registry.getManifest("
|
|
176
|
-
registry.tryGetManifest("
|
|
173
|
+
registry.getManifest("specialist");
|
|
174
|
+
registry.tryGetManifest("specialist");
|
|
177
175
|
|
|
178
176
|
expect(reads).toBe(1);
|
|
179
177
|
});
|
|
@@ -273,9 +271,9 @@ voice: true
|
|
|
273
271
|
});
|
|
274
272
|
|
|
275
273
|
describe("specialties option resolution", () => {
|
|
276
|
-
it("defaults to
|
|
277
|
-
expect(resolveDefaultSpecialtyName({})).toBe("
|
|
278
|
-
expect(resolveSpecialtiesDir({})).toMatch(/
|
|
274
|
+
it("defaults to specialist and the shipped agent-types/ with no env set", () => {
|
|
275
|
+
expect(resolveDefaultSpecialtyName({})).toBe("specialist");
|
|
276
|
+
expect(resolveSpecialtiesDir({})).toMatch(/agent-types$/);
|
|
279
277
|
});
|
|
280
278
|
|
|
281
279
|
it("honours SHIPWRIGHT_DEFAULT_AGENT_TYPE and SHIPWRIGHT_SPECIALTIES_DIR", () => {
|