@app-vitals/agent-admin 0.16.2 → 1.1.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 +9 -9
- package/e2e/test-server.ts +6 -6
- 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/api.ts +1 -1
- 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
- /package/e2e/fixtures/specialties/{coding → specialist}/templates/CLAUDE.md.template +0 -0
- /package/e2e/fixtures/specialties/{coding → specialist}/templates/IDENTITY.md.template +0 -0
- /package/e2e/fixtures/specialties/{coding → specialist}/templates/SOUL.md.template +0 -0
|
@@ -18,8 +18,14 @@ import {
|
|
|
18
18
|
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
19
19
|
import { NotFoundError, UnprocessableEntityError } from "./errors.ts";
|
|
20
20
|
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
21
|
-
import type {
|
|
22
|
-
|
|
21
|
+
import type {
|
|
22
|
+
SpecialtyDefaultCron,
|
|
23
|
+
SpecialtyDefaultsResolver,
|
|
24
|
+
} from "./specialty-defaults.ts";
|
|
25
|
+
import {
|
|
26
|
+
SPECIALIST_TYPE,
|
|
27
|
+
type SpecialtyTypeResolver,
|
|
28
|
+
} from "./specialty-types.ts";
|
|
23
29
|
|
|
24
30
|
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
25
31
|
|
|
@@ -34,9 +40,7 @@ function makePrisma(): PrismaClient {
|
|
|
34
40
|
// cron, covering every field reconcile maps into a row.
|
|
35
41
|
const FIXTURE_TYPE = "fixture";
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const FIXTURE_CRONS: ManifestCron[] = [
|
|
43
|
+
const FIXTURE_CRONS: SpecialtyDefaultCron[] = [
|
|
40
44
|
{
|
|
41
45
|
name: "fixture-loop",
|
|
42
46
|
schedule: "*/30 * * * *",
|
|
@@ -70,23 +74,16 @@ const FIXTURE_CRONS: ManifestCron[] = [
|
|
|
70
74
|
},
|
|
71
75
|
];
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
/** The extension hook supplying FIXTURE_TYPE's crons. */
|
|
78
|
+
const fixtureDefaults: SpecialtyDefaultsResolver = (specialty) =>
|
|
79
|
+
specialty === FIXTURE_TYPE ? { crons: FIXTURE_CRONS } : undefined;
|
|
76
80
|
|
|
77
81
|
/** Resolves only FIXTURE_TYPE; every other type is unknown. */
|
|
78
|
-
const fixtureResolver:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
84
|
-
tryGetManifest(specialty: string): SpecialtyManifest | undefined {
|
|
85
|
-
return specialty === FIXTURE_TYPE ? fixtureManifest : undefined;
|
|
86
|
-
},
|
|
87
|
-
listTypes() {
|
|
88
|
-
return [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }];
|
|
89
|
-
},
|
|
82
|
+
const fixtureResolver: SpecialtyTypeResolver = {
|
|
83
|
+
getType: () => SPECIALIST_TYPE,
|
|
84
|
+
tryGetType: (specialty) =>
|
|
85
|
+
specialty === FIXTURE_TYPE ? SPECIALIST_TYPE : undefined,
|
|
86
|
+
listTypes: () => [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }],
|
|
90
87
|
};
|
|
91
88
|
|
|
92
89
|
async function createAgent(
|
|
@@ -109,7 +106,12 @@ describeOrSkip("AgentCronJobService (integration)", () => {
|
|
|
109
106
|
await prisma.agentTool.deleteMany();
|
|
110
107
|
await prisma.agentEnv.deleteMany();
|
|
111
108
|
await prisma.agent.deleteMany();
|
|
112
|
-
service = new AgentCronJobService(
|
|
109
|
+
service = new AgentCronJobService(
|
|
110
|
+
prisma,
|
|
111
|
+
undefined,
|
|
112
|
+
fixtureResolver,
|
|
113
|
+
fixtureDefaults,
|
|
114
|
+
);
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
afterEach(async () => {
|
package/src/agent-cron-jobs.ts
CHANGED
|
@@ -15,17 +15,9 @@ import type {
|
|
|
15
15
|
SpecialtyDefaultsResolver,
|
|
16
16
|
} from "./specialty-defaults.ts";
|
|
17
17
|
import {
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
} from "./specialty-
|
|
21
|
-
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The shape of a single cron template resolved from a specialty's manifest.
|
|
25
|
-
* Structurally identical to the former SYSTEM_CRONS SystemCron entries — the
|
|
26
|
-
* manifest is now the source of truth (see specialty-manifest-loader.ts).
|
|
27
|
-
*/
|
|
28
|
-
type ManifestCron = SpecialtyManifest["crons"][number];
|
|
18
|
+
type SpecialtyTypeResolver,
|
|
19
|
+
SpecialtyTypes,
|
|
20
|
+
} from "./specialty-types.ts";
|
|
29
21
|
|
|
30
22
|
export type { AgentCronJob };
|
|
31
23
|
|
|
@@ -87,7 +79,7 @@ export class AgentCronJobService {
|
|
|
87
79
|
constructor(
|
|
88
80
|
private prisma: PrismaClient,
|
|
89
81
|
private clock: Clock = SystemClock(),
|
|
90
|
-
private specialtyRegistry:
|
|
82
|
+
private specialtyRegistry: SpecialtyTypeResolver = new SpecialtyTypes(),
|
|
91
83
|
private specialtyDefaults: SpecialtyDefaultsResolver = () => undefined,
|
|
92
84
|
) {}
|
|
93
85
|
|
|
@@ -442,26 +434,25 @@ export class AgentCronJobService {
|
|
|
442
434
|
/**
|
|
443
435
|
* Reconcile system crons for the given agent.
|
|
444
436
|
*
|
|
445
|
-
* The cron list
|
|
446
|
-
* stored `specialty
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* (no seeding of another type's crons, no deletion of
|
|
451
|
-
* crons).
|
|
452
|
-
* SYSTEM_CRONS array as the source of truth (ATS-3.2).
|
|
437
|
+
* The cron list comes from the extension's specialtyDefaults() for the
|
|
438
|
+
* agent's stored `specialty`. Without extension crons the built-in types
|
|
439
|
+
* (looked up via the injected resolver's `tryGetType()`, deliberately
|
|
440
|
+
* WITHOUT a fallback) declare none, so their system crons are cleared; a
|
|
441
|
+
* specialty that is neither registered nor built in logs an error and
|
|
442
|
+
* reconciles nothing (no seeding of another type's crons, no deletion of
|
|
443
|
+
* existing system crons).
|
|
453
444
|
*
|
|
454
|
-
* Two passes so parent/child links resolve regardless of the
|
|
445
|
+
* Two passes so parent/child links resolve regardless of the default
|
|
455
446
|
* cron order or whether this is the agent's very first reconcile (i.e.
|
|
456
447
|
* the parent row may not exist yet when a child entry is processed):
|
|
457
448
|
*
|
|
458
|
-
* Pass 1 — for each
|
|
449
|
+
* Pass 1 — for each default cron entry:
|
|
459
450
|
* - If an existing system cron with matching name exists: update it in
|
|
460
|
-
* place with the current
|
|
451
|
+
* place with the current default definition, preserving its id and
|
|
461
452
|
* existing enabled state. Updating in place (rather than delete+create)
|
|
462
453
|
* keeps the row's id stable across restarts so AgentCronRun history
|
|
463
454
|
* (FK onDelete: Cascade) is never wiped out from under it.
|
|
464
|
-
* - If no matching cron exists: create it with the
|
|
455
|
+
* - If no matching cron exists: create it with the default
|
|
465
456
|
* enabled state.
|
|
466
457
|
* Records each entry's resulting row id in a name → id map as it goes.
|
|
467
458
|
*
|
|
@@ -472,10 +463,10 @@ export class AgentCronJobService {
|
|
|
472
463
|
* non-null parentCronId on that row is cleared back to null. Self-heals
|
|
473
464
|
* the link on every reconcile call in both directions — null→set (e.g.
|
|
474
465
|
* a pre-existing row from before parentCron was introduced) and set→null
|
|
475
|
-
* (e.g. a parentCron declaration removed from the
|
|
466
|
+
* (e.g. a parentCron declaration removed from the defaults).
|
|
476
467
|
*
|
|
477
468
|
* Orphan pass: delete any cron with system=true whose name is no longer in
|
|
478
|
-
* the resolved
|
|
469
|
+
* the resolved default cron list.
|
|
479
470
|
*
|
|
480
471
|
* All three passes run inside a single transaction for atomicity.
|
|
481
472
|
*
|
|
@@ -484,7 +475,7 @@ export class AgentCronJobService {
|
|
|
484
475
|
async reconcileSystemCrons(
|
|
485
476
|
agentId: string,
|
|
486
477
|
): Promise<{ created: number; updated: number; deleted: number }> {
|
|
487
|
-
// Resolve the agent's
|
|
478
|
+
// Resolve the agent's default cron list. No default-type
|
|
488
479
|
// fallback: seeding another type's crons on an agent is worse than
|
|
489
480
|
// leaving its crons untouched.
|
|
490
481
|
const agent = await this.prisma.agent.findUnique({
|
|
@@ -495,19 +486,18 @@ export class AgentCronJobService {
|
|
|
495
486
|
throw new NotFoundError(`agent ${agentId} not found`);
|
|
496
487
|
}
|
|
497
488
|
const { specialty } = agent;
|
|
498
|
-
// Extension-supplied crons win;
|
|
489
|
+
// Extension-supplied crons win; built-in types declare none.
|
|
499
490
|
const extensionCrons = this.specialtyDefaults(specialty)?.crons;
|
|
500
|
-
const
|
|
491
|
+
const knownType = extensionCrons
|
|
501
492
|
? undefined
|
|
502
|
-
: this.specialtyRegistry.
|
|
503
|
-
if (!extensionCrons && !
|
|
493
|
+
: this.specialtyRegistry.tryGetType(specialty);
|
|
494
|
+
if (!extensionCrons && !knownType) {
|
|
504
495
|
console.error(
|
|
505
|
-
`reconcileSystemCrons: no
|
|
496
|
+
`reconcileSystemCrons: no known type for specialty "${specialty}" (agent ${agentId}) — leaving crons untouched`,
|
|
506
497
|
);
|
|
507
498
|
return { created: 0, updated: 0, deleted: 0 };
|
|
508
499
|
}
|
|
509
|
-
const
|
|
510
|
-
extensionCrons ?? manifest?.crons ?? [];
|
|
500
|
+
const defaultCrons: readonly SpecialtyDefaultCron[] = extensionCrons ?? [];
|
|
511
501
|
|
|
512
502
|
// Fetch all existing system crons for this agent
|
|
513
503
|
const existingSystemCrons = await this.prisma.agentCronJob.findMany({
|
|
@@ -522,8 +512,8 @@ export class AgentCronJobService {
|
|
|
522
512
|
}
|
|
523
513
|
}
|
|
524
514
|
|
|
525
|
-
// Build set of valid
|
|
526
|
-
const systemCronNames = new Set(
|
|
515
|
+
// Build set of valid default cron names for orphan detection
|
|
516
|
+
const systemCronNames = new Set(defaultCrons.map((c) => c.name));
|
|
527
517
|
|
|
528
518
|
let created = 0;
|
|
529
519
|
let updated = 0;
|
|
@@ -532,9 +522,9 @@ export class AgentCronJobService {
|
|
|
532
522
|
// Wrap all mutations in a transaction so a crash mid-loop cannot leave
|
|
533
523
|
// the agent missing required system crons (e.g. shipwright-dev-task).
|
|
534
524
|
await this.prisma.$transaction(async (tx) => {
|
|
535
|
-
// Pass 1: reconcile each
|
|
525
|
+
// Pass 1: reconcile each default cron entry, recording name → row id.
|
|
536
526
|
const idByName = new Map<string, string>();
|
|
537
|
-
for (const systemCron of
|
|
527
|
+
for (const systemCron of defaultCrons) {
|
|
538
528
|
const existing = existingByName.get(systemCron.name);
|
|
539
529
|
|
|
540
530
|
if (existing) {
|
|
@@ -577,7 +567,7 @@ export class AgentCronJobService {
|
|
|
577
567
|
// Also clears parentCronId back to null when an entry no longer
|
|
578
568
|
// declares a resolvable parentCron, so the link self-heals in both
|
|
579
569
|
// directions rather than only null→set.
|
|
580
|
-
for (const systemCron of
|
|
570
|
+
for (const systemCron of defaultCrons) {
|
|
581
571
|
const childId = idByName.get(systemCron.name);
|
|
582
572
|
if (!childId) continue;
|
|
583
573
|
const parentId = systemCron.parentCron
|
|
@@ -600,7 +590,7 @@ export class AgentCronJobService {
|
|
|
600
590
|
}
|
|
601
591
|
}
|
|
602
592
|
|
|
603
|
-
// Orphan pass: delete any system cron whose name is not in the
|
|
593
|
+
// Orphan pass: delete any system cron whose name is not in the defaults.
|
|
604
594
|
for (const cron of existingSystemCrons) {
|
|
605
595
|
if (!cron.name || !systemCronNames.has(cron.name)) {
|
|
606
596
|
await tx.agentCronJob.delete({ where: { id: cron.id } });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Unit tests for agent-cron-jobs.ts:
|
|
3
3
|
* - the pure cron-expression validator (isValidCron)
|
|
4
4
|
* - reconcileSystemCrons()'s create/update/orphan/parent-link logic, driven
|
|
5
|
-
* by an injected
|
|
5
|
+
* by an injected SpecialtyTypeResolver double and an in-memory prisma
|
|
6
6
|
* test double (no real DB, no filesystem).
|
|
7
7
|
*
|
|
8
8
|
* Pure logic against injected doubles — see docs/testing.md for the unit-layer
|
|
@@ -11,11 +11,16 @@
|
|
|
11
11
|
import { describe, expect, test } from "bun:test";
|
|
12
12
|
import type { AgentCronJob, PrismaClient } from "../prisma/client/client.ts";
|
|
13
13
|
import { AgentCronJobService, isValidCron } from "./agent-cron-jobs.ts";
|
|
14
|
+
import type {
|
|
15
|
+
SpecialtyDefaultCron,
|
|
16
|
+
SpecialtyDefaultsResolver,
|
|
17
|
+
} from "./specialty-defaults.ts";
|
|
14
18
|
import {
|
|
15
19
|
resolveDefaultSpecialtyName,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
SPECIALIST_TYPE,
|
|
21
|
+
type SpecialtyType,
|
|
22
|
+
type SpecialtyTypeResolver,
|
|
23
|
+
} from "./specialty-types.ts";
|
|
19
24
|
|
|
20
25
|
describe("isValidCron", () => {
|
|
21
26
|
test("accepts standard 5-field schedules", () => {
|
|
@@ -54,31 +59,31 @@ describe("isValidCron", () => {
|
|
|
54
59
|
|
|
55
60
|
// ─── reconcileSystemCrons() with injected registry + fake prisma ────────────
|
|
56
61
|
|
|
57
|
-
type
|
|
62
|
+
type DefaultCron = SpecialtyDefaultCron;
|
|
58
63
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/** A registry double that always returns a fixed manifest and records lookups. */
|
|
64
|
+
/**
|
|
65
|
+
* A resolver double knowing the types in `byType`, plus the matching
|
|
66
|
+
* specialtyDefaults() hook supplying their crons. Records registry lookups.
|
|
67
|
+
*/
|
|
65
68
|
function fakeRegistry(
|
|
66
|
-
byType: Record<string,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
byType: Record<string, DefaultCron[]>,
|
|
70
|
+
): SpecialtyTypeResolver & {
|
|
71
|
+
lookups: string[];
|
|
72
|
+
defaults: SpecialtyDefaultsResolver;
|
|
73
|
+
} {
|
|
69
74
|
const lookups: string[] = [];
|
|
70
75
|
return {
|
|
71
76
|
lookups,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!crons) throw new Error(`no manifest for ${specialty}`);
|
|
76
|
-
return manifestWithCrons(crons);
|
|
77
|
+
defaults: (specialty) => {
|
|
78
|
+
const crons = byType[specialty];
|
|
79
|
+
return crons ? { crons } : undefined;
|
|
77
80
|
},
|
|
78
|
-
|
|
81
|
+
getType(): SpecialtyType {
|
|
82
|
+
return SPECIALIST_TYPE;
|
|
83
|
+
},
|
|
84
|
+
tryGetType(specialty: string): SpecialtyType | undefined {
|
|
79
85
|
lookups.push(specialty);
|
|
80
|
-
|
|
81
|
-
return crons ? manifestWithCrons(crons) : undefined;
|
|
86
|
+
return specialty in byType ? SPECIALIST_TYPE : undefined;
|
|
82
87
|
},
|
|
83
88
|
listTypes() {
|
|
84
89
|
return Object.keys(byType).map((name) => ({ name, displayName: name }));
|
|
@@ -211,7 +216,7 @@ function makeFakePrisma(opts: {
|
|
|
211
216
|
return { prisma, rows };
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
const loopCron:
|
|
219
|
+
const loopCron: DefaultCron = {
|
|
215
220
|
name: "shipwright-loop",
|
|
216
221
|
schedule: "* * * * *",
|
|
217
222
|
prompt: "internal",
|
|
@@ -219,7 +224,7 @@ const loopCron: ManifestCron = {
|
|
|
219
224
|
enabled: false,
|
|
220
225
|
};
|
|
221
226
|
|
|
222
|
-
const childCron:
|
|
227
|
+
const childCron: DefaultCron = {
|
|
223
228
|
name: "shipwright-dev-task",
|
|
224
229
|
schedule: "* * * * *",
|
|
225
230
|
prompt: "/shipwright:dev-task",
|
|
@@ -229,18 +234,22 @@ const childCron: ManifestCron = {
|
|
|
229
234
|
};
|
|
230
235
|
|
|
231
236
|
describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
232
|
-
test("
|
|
237
|
+
test("uses the extension defaults for the agent's specialty without consulting the registry", async () => {
|
|
233
238
|
const registry = fakeRegistry({ coding: [loopCron] });
|
|
234
|
-
const { prisma } = makeFakePrisma({ specialty: "coding" });
|
|
239
|
+
const { prisma, rows } = makeFakePrisma({ specialty: "coding" });
|
|
235
240
|
const service = new AgentCronJobService(
|
|
236
241
|
prisma as unknown as PrismaClient,
|
|
237
242
|
undefined,
|
|
238
243
|
registry,
|
|
244
|
+
registry.defaults,
|
|
239
245
|
);
|
|
240
246
|
|
|
241
247
|
await service.reconcileSystemCrons("agent-1");
|
|
242
248
|
|
|
243
|
-
expect(registry.lookups).toEqual([
|
|
249
|
+
expect(registry.lookups).toEqual([]);
|
|
250
|
+
expect(Array.from(rows.values()).map((r) => r.name)).toEqual([
|
|
251
|
+
"shipwright-loop",
|
|
252
|
+
]);
|
|
244
253
|
});
|
|
245
254
|
|
|
246
255
|
test("creates one row per manifest cron on a fresh agent", async () => {
|
|
@@ -250,6 +259,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
250
259
|
prisma as unknown as PrismaClient,
|
|
251
260
|
undefined,
|
|
252
261
|
registry,
|
|
262
|
+
registry.defaults,
|
|
253
263
|
);
|
|
254
264
|
|
|
255
265
|
const result = await service.reconcileSystemCrons("agent-1");
|
|
@@ -270,6 +280,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
270
280
|
prisma as unknown as PrismaClient,
|
|
271
281
|
undefined,
|
|
272
282
|
registry,
|
|
283
|
+
registry.defaults,
|
|
273
284
|
);
|
|
274
285
|
|
|
275
286
|
await service.reconcileSystemCrons("agent-1");
|
|
@@ -291,6 +302,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
291
302
|
prisma as unknown as PrismaClient,
|
|
292
303
|
undefined,
|
|
293
304
|
registry,
|
|
305
|
+
registry.defaults,
|
|
294
306
|
);
|
|
295
307
|
|
|
296
308
|
const result = await service.reconcileSystemCrons("agent-1");
|
|
@@ -310,6 +322,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
310
322
|
prisma as unknown as PrismaClient,
|
|
311
323
|
undefined,
|
|
312
324
|
registry,
|
|
325
|
+
registry.defaults,
|
|
313
326
|
);
|
|
314
327
|
|
|
315
328
|
const result = await service.reconcileSystemCrons("agent-1");
|
|
@@ -334,6 +347,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
334
347
|
prisma as unknown as PrismaClient,
|
|
335
348
|
undefined,
|
|
336
349
|
registry,
|
|
350
|
+
registry.defaults,
|
|
337
351
|
);
|
|
338
352
|
|
|
339
353
|
await service.reconcileSystemCrons("agent-1");
|
|
@@ -354,6 +368,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
354
368
|
prisma as unknown as PrismaClient,
|
|
355
369
|
undefined,
|
|
356
370
|
registry,
|
|
371
|
+
registry.defaults,
|
|
357
372
|
);
|
|
358
373
|
const errors: unknown[][] = [];
|
|
359
374
|
const original = console.error;
|
|
@@ -384,6 +399,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
384
399
|
prisma as unknown as PrismaClient,
|
|
385
400
|
undefined,
|
|
386
401
|
registry,
|
|
402
|
+
registry.defaults,
|
|
387
403
|
);
|
|
388
404
|
const original = console.error;
|
|
389
405
|
console.error = () => {};
|
|
@@ -399,19 +415,19 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
|
399
415
|
expect(Array.from(rows.keys())).toEqual(["sys-1"]);
|
|
400
416
|
});
|
|
401
417
|
|
|
402
|
-
test("a
|
|
418
|
+
test("extension crons for a non-coding type reconcile normally", async () => {
|
|
403
419
|
const registry = fakeRegistry({ specialist: [loopCron] });
|
|
404
420
|
const { prisma, rows } = makeFakePrisma({ specialty: "specialist" });
|
|
405
421
|
const service = new AgentCronJobService(
|
|
406
422
|
prisma as unknown as PrismaClient,
|
|
407
423
|
undefined,
|
|
408
424
|
registry,
|
|
425
|
+
registry.defaults,
|
|
409
426
|
);
|
|
410
427
|
|
|
411
428
|
const result = await service.reconcileSystemCrons("agent-1");
|
|
412
429
|
|
|
413
430
|
expect(result.created).toBe(1);
|
|
414
|
-
expect(registry.lookups).toEqual(["specialist"]);
|
|
415
431
|
expect(Array.from(rows.values())[0]?.name).toBe("shipwright-loop");
|
|
416
432
|
});
|
|
417
433
|
});
|
|
@@ -444,8 +460,8 @@ describe("reconcileSystemCrons (specialtyDefaults hook)", () => {
|
|
|
444
460
|
expect(row?.enabled).toBe(false);
|
|
445
461
|
});
|
|
446
462
|
|
|
447
|
-
test("hook returning nothing falls back to the
|
|
448
|
-
const registry = fakeRegistry({ coding: [
|
|
463
|
+
test("hook returning nothing falls back to the built-in type, which declares no crons", async () => {
|
|
464
|
+
const registry = fakeRegistry({ coding: [] });
|
|
449
465
|
const { prisma, rows } = makeFakePrisma({ specialty: "coding" });
|
|
450
466
|
const service = new AgentCronJobService(
|
|
451
467
|
prisma as unknown as PrismaClient,
|
|
@@ -456,13 +472,11 @@ describe("reconcileSystemCrons (specialtyDefaults hook)", () => {
|
|
|
456
472
|
|
|
457
473
|
await service.reconcileSystemCrons("agent-1");
|
|
458
474
|
|
|
459
|
-
expect(Array.from(rows.values())
|
|
460
|
-
"shipwright-loop",
|
|
461
|
-
]);
|
|
475
|
+
expect(Array.from(rows.values())).toEqual([]);
|
|
462
476
|
});
|
|
463
477
|
|
|
464
|
-
test("defaults without crons fall back to the
|
|
465
|
-
const registry = fakeRegistry({ coding: [
|
|
478
|
+
test("defaults without crons fall back to the built-in type, which declares no crons", async () => {
|
|
479
|
+
const registry = fakeRegistry({ coding: [] });
|
|
466
480
|
const { prisma, rows } = makeFakePrisma({ specialty: "coding" });
|
|
467
481
|
const service = new AgentCronJobService(
|
|
468
482
|
prisma as unknown as PrismaClient,
|
|
@@ -473,9 +487,7 @@ describe("reconcileSystemCrons (specialtyDefaults hook)", () => {
|
|
|
473
487
|
|
|
474
488
|
await service.reconcileSystemCrons("agent-1");
|
|
475
489
|
|
|
476
|
-
expect(Array.from(rows.values())
|
|
477
|
-
"shipwright-loop",
|
|
478
|
-
]);
|
|
490
|
+
expect(Array.from(rows.values())).toEqual([]);
|
|
479
491
|
});
|
|
480
492
|
|
|
481
493
|
test("reconciles hook crons without any manifest for the specialty", async () => {
|
|
@@ -432,10 +432,10 @@ function makeMockDeps(): AdminDeps {
|
|
|
432
432
|
listByAgentId: async () => [],
|
|
433
433
|
},
|
|
434
434
|
specialtyRegistry: {
|
|
435
|
-
|
|
435
|
+
getType: () => {
|
|
436
436
|
throw new Error("not implemented");
|
|
437
437
|
},
|
|
438
|
-
|
|
438
|
+
tryGetType: () => undefined,
|
|
439
439
|
listTypes: () => [],
|
|
440
440
|
},
|
|
441
441
|
agentChatTokenService: {
|
package/src/agent-manifest.ts
CHANGED
|
@@ -70,13 +70,9 @@ const DEFAULT_TENANT_CPU_LIMIT = "2";
|
|
|
70
70
|
* from `buildAgentDeploymentManifest` with `opts.resources` — omitted or
|
|
71
71
|
* partial overrides fall back field-by-field to the default below.
|
|
72
72
|
*
|
|
73
|
-
* Precedence (highest wins):
|
|
73
|
+
* Precedence (highest wins): env override > default.
|
|
74
74
|
* `overrides` here is the ENV layer (populated in admin/src/main.ts's
|
|
75
|
-
* buildProvisioner from SHIPWRIGHT_K8S_AGENT_* vars).
|
|
76
|
-
* SpecialtyResourcesSchema in specialty-registry.ts is already parsed as
|
|
77
|
-
* part of SpecialtyManifestSchema.resources but is NOT consumed here or by
|
|
78
|
-
* any provisioner — it stays inert until a future task wires specialty-level
|
|
79
|
-
* resource resolution in ahead of this function's env layer.
|
|
75
|
+
* buildProvisioner from SHIPWRIGHT_K8S_AGENT_* vars).
|
|
80
76
|
*
|
|
81
77
|
* Defaults: requests mirror the GKE Autopilot defaults the agent ran with
|
|
82
78
|
* before these were explicit. The memory limit exists to contain a runaway
|
|
@@ -27,13 +27,15 @@ import { AgentPluginService } from "./agent-plugins.ts";
|
|
|
27
27
|
import { NoopAgentProvisioner } from "./agent-provisioner.ts";
|
|
28
28
|
import { AgentTokenService } from "./agent-tokens.ts";
|
|
29
29
|
import { AgentToolService } from "./agent-tools.ts";
|
|
30
|
-
import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
|
|
31
|
-
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
32
30
|
import { AgentService } from "./agents.ts";
|
|
33
31
|
import type { AdminDeps } from "./agents-api.ts";
|
|
34
32
|
import { createAdminApp } from "./agents-api.ts";
|
|
35
33
|
import { NoopChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
36
34
|
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
35
|
+
import type {
|
|
36
|
+
SpecialtyType,
|
|
37
|
+
SpecialtyTypeResolver,
|
|
38
|
+
} from "./specialty-types.ts";
|
|
37
39
|
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
38
40
|
|
|
39
41
|
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
@@ -63,40 +65,27 @@ function makePrisma(): PrismaClient {
|
|
|
63
65
|
return createAdminPrismaClient(TEST_DB as string);
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
name: "coding",
|
|
71
|
-
displayName: "Coding Agent",
|
|
72
|
-
description: "test manifest",
|
|
73
|
-
version: "1.0.0",
|
|
74
|
-
skills: [],
|
|
75
|
-
},
|
|
76
|
-
identity: { templatesDir: "workspace/" },
|
|
77
|
-
crons: [],
|
|
78
|
-
plugins: ["shipwright"],
|
|
68
|
+
const CODING_TYPE: SpecialtyType = {
|
|
69
|
+
name: "coding",
|
|
70
|
+
displayName: "Coding Agent",
|
|
71
|
+
templatesDir: "workspace/",
|
|
79
72
|
tools: ["Read", "Write", "Edit"],
|
|
80
|
-
|
|
81
|
-
members: [],
|
|
82
|
-
repos: [],
|
|
83
|
-
chat: true,
|
|
84
|
-
voice: true,
|
|
73
|
+
requiredEnv: [],
|
|
85
74
|
};
|
|
86
75
|
|
|
87
|
-
function
|
|
88
|
-
const byType: Record<string,
|
|
76
|
+
function fakeSpecialtyTypes(): SpecialtyTypeResolver {
|
|
77
|
+
const byType: Record<string, SpecialtyType> = { coding: CODING_TYPE };
|
|
89
78
|
return {
|
|
90
|
-
|
|
91
|
-
return byType[specialty] ?? (byType.coding as
|
|
79
|
+
getType(specialty: string): SpecialtyType {
|
|
80
|
+
return byType[specialty] ?? (byType.coding as SpecialtyType);
|
|
92
81
|
},
|
|
93
|
-
|
|
82
|
+
tryGetType(specialty: string): SpecialtyType | undefined {
|
|
94
83
|
return byType[specialty];
|
|
95
84
|
},
|
|
96
85
|
listTypes() {
|
|
97
86
|
return Object.entries(byType).map(([name, manifest]) => ({
|
|
98
87
|
name,
|
|
99
|
-
displayName: manifest.
|
|
88
|
+
displayName: manifest.displayName,
|
|
100
89
|
}));
|
|
101
90
|
},
|
|
102
91
|
};
|
|
@@ -122,7 +111,7 @@ function makeDeps(prisma: PrismaClient): AdminDeps {
|
|
|
122
111
|
agentTokenService: new AgentTokenService(prisma),
|
|
123
112
|
agentPluginService: new AgentPluginService(prisma),
|
|
124
113
|
agentMemberService: new AgentMemberService(prisma),
|
|
125
|
-
specialtyRegistry:
|
|
114
|
+
specialtyRegistry: fakeSpecialtyTypes(),
|
|
126
115
|
agentChatTokenService: new AgentChatTokenService(prisma),
|
|
127
116
|
agentGitHubInstallationsService: new AgentGitHubInstallationsService(
|
|
128
117
|
prisma,
|
|
@@ -25,13 +25,15 @@ import type { AgentProvisioner } from "./agent-provisioner.ts";
|
|
|
25
25
|
import { NoopAgentProvisioner } from "./agent-provisioner.ts";
|
|
26
26
|
import { AgentTokenService } from "./agent-tokens.ts";
|
|
27
27
|
import { AgentToolService } from "./agent-tools.ts";
|
|
28
|
-
import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
|
|
29
|
-
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
30
28
|
import { AgentService } from "./agents.ts";
|
|
31
29
|
import type { AdminDeps } from "./agents-api.ts";
|
|
32
30
|
import { createAdminApp } from "./agents-api.ts";
|
|
33
31
|
import { NoopChatServiceProvisioningClient } from "./chat-service-provisioning-client.ts";
|
|
34
32
|
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
33
|
+
import type {
|
|
34
|
+
SpecialtyType,
|
|
35
|
+
SpecialtyTypeResolver,
|
|
36
|
+
} from "./specialty-types.ts";
|
|
35
37
|
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
36
38
|
|
|
37
39
|
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
@@ -70,19 +72,10 @@ async function createAgent(
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/** A minimal valid "coding" manifest used to drive seeding assertions below. */
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
name: "coding",
|
|
78
|
-
displayName: "Coding Agent",
|
|
79
|
-
description: "test manifest",
|
|
80
|
-
version: "1.0.0",
|
|
81
|
-
skills: [],
|
|
82
|
-
},
|
|
83
|
-
identity: { templatesDir: "workspace/" },
|
|
84
|
-
crons: [],
|
|
85
|
-
plugins: ["shipwright"],
|
|
75
|
+
const CODING_TYPE: SpecialtyType = {
|
|
76
|
+
name: "coding",
|
|
77
|
+
displayName: "Coding Agent",
|
|
78
|
+
templatesDir: "workspace/",
|
|
86
79
|
tools: [
|
|
87
80
|
"Read",
|
|
88
81
|
"Write",
|
|
@@ -95,32 +88,28 @@ const CODING_MANIFEST: SpecialtyManifest = {
|
|
|
95
88
|
"Skill",
|
|
96
89
|
"Agent",
|
|
97
90
|
],
|
|
98
|
-
|
|
99
|
-
members: [],
|
|
100
|
-
repos: [],
|
|
101
|
-
chat: true,
|
|
102
|
-
voice: true,
|
|
91
|
+
requiredEnv: [],
|
|
103
92
|
};
|
|
104
93
|
|
|
105
94
|
/**
|
|
106
|
-
* Fake
|
|
107
|
-
* specialty.
|
|
108
|
-
* not in the map, mirroring
|
|
95
|
+
* Fake SpecialtyTypeResolver — resolves a fixed map of manifests by
|
|
96
|
+
* specialty. tryGetType() returns undefined (no fallback) for anything
|
|
97
|
+
* not in the map, mirroring SpecialtyTypes's real contract.
|
|
109
98
|
*/
|
|
110
|
-
function
|
|
111
|
-
byType: Record<string,
|
|
112
|
-
):
|
|
99
|
+
function fakeSpecialtyTypes(
|
|
100
|
+
byType: Record<string, SpecialtyType> = { coding: CODING_TYPE },
|
|
101
|
+
): SpecialtyTypeResolver {
|
|
113
102
|
return {
|
|
114
|
-
|
|
115
|
-
return byType[specialty] ?? (byType.coding as
|
|
103
|
+
getType(specialty: string): SpecialtyType {
|
|
104
|
+
return byType[specialty] ?? (byType.coding as SpecialtyType);
|
|
116
105
|
},
|
|
117
|
-
|
|
106
|
+
tryGetType(specialty: string): SpecialtyType | undefined {
|
|
118
107
|
return byType[specialty];
|
|
119
108
|
},
|
|
120
109
|
listTypes() {
|
|
121
110
|
return Object.entries(byType).map(([name, manifest]) => ({
|
|
122
111
|
name,
|
|
123
|
-
displayName: manifest.
|
|
112
|
+
displayName: manifest.displayName,
|
|
124
113
|
}));
|
|
125
114
|
},
|
|
126
115
|
};
|
|
@@ -129,7 +118,7 @@ function fakeSpecialtyRegistry(
|
|
|
129
118
|
function makeDeps(
|
|
130
119
|
prisma: PrismaClient,
|
|
131
120
|
provisioner: AgentProvisioner = new NoopAgentProvisioner(),
|
|
132
|
-
specialtyRegistry:
|
|
121
|
+
specialtyRegistry: SpecialtyTypeResolver = fakeSpecialtyTypes(),
|
|
133
122
|
): AdminDeps {
|
|
134
123
|
const savedKey = process.env.SHIPWRIGHT_ENCRYPTION_KEY;
|
|
135
124
|
process.env.SHIPWRIGHT_ENCRYPTION_KEY = REAL_KEY;
|