@app-vitals/agent-admin 0.12.0 → 0.13.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.
Files changed (41) hide show
  1. package/e2e/test-server.ts +5 -5
  2. package/openapi.json +12 -19
  3. package/openapi.platform.json +11 -18
  4. package/package.json +2 -2
  5. package/prisma/client/internal/class.ts +4 -4
  6. package/prisma/client/internal/prismaNamespace.ts +0 -1
  7. package/prisma/client/internal/prismaNamespaceBrowser.ts +0 -1
  8. package/prisma/client/models/Agent.ts +62 -137
  9. package/prisma/migrations/20261030000000_drop_agent_type_name/migration.sql +8 -0
  10. package/prisma/schema/schema.prisma +3 -4
  11. package/src/account-lifecycle.integration.test.ts +1 -1
  12. package/src/account-trial-warning-sweeper.integration.test.ts +1 -1
  13. package/src/admin-local-agent.smoke.test.ts +12 -12
  14. package/src/admin-ui-pages.ts +2 -2
  15. package/src/admin-ui-pages.unit.test.ts +2 -2
  16. package/src/admin-ui.fixtures.ts +6 -6
  17. package/src/admin-ui.smoke.test.ts +39 -39
  18. package/src/admin-ui.ts +8 -8
  19. package/src/agent-cron-jobs.integration.test.ts +8 -7
  20. package/src/agent-cron-jobs.ts +4 -5
  21. package/src/agent-cron-jobs.unit.test.ts +23 -23
  22. package/src/agent-patch-author-allowlist.integration.test.ts +4 -4
  23. package/src/agent-specialty-backfill.integration.test.ts +126 -0
  24. package/src/agents-api.integration.test.ts +20 -20
  25. package/src/agents-api.smoke.test.ts +76 -77
  26. package/src/agents-api.ts +9 -14
  27. package/src/agents.integration.test.ts +40 -53
  28. package/src/agents.ts +33 -46
  29. package/src/agents.unit.test.ts +28 -35
  30. package/src/api.smoke.test.ts +24 -2
  31. package/src/api.ts +15 -16
  32. package/src/openapi-schemas.ts +14 -7
  33. package/src/slack-provisioning-service.integration.test.ts +2 -2
  34. package/src/slack-provisioning.integration.test.ts +2 -2
  35. package/src/specialty-manifest-loader.ts +32 -32
  36. package/src/specialty-manifest-loader.unit.test.ts +8 -8
  37. package/src/startup-policy-reconcile.unit.test.ts +1 -1
  38. package/src/trial-expiry-sweeper.integration.test.ts +1 -1
  39. package/src/trial-expiry-warning-sweeper.integration.test.ts +1 -1
  40. package/src/agent-specialty.ts +0 -22
  41. package/src/agent-specialty.unit.test.ts +0 -43
@@ -72,12 +72,13 @@ const fixtureManifest = {
72
72
 
73
73
  /** Resolves only FIXTURE_TYPE; every other type is unknown. */
74
74
  const fixtureResolver: SpecialtyManifestResolver = {
75
- getManifest(typeName: string): SpecialtyManifest {
76
- if (typeName !== FIXTURE_TYPE) throw new Error(`no manifest: ${typeName}`);
75
+ getManifest(specialty: string): SpecialtyManifest {
76
+ if (specialty !== FIXTURE_TYPE)
77
+ throw new Error(`no manifest: ${specialty}`);
77
78
  return fixtureManifest;
78
79
  },
79
- tryGetManifest(typeName: string): SpecialtyManifest | undefined {
80
- return typeName === FIXTURE_TYPE ? fixtureManifest : undefined;
80
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined {
81
+ return specialty === FIXTURE_TYPE ? fixtureManifest : undefined;
81
82
  },
82
83
  listTypes() {
83
84
  return [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }];
@@ -87,9 +88,9 @@ const fixtureResolver: SpecialtyManifestResolver = {
87
88
  async function createAgent(
88
89
  prisma: PrismaClient,
89
90
  name = "Test Agent",
90
- typeName = FIXTURE_TYPE,
91
+ specialty = FIXTURE_TYPE,
91
92
  ): Promise<string> {
92
- const agent = await prisma.agent.create({ data: { name, typeName } });
93
+ const agent = await prisma.agent.create({ data: { name, specialty } });
93
94
  return agent.id;
94
95
  }
95
96
 
@@ -570,7 +571,7 @@ describeOrSkip("AgentCronJobService (integration)", () => {
570
571
  expect(systemJobs.length).toBe(FIXTURE_CRONS.length);
571
572
  });
572
573
 
573
- it("reconcileSystemCrons() for an unknown typeName changes nothing (no fallback)", async () => {
574
+ it("reconcileSystemCrons() for an unknown specialty changes nothing (no fallback)", async () => {
574
575
  const agentId = await createAgent(prisma, "Renamed Agent", "renamed-type");
575
576
 
576
577
  // Must not throw, and must not seed another specialty's crons.
@@ -17,7 +17,6 @@ import {
17
17
  } from "./specialty-manifest-loader.ts";
18
18
  import type { SpecialtyManifest } from "./specialty-registry.ts";
19
19
  import { type Clock, SystemClock } from "./clock.ts";
20
- import { resolveSpecialty } from "./agent-specialty.ts";
21
20
  import { NotFoundError, UnprocessableEntityError } from "./errors.ts";
22
21
 
23
22
  /**
@@ -442,8 +441,8 @@ export class AgentCronJobService {
442
441
  * Reconcile system crons for the given agent.
443
442
  *
444
443
  * The cron list is resolved from the agent's type manifest — the agent's
445
- * stored `typeName` is looked up via the injected SpecialtyRegistry's
446
- * `tryGetManifest()` (the parsed `specialties/{typeName}/manifest.yaml`),
444
+ * stored `specialty` is looked up via the injected SpecialtyRegistry's
445
+ * `tryGetManifest()` (the parsed `specialties/{specialty}/manifest.yaml`),
447
446
  * deliberately WITHOUT the "coding" fallback: a non-default type with no
448
447
  * loadable manifest logs an error and reconciles nothing (no seeding of
449
448
  * coding crons, no deletion of existing system crons), while a "coding"
@@ -489,12 +488,12 @@ export class AgentCronJobService {
489
488
  // than leaving its crons untouched.
490
489
  const agent = await this.prisma.agent.findUnique({
491
490
  where: { id: agentId },
492
- select: { typeName: true, specialty: true },
491
+ select: { specialty: true },
493
492
  });
494
493
  if (!agent) {
495
494
  throw new NotFoundError(`agent ${agentId} not found`);
496
495
  }
497
- const specialty = resolveSpecialty(agent);
496
+ const { specialty } = agent;
498
497
  const manifest = this.specialtyRegistry.tryGetManifest(specialty);
499
498
  if (!manifest) {
500
499
  const defaultName = resolveDefaultSpecialtyName();
@@ -69,15 +69,15 @@ function fakeRegistry(
69
69
  const lookups: string[] = [];
70
70
  return {
71
71
  lookups,
72
- getManifest(typeName: string): SpecialtyManifest {
73
- lookups.push(typeName);
74
- const crons = byType[typeName] ?? byType[fallbackType];
75
- if (!crons) throw new Error(`no manifest for ${typeName}`);
72
+ getManifest(specialty: string): SpecialtyManifest {
73
+ lookups.push(specialty);
74
+ const crons = byType[specialty] ?? byType[fallbackType];
75
+ if (!crons) throw new Error(`no manifest for ${specialty}`);
76
76
  return manifestWithCrons(crons);
77
77
  },
78
- tryGetManifest(typeName: string): SpecialtyManifest | undefined {
79
- lookups.push(typeName);
80
- const crons = byType[typeName];
78
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined {
79
+ lookups.push(specialty);
80
+ const crons = byType[specialty];
81
81
  return crons ? manifestWithCrons(crons) : undefined;
82
82
  },
83
83
  listTypes() {
@@ -90,12 +90,12 @@ type FakeCronRow = AgentCronJob;
90
90
 
91
91
  /**
92
92
  * In-memory prisma double covering only what reconcileSystemCrons() touches:
93
- * agent.findUnique (typeName resolution),
93
+ * agent.findUnique (specialty resolution),
94
94
  * agentCronJob.findMany, and a $transaction exposing
95
95
  * agentCronJob.{create,update,delete}.
96
96
  */
97
97
  function makeFakePrisma(opts: {
98
- typeName: string;
98
+ specialty: string;
99
99
  existing?: Partial<FakeCronRow>[];
100
100
  }) {
101
101
  let nextId = 1;
@@ -193,8 +193,8 @@ function makeFakePrisma(opts: {
193
193
  where,
194
194
  }: {
195
195
  where: { id: string };
196
- }): Promise<{ typeName: string } | null> {
197
- return where.id === "agent-1" ? { typeName: opts.typeName } : null;
196
+ }): Promise<{ specialty: string } | null> {
197
+ return where.id === "agent-1" ? { specialty: opts.specialty } : null;
198
198
  },
199
199
  };
200
200
 
@@ -229,9 +229,9 @@ const childCron: ManifestCron = {
229
229
  };
230
230
 
231
231
  describe("reconcileSystemCrons (unit, injected registry)", () => {
232
- test("resolves the agent's typeName via the injected registry", async () => {
232
+ test("resolves the agent's specialty via the injected registry", async () => {
233
233
  const registry = fakeRegistry({ coding: [loopCron] });
234
- const { prisma } = makeFakePrisma({ typeName: "coding" });
234
+ const { prisma } = makeFakePrisma({ specialty: "coding" });
235
235
  const service = new AgentCronJobService(
236
236
  prisma as unknown as PrismaClient,
237
237
  undefined,
@@ -245,7 +245,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
245
245
 
246
246
  test("creates one row per manifest cron on a fresh agent", async () => {
247
247
  const registry = fakeRegistry({ coding: [loopCron, childCron] });
248
- const { prisma, rows } = makeFakePrisma({ typeName: "coding" });
248
+ const { prisma, rows } = makeFakePrisma({ specialty: "coding" });
249
249
  const service = new AgentCronJobService(
250
250
  prisma as unknown as PrismaClient,
251
251
  undefined,
@@ -265,7 +265,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
265
265
 
266
266
  test("links a child cron to its parent on first reconcile", async () => {
267
267
  const registry = fakeRegistry({ coding: [childCron, loopCron] });
268
- const { prisma, rows } = makeFakePrisma({ typeName: "coding" });
268
+ const { prisma, rows } = makeFakePrisma({ specialty: "coding" });
269
269
  const service = new AgentCronJobService(
270
270
  prisma as unknown as PrismaClient,
271
271
  undefined,
@@ -284,7 +284,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
284
284
  test("updates a matching existing row in place, preserving its id", async () => {
285
285
  const registry = fakeRegistry({ coding: [loopCron] });
286
286
  const { prisma, rows } = makeFakePrisma({
287
- typeName: "coding",
287
+ specialty: "coding",
288
288
  existing: [{ id: "stable-id", name: "shipwright-loop", enabled: false }],
289
289
  });
290
290
  const service = new AgentCronJobService(
@@ -303,7 +303,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
303
303
  test("orphan pass deletes a system cron absent from the manifest", async () => {
304
304
  const registry = fakeRegistry({ coding: [loopCron] });
305
305
  const { prisma, rows } = makeFakePrisma({
306
- typeName: "coding",
306
+ specialty: "coding",
307
307
  existing: [{ id: "orphan", name: "removed-cron" }],
308
308
  });
309
309
  const service = new AgentCronJobService(
@@ -321,7 +321,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
321
321
  test("clears a stale parentCronId when the entry no longer declares a resolvable parent", async () => {
322
322
  const registry = fakeRegistry({ coding: [loopCron] });
323
323
  const { prisma, rows } = makeFakePrisma({
324
- typeName: "coding",
324
+ specialty: "coding",
325
325
  existing: [
326
326
  {
327
327
  id: "loop-id",
@@ -341,10 +341,10 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
341
341
  expect(rows.get("loop-id")?.parentCronId).toBeNull();
342
342
  });
343
343
 
344
- test("an unknown non-coding typeName logs an error and changes nothing", async () => {
344
+ test("an unknown non-coding specialty logs an error and changes nothing", async () => {
345
345
  const registry = fakeRegistry({ coding: [loopCron] });
346
346
  const { prisma, rows } = makeFakePrisma({
347
- typeName: "specialist",
347
+ specialty: "specialist",
348
348
  existing: [
349
349
  { id: "sys-1", name: "old-system", system: true },
350
350
  { id: "user-1", name: "mine", system: false },
@@ -377,7 +377,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
377
377
  test("the default manifest that cannot load still throws", async () => {
378
378
  const registry = fakeRegistry({});
379
379
  const { prisma } = makeFakePrisma({
380
- typeName: resolveDefaultSpecialtyName(),
380
+ specialty: resolveDefaultSpecialtyName(),
381
381
  });
382
382
  const service = new AgentCronJobService(
383
383
  prisma as unknown as PrismaClient,
@@ -393,7 +393,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
393
393
  test("the default-manifest failure names the env vars that configure it", async () => {
394
394
  const registry = fakeRegistry({});
395
395
  const { prisma } = makeFakePrisma({
396
- typeName: resolveDefaultSpecialtyName(),
396
+ specialty: resolveDefaultSpecialtyName(),
397
397
  });
398
398
  const service = new AgentCronJobService(
399
399
  prisma as unknown as PrismaClient,
@@ -408,7 +408,7 @@ describe("reconcileSystemCrons (unit, injected registry)", () => {
408
408
 
409
409
  test("a valid non-coding manifest reconciles normally", async () => {
410
410
  const registry = fakeRegistry({ specialist: [loopCron] });
411
- const { prisma, rows } = makeFakePrisma({ typeName: "specialist" });
411
+ const { prisma, rows } = makeFakePrisma({ specialty: "specialist" });
412
412
  const service = new AgentCronJobService(
413
413
  prisma as unknown as PrismaClient,
414
414
  undefined,
@@ -87,11 +87,11 @@ const CODING_MANIFEST: SpecialtyManifest = {
87
87
  function fakeSpecialtyRegistry(): SpecialtyManifestResolver {
88
88
  const byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST };
89
89
  return {
90
- getManifest(typeName: string): SpecialtyManifest {
91
- return byType[typeName] ?? (byType.coding as SpecialtyManifest);
90
+ getManifest(specialty: string): SpecialtyManifest {
91
+ return byType[specialty] ?? (byType.coding as SpecialtyManifest);
92
92
  },
93
- tryGetManifest(typeName: string): SpecialtyManifest | undefined {
94
- return byType[typeName];
93
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined {
94
+ return byType[specialty];
95
95
  },
96
96
  listTypes() {
97
97
  return Object.entries(byType).map(([name, manifest]) => ({
@@ -0,0 +1,126 @@
1
+ /**
2
+ * admin/src/agent-specialty-backfill.integration.test.ts
3
+ * Migration-ordering test for the typeName -> specialty rename
4
+ * (AGR-6.1 ADD, AGR-6.3 REMOVE).
5
+ *
6
+ * After AGR-6.3 the live test database no longer has an "Agent"."typeName"
7
+ * column, so this test cannot seed the pre-drop state through Prisma. Instead
8
+ * it replays the real migration files, in order, against a skeleton "Agent"
9
+ * table inside a throwaway Postgres schema:
10
+ *
11
+ * 20260724000000_add_agent_type_name adds typeName NOT NULL DEFAULT 'coding'
12
+ * 20261029000000_add_agent_specialty adds nullable specialty + backfill
13
+ * 20261030000000_drop_agent_type_name backfills stragglers, specialty NOT NULL,
14
+ * drops typeName
15
+ *
16
+ * Seeded from the live distinct values (admin API, 2026-10-27): coding
17
+ * (11 agents) and specialist (1 agent). A row written between the AGR-6.1
18
+ * backfill and the AGR-6.3 migration (specialty still NULL) must be
19
+ * backfilled from typeName by the drop migration.
20
+ *
21
+ * Requires DATABASE_URL_ADMIN_TEST; skips otherwise.
22
+ */
23
+
24
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
25
+ import { readFileSync } from "node:fs";
26
+ import { Client } from "pg";
27
+
28
+ const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
29
+ const describeOrSkip = TEST_DB ? describe : describe.skip;
30
+
31
+ const LIVE_SPECIALTIES = ["coding", "specialist"] as const;
32
+
33
+ function migrationSql(dir: string): string {
34
+ return readFileSync(
35
+ new URL(`../prisma/migrations/${dir}/migration.sql`, import.meta.url),
36
+ "utf8",
37
+ );
38
+ }
39
+
40
+ const ADD_TYPE_NAME = migrationSql("20260724000000_add_agent_type_name");
41
+ const ADD_SPECIALTY = migrationSql("20261029000000_add_agent_specialty");
42
+ const DROP_TYPE_NAME = migrationSql("20261030000000_drop_agent_type_name");
43
+
44
+ describeOrSkip("typeName -> specialty migrations (AGR-6.1 / AGR-6.3)", () => {
45
+ const schema = `agr63_${process.pid}_${Date.now()}`;
46
+ let client: Client;
47
+
48
+ const columns = async (): Promise<string[]> =>
49
+ (
50
+ await client.query<{ column_name: string }>(
51
+ `SELECT column_name FROM information_schema.columns
52
+ WHERE table_schema = $1 AND table_name = 'Agent'`,
53
+ [schema],
54
+ )
55
+ ).rows.map((r) => r.column_name);
56
+
57
+ beforeEach(async () => {
58
+ client = new Client({ connectionString: TEST_DB as string });
59
+ await client.connect();
60
+ await client.query(`CREATE SCHEMA "${schema}"`);
61
+ await client.query(`SET search_path TO "${schema}"`);
62
+ await client.query(
63
+ `CREATE TABLE "Agent" ("id" TEXT PRIMARY KEY, "name" TEXT NOT NULL)`,
64
+ );
65
+ });
66
+
67
+ afterEach(async () => {
68
+ await client.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
69
+ await client.end();
70
+ });
71
+
72
+ it("add_agent_specialty backfills specialty from typeName and leaves it nullable", async () => {
73
+ await client.query(ADD_TYPE_NAME);
74
+ for (const [i, s] of LIVE_SPECIALTIES.entries()) {
75
+ await client.query(
76
+ `INSERT INTO "Agent" ("id","name","typeName") VALUES ($1,$2,$3)`,
77
+ [`a${i}`, `agent-${i}`, s],
78
+ );
79
+ }
80
+ await client.query(ADD_SPECIALTY);
81
+
82
+ const { rows } = await client.query<{ specialty: string | null }>(
83
+ `SELECT "specialty" FROM "Agent" ORDER BY "id"`,
84
+ );
85
+ expect(rows.map((r) => r.specialty)).toEqual([...LIVE_SPECIALTIES]);
86
+ });
87
+
88
+ it("drop_agent_type_name backfills NULL specialty, makes it required and drops typeName", async () => {
89
+ await client.query(ADD_TYPE_NAME);
90
+ await client.query(ADD_SPECIALTY);
91
+ // Rows written after the AGR-6.1 backfill: one with specialty set, one
92
+ // straggler with specialty NULL and a non-default typeName.
93
+ await client.query(
94
+ `INSERT INTO "Agent" ("id","name","typeName","specialty")
95
+ VALUES ('a0','agent-0','coding','coding')`,
96
+ );
97
+ await client.query(
98
+ `INSERT INTO "Agent" ("id","name","typeName") VALUES ('a1','agent-1','specialist')`,
99
+ );
100
+
101
+ await client.query(DROP_TYPE_NAME);
102
+
103
+ expect(await columns()).not.toContain("typeName");
104
+ const { rows } = await client.query<{ id: string; specialty: string }>(
105
+ `SELECT "id","specialty" FROM "Agent" ORDER BY "id"`,
106
+ );
107
+ expect(rows).toEqual([
108
+ { id: "a0", specialty: "coding" },
109
+ { id: "a1", specialty: "specialist" },
110
+ ]);
111
+
112
+ // specialty is now NOT NULL with a 'coding' default.
113
+ await expect(
114
+ client.query(
115
+ `INSERT INTO "Agent" ("id","name","specialty") VALUES ('a2','agent-2',NULL)`,
116
+ ),
117
+ ).rejects.toThrow();
118
+ await client.query(
119
+ `INSERT INTO "Agent" ("id","name") VALUES ('a3','agent-3')`,
120
+ );
121
+ const def = await client.query<{ specialty: string }>(
122
+ `SELECT "specialty" FROM "Agent" WHERE "id" = 'a3'`,
123
+ );
124
+ expect(def.rows[0]?.specialty).toBe("coding");
125
+ });
126
+ });
@@ -104,18 +104,18 @@ const CODING_MANIFEST: SpecialtyManifest = {
104
104
 
105
105
  /**
106
106
  * Fake SpecialtyManifestResolver — resolves a fixed map of manifests by
107
- * typeName. tryGetManifest() returns undefined (no fallback) for anything
107
+ * specialty. tryGetManifest() returns undefined (no fallback) for anything
108
108
  * not in the map, mirroring SpecialtyRegistry's real contract.
109
109
  */
110
110
  function fakeSpecialtyRegistry(
111
111
  byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST },
112
112
  ): SpecialtyManifestResolver {
113
113
  return {
114
- getManifest(typeName: string): SpecialtyManifest {
115
- return byType[typeName] ?? (byType.coding as SpecialtyManifest);
114
+ getManifest(specialty: string): SpecialtyManifest {
115
+ return byType[specialty] ?? (byType.coding as SpecialtyManifest);
116
116
  },
117
- tryGetManifest(typeName: string): SpecialtyManifest | undefined {
118
- return byType[typeName];
117
+ tryGetManifest(specialty: string): SpecialtyManifest | undefined {
118
+ return byType[specialty];
119
119
  },
120
120
  listTypes() {
121
121
  return Object.entries(byType).map(([name, manifest]) => ({
@@ -309,9 +309,9 @@ describeOrSkip("admin CRUD API (integration)", () => {
309
309
  expect(listedToken.id).toBe(dbRecord?.id);
310
310
  });
311
311
 
312
- // ─── typeName field ─────────────────────────────────────────────────────────
312
+ // ─── specialty field ─────────────────────────────────────────────────────────
313
313
 
314
- it("GET /agents includes typeName:'coding' for an agent created before typeName existed", async () => {
314
+ it("GET /agents includes specialty:'coding' for an agent created before specialty existed", async () => {
315
315
  const res = await app.request("/agents", {
316
316
  headers: { Cookie: `admin_session=${cookie}` },
317
317
  });
@@ -319,30 +319,30 @@ describeOrSkip("admin CRUD API (integration)", () => {
319
319
  const body = await res.json();
320
320
  const entry = body.find((a: { id: string }) => a.id === agentId);
321
321
  expect(entry).toBeDefined();
322
- expect(entry.typeName).toBe("coding");
322
+ expect(entry.specialty).toBe("coding");
323
323
  });
324
324
 
325
- it("GET /agents/:id includes typeName:'coding' by default", async () => {
325
+ it("GET /agents/:id includes specialty:'coding' by default", async () => {
326
326
  const res = await app.request(`/agents/${agentId}`, {
327
327
  headers: { Cookie: `admin_session=${cookie}` },
328
328
  });
329
329
  expect(res.status).toBe(200);
330
330
  const body = await res.json();
331
- expect(body.typeName).toBe("coding");
331
+ expect(body.specialty).toBe("coding");
332
332
  });
333
333
 
334
334
  // ─── Migration backfill read-back (pre-existing rows + child rows) ────────────
335
335
  //
336
336
  // Simulates the exact scenario the 20260724000000_add_agent_type_name
337
- // migration must handle: rows that existed *before* the typeName column was
338
- // added (never received an explicit value) must read back typeName="coding"
337
+ // migration must handle: rows that existed *before* the specialty column was
338
+ // added (never received an explicit value) must read back specialty="coding"
339
339
  // via the column DEFAULT, and every child row (cron/plugin/tool/env/member)
340
340
  // must be completely untouched — same count, same updatedAt — since the
341
341
  // migration only ALTERs the Agent table.
342
342
 
343
- it("migration backfill: pre-existing agent rows read back typeName='coding' and child rows (cron/plugin/tool/env/member) are untouched", async () => {
343
+ it("migration backfill: pre-existing agent rows read back specialty='coding' and child rows (cron/plugin/tool/env/member) are untouched", async () => {
344
344
  // Seed a second "pre-existing" agent with one row in every child table,
345
- // simulating data that existed prior to the typeName migration landing.
345
+ // simulating data that existed prior to the specialty migration landing.
346
346
  const preExisting = await prisma.agent.create({
347
347
  data: { name: "Pre-Existing Agent" },
348
348
  });
@@ -379,11 +379,11 @@ describeOrSkip("admin CRUD API (integration)", () => {
379
379
 
380
380
  // Read back via the raw Agent row (bypasses the API layer entirely) —
381
381
  // this is the direct DB-level assertion that the column DEFAULT applies
382
- // to a row that never set typeName explicitly.
382
+ // to a row that never set specialty explicitly.
383
383
  const rawAgent = await prisma.agent.findUniqueOrThrow({
384
384
  where: { id: preExisting.id },
385
385
  });
386
- expect(rawAgent.typeName).toBe("coding");
386
+ expect(rawAgent.specialty).toBe("coding");
387
387
 
388
388
  // Read back via the HTTP API too — end-to-end confirmation.
389
389
  const res = await app.request(`/agents/${preExisting.id}`, {
@@ -391,7 +391,7 @@ describeOrSkip("admin CRUD API (integration)", () => {
391
391
  });
392
392
  expect(res.status).toBe(200);
393
393
  const body = await res.json();
394
- expect(body.typeName).toBe("coding");
394
+ expect(body.specialty).toBe("coding");
395
395
 
396
396
  // Child row counts are unchanged — exactly one of each, still linked to
397
397
  // the same agent.
@@ -411,7 +411,7 @@ describeOrSkip("admin CRUD API (integration)", () => {
411
411
  await prisma.agentMember.count({ where: { agentId: preExisting.id } }),
412
412
  ).toBe(1);
413
413
 
414
- // Child row updatedAt values are unchanged — the typeName column only
414
+ // Child row updatedAt values are unchanged — the specialty column only
415
415
  // touches the Agent table, so nothing here should have been rewritten.
416
416
  const cronAfter = await prisma.agentCronJob.findUniqueOrThrow({
417
417
  where: { id: cron.id },
@@ -438,7 +438,7 @@ describeOrSkip("admin CRUD API (integration)", () => {
438
438
  expect(memberAfter.createdAt.getTime()).toBe(member.createdAt.getTime());
439
439
  });
440
440
 
441
- it("prisma migrate deploy is idempotent — running it twice does not error or duplicate the typeName column", async () => {
441
+ it("prisma migrate deploy is idempotent — running it twice does not error or duplicate the specialty column", async () => {
442
442
  // The migration has already been applied once (this test DB was
443
443
  // provisioned via `prisma migrate deploy` before the suite ran). Re-run
444
444
  // deploy here and confirm the column is still readable with a single,
@@ -455,7 +455,7 @@ describeOrSkip("admin CRUD API (integration)", () => {
455
455
  const rawAgent = await prisma.agent.findUniqueOrThrow({
456
456
  where: { id: agentId },
457
457
  });
458
- expect(rawAgent.typeName).toBe("coding");
458
+ expect(rawAgent.specialty).toBe("coding");
459
459
  });
460
460
 
461
461
  // ─── repos field round-trip ───────────────────────────────────────────────────