refactor: remove legacy agent IDs, simplify migration (-174 lines)

- Remove sysadmin, db-admin, git-admin aliases from DEFAULT_AGENTS (8→5)
- Replace 120-line migrateAgentIds with 20-line cleanupLegacyAgents
  (one-time cleanup, no-op after first run)
- Simplify role CHECK constraint to 5 current roles
- Fix claimTask mock in controlplane tests (Linux agent's race fix)
- CANONICAL_AGENT_MAP kept as runtime safety net

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mevy Assistant 2026-04-18 23:04:41 +00:00
parent 9b88f6ca2c
commit 0f7fbc400c
3 changed files with 33 additions and 209 deletions

View file

@ -38,9 +38,9 @@ afterEach(() => {
});
describe('getDefaultAgents', () => {
it('returns 8 agents (4 legacy + 4 library-style)', () => {
it('returns 5 agents', () => {
const agents = getDefaultAgents('clawdie');
expect(agents).toHaveLength(8);
expect(agents).toHaveLength(5);
});
it('first agent id matches the input name with orchestrator role', () => {
@ -49,15 +49,13 @@ describe('getDefaultAgents', () => {
expect(agents[0].role).toBe('orchestrator');
});
it('contains both legacy and library-style agent ids', () => {
it('contains all canonical agent ids', () => {
const agents = getDefaultAgents('clawdie');
const ids = agents.map((a) => a.id);
expect(ids).toContain('clawdie');
expect(ids).toContain('sysadmin_agent');
expect(ids).toContain('sysadmin');
expect(ids).toContain('db_admin_agent');
expect(ids).toContain('db-admin');
expect(ids).toContain('git_admin_agent');
expect(ids).toContain('git-admin');
expect(ids).toContain('coordinator');
});
@ -67,28 +65,15 @@ describe('getDefaultAgents', () => {
expect(total).toBe(100);
});
it('sysadmin roles have heartbeat_enabled', () => {
it('sysadmin_agent has heartbeat_enabled', () => {
const agents = getDefaultAgents('clawdie');
const sysadminAgents = agents.filter(
(a) => a.id === 'sysadmin_agent' || a.id === 'sysadmin',
);
for (const a of sysadminAgents) {
expect(a.heartbeat_enabled).toBe(true);
}
const sysadmin = agents.find((a) => a.id === 'sysadmin_agent');
expect(sysadmin?.heartbeat_enabled).toBe(true);
});
it('DEFAULT_AGENTS uses AGENT_NAME as orchestrator id', () => {
expect(DEFAULT_AGENTS[0].id).toBe('mevy');
});
it('library-style aliases have zero budget (FK placeholders)', () => {
const agents = getDefaultAgents('clawdie');
const byId = Object.fromEntries(agents.map((a) => [a.id, a]));
expect(byId['db-admin'].budget_allocation_pct).toBe(0);
expect(byId['git-admin'].budget_allocation_pct).toBe(0);
expect(byId['sysadmin'].budget_allocation_pct).toBe(0);
expect(byId['coordinator'].budget_allocation_pct).toBe(0);
});
});
describe('hashPassword', () => {

View file

@ -95,14 +95,6 @@ export function getDefaultAgents(
heartbeat_interval_sec: 86400,
budget_allocation_pct: 10,
},
{
id: 'sysadmin',
role: 'sysadmin',
adapter: 'pi-local',
heartbeat_enabled: true,
heartbeat_interval_sec: 86400,
budget_allocation_pct: 0,
},
{
id: 'db_admin_agent',
role: 'db_admin_agent',
@ -111,14 +103,6 @@ export function getDefaultAgents(
heartbeat_interval_sec: null,
budget_allocation_pct: 5,
},
{
id: 'db-admin',
role: 'db-admin',
adapter: 'pi-local',
heartbeat_enabled: false,
heartbeat_interval_sec: null,
budget_allocation_pct: 0,
},
{
id: 'git_admin_agent',
role: 'git_admin_agent',
@ -127,14 +111,6 @@ export function getDefaultAgents(
heartbeat_interval_sec: null,
budget_allocation_pct: 5,
},
{
id: 'git-admin',
role: 'git-admin',
adapter: 'pi-local',
heartbeat_enabled: false,
heartbeat_interval_sec: null,
budget_allocation_pct: 0,
},
{
id: 'coordinator',
role: 'coordinator',
@ -153,7 +129,7 @@ export const DEFAULT_AGENTS = getDefaultAgents(AGENT_NAME);
export const CONTROLPLANE_SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent','sysadmin','db-admin','git-admin','coordinator')),
role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent','coordinator')),
adapter TEXT NOT NULL DEFAULT 'pi-local',
heartbeat_enabled BOOLEAN NOT NULL DEFAULT FALSE,
heartbeat_interval_sec INTEGER,
@ -211,7 +187,7 @@ CREATE TABLE IF NOT EXISTS operators (
export async function runSchemaMigration(pool: pg.Pool): Promise<void> {
await pool.query(CONTROLPLANE_SCHEMA_SQL);
await migrateAgentIds(pool);
await cleanupLegacyAgents(pool);
await ensureRoleConstraint(pool);
await ensureApprovalDecisionColumn(pool);
await ensureParentTaskId(pool);
@ -232,169 +208,32 @@ export async function runSchemaMigration(pool: pg.Pool): Promise<void> {
await upsertBudget(pool, agent.id, daily);
}
}
}
async function migrateAgentIds(pool: pg.Pool): Promise<void> {
await pool.query(
'ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_role_check',
async function cleanupLegacyAgents(pool: pg.Pool): Promise<void> {
// One-time cleanup: remove legacy agent IDs that predate the current naming.
// After this runs once, all subsequent calls are no-ops (cheap SELECTs).
const legacyIds = ['sysadmin', 'db-admin', 'git-admin', 'dba', 'git_admin', 'ceo'];
const { rows } = await pool.query(
`SELECT id FROM agents WHERE id = ANY($1)`,
[legacyIds],
);
if (rows.length === 0) return;
// Drop FKs so we can rewrite agent IDs safely.
await pool.query(
'ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_assigned_to_fkey',
);
await pool.query(
'ALTER TABLE agent_activity DROP CONSTRAINT IF EXISTS agent_activity_agent_id_fkey',
);
await pool.query(
'ALTER TABLE agent_budgets DROP CONSTRAINT IF EXISTS agent_budgets_agent_id_fkey',
);
await pool.query(
'ALTER TABLE approvals DROP CONSTRAINT IF EXISTS approvals_agent_id_fkey',
);
// Drop FKs, clean up, re-add.
await pool.query('ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_role_check');
for (const table of ['tasks', 'agent_activity', 'agent_budgets', 'approvals']) {
await pool.query(`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_${table === 'tasks' ? 'assigned_to' : 'agent_id'}_fkey`);
}
const orchestratorId =
process.env.CONTROLPLANE_NAME || process.env.AGENT_NAME || AGENT_NAME;
// Update references first (no FK enforcement while dropped).
await pool.query('UPDATE tasks SET assigned_to = $1 WHERE assigned_to = $2', [
'sysadmin_agent',
'sysadmin',
]);
await pool.query('UPDATE tasks SET assigned_to = $1 WHERE assigned_to = $2', [
'db_admin_agent',
'dba',
]);
await pool.query('UPDATE tasks SET assigned_to = $1 WHERE assigned_to = $2', [
'git_admin_agent',
'git_admin',
]);
await pool.query('UPDATE tasks SET assigned_to = $1 WHERE assigned_to = $2', [
orchestratorId,
'ceo',
]);
await pool.query(
'UPDATE agent_activity SET agent_id = $1 WHERE agent_id = $2',
['sysadmin_agent', 'sysadmin'],
);
await pool.query(
'UPDATE agent_activity SET agent_id = $1 WHERE agent_id = $2',
['db_admin_agent', 'dba'],
);
await pool.query(
'UPDATE agent_activity SET agent_id = $1 WHERE agent_id = $2',
['git_admin_agent', 'git_admin'],
);
await pool.query(
'UPDATE agent_activity SET agent_id = $1 WHERE agent_id = $2',
[orchestratorId, 'ceo'],
);
await pool.query(
'UPDATE agent_budgets SET agent_id = $1 WHERE agent_id = $2',
['sysadmin_agent', 'sysadmin'],
);
await pool.query(
'UPDATE agent_budgets SET agent_id = $1 WHERE agent_id = $2',
['db_admin_agent', 'dba'],
);
await pool.query(
'UPDATE agent_budgets SET agent_id = $1 WHERE agent_id = $2',
['git_admin_agent', 'git_admin'],
);
await pool.query(
'UPDATE agent_budgets SET agent_id = $1 WHERE agent_id = $2',
[orchestratorId, 'ceo'],
);
await pool.query('UPDATE approvals SET agent_id = $1 WHERE agent_id = $2', [
'sysadmin_agent',
'sysadmin',
]);
await pool.query('UPDATE approvals SET agent_id = $1 WHERE agent_id = $2', [
'db_admin_agent',
'dba',
]);
await pool.query('UPDATE approvals SET agent_id = $1 WHERE agent_id = $2', [
'git_admin_agent',
'git_admin',
]);
await pool.query('UPDATE approvals SET agent_id = $1 WHERE agent_id = $2', [
orchestratorId,
'ceo',
]);
// Update agents table (ids + roles). If a new id already exists, drop the legacy row.
await pool.query(
`DELETE FROM agents
WHERE id = 'sysadmin'
AND EXISTS (SELECT 1 FROM agents WHERE id = 'sysadmin_agent')`,
);
await pool.query(
`DELETE FROM agents
WHERE id = 'dba'
AND EXISTS (SELECT 1 FROM agents WHERE id = 'db_admin_agent')`,
);
await pool.query(
`DELETE FROM agents
WHERE id = 'git_admin'
AND EXISTS (SELECT 1 FROM agents WHERE id = 'git_admin_agent')`,
);
await pool.query(
`DELETE FROM agents
WHERE id = 'ceo'
AND EXISTS (SELECT 1 FROM agents WHERE id = $1)`,
[orchestratorId],
);
await pool.query('UPDATE agents SET id = $1, role = $1 WHERE id = $2', [
'sysadmin_agent',
'sysadmin',
]);
await pool.query('UPDATE agents SET id = $1, role = $1 WHERE id = $2', [
'db_admin_agent',
'dba',
]);
await pool.query('UPDATE agents SET id = $1, role = $1 WHERE id = $2', [
'git_admin_agent',
'git_admin',
]);
await pool.query('UPDATE agents SET id = $1, role = $2 WHERE id = $3', [
orchestratorId,
'orchestrator',
'ceo',
]);
await pool.query('UPDATE agents SET role = $1 WHERE role = $2', [
'sysadmin_agent',
'sysadmin',
]);
await pool.query('UPDATE agents SET role = $1 WHERE role = $2', [
'db_admin_agent',
'dba',
]);
await pool.query('UPDATE agents SET role = $1 WHERE role = $2', [
'git_admin_agent',
'git_admin',
]);
await pool.query('UPDATE agents SET role = $1 WHERE role = $2', [
'orchestrator',
'ceo',
]);
// Re-add FKs.
await pool.query(
'ALTER TABLE tasks ADD CONSTRAINT tasks_assigned_to_fkey FOREIGN KEY (assigned_to) REFERENCES agents(id)',
);
await pool.query(
'ALTER TABLE agent_activity ADD CONSTRAINT agent_activity_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES agents(id)',
);
await pool.query(
'ALTER TABLE agent_budgets ADD CONSTRAINT agent_budgets_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES agents(id)',
);
await pool.query(
'ALTER TABLE approvals ADD CONSTRAINT approvals_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES agents(id)',
);
for (const id of legacyIds) {
await pool.query('DELETE FROM agent_budgets WHERE agent_id = $1', [id]);
await pool.query('DELETE FROM agent_activity WHERE agent_id = $1', [id]);
await pool.query('DELETE FROM approvals WHERE agent_id = $1', [id]);
await pool.query('UPDATE tasks SET assigned_to = NULL WHERE assigned_to = $1', [id]);
await pool.query('DELETE FROM agents WHERE id = $1', [id]);
}
}
async function ensureRoleConstraint(pool: pg.Pool): Promise<void> {
@ -404,7 +243,7 @@ async function ensureRoleConstraint(pool: pg.Pool): Promise<void> {
await pool.query(
`ALTER TABLE agents
ADD CONSTRAINT agents_role_check
CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent','sysadmin','db-admin','git-admin','coordinator'))`,
CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent','coordinator'))`,
);
}

View file

@ -72,8 +72,8 @@ describe('Control Plane Provisioning', () => {
}
});
it('exactly 8 default agents are defined (4 canonical + 4 alias)', () => {
expect(DEFAULT_AGENTS).toHaveLength(8);
it('exactly 5 default agents are defined', () => {
expect(DEFAULT_AGENTS).toHaveLength(5);
});
});