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:
parent
9b88f6ca2c
commit
0f7fbc400c
3 changed files with 33 additions and 209 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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'))`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue