import { afterEach, describe, expect, it } from 'vitest' import { createMemoryDb, runMigrations, repos } from '@evofw/db' import { previewLabelAssignmentSchema, applyLabelAssignmentSchema, permissionForRequest } from '@evofw/shared' import { createAgentLabel, patchAgentLabel, putAgentLabels, previewLabelAssignment, applyLabelAssignment } from './labels.js' import { createPolicySnapshot, startPolicyRollout } from './policy/rollouts.js' import { buildApp } from '../app.js' import { loadConfig } from '../config.js' const opened: ReturnType[] = [] afterEach(() => { for (const mem of opened.splice(0)) mem.sqlite.close() }) function memory(count = 3) { const mem = createMemoryDb(); opened.push(mem); runMigrations(mem.sqlite) for (let index = 0; index < count; index++) repos.insertAgent(mem.db, { id: `a${index}`, name: `Agent ${index}`, tokenHash: `t${index}`, tokenPrefix: `t${index}`, status: 'approved', telemetryVersion: 2, capabilitiesJson: '["policy-ipv4"]' }) for (const id of ['old', 'new', 'third']) repos.insertPolicySet(mem.db, { id, name: id }) return mem } describe('labels and materialized group assignment', () => { it('treats labels as metadata, uses normalized unique names and atomically validates replacement', () => { const { db } = memory() const label = createAgentLabel(db, { name: 'Production', color: 'success' }) expect(() => createAgentLabel(db, { name: 'PRODUCTION' })).toThrow('already exists') putAgentLabels(db, 'a0', [label.id]) patchAgentLabel(db, label.id, { name: 'Production fleet' }) expect(repos.getAgent(db, 'a0')!.policyGeneration).toBe(1) expect(() => putAgentLabels(db, 'a0', ['missing'])).toThrow('Label not found') expect(repos.agentLabelIds(db, 'a0')).toEqual([label.id]) repos.deleteAgentLabel(db, label.id) expect(repos.agentLabelIds(db, 'a0')).toEqual([]) expect(repos.getAgent(db, 'a0')!.policyGeneration).toBe(1) }) it('preserves existing order in add, performs exact replace and skips identical assignments', () => { const { db } = memory() repos.setAgentPolicySets(db, 'a0', ['old', 'third']) const request = { selector: { agent_ids: ['a0', 'a1'] }, mode: 'add' as const, set_ids: ['third', 'new'] } const preview = previewLabelAssignment(db, request) expect(preview.items[0]!.set_ids).toEqual(['old', 'third', 'new']) const result = applyLabelAssignment(db, { ...request, expected: preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) }) expect(result.affected_agent_ids).toEqual(['a0', 'a1']) const again = previewLabelAssignment(db, request) const unchanged = applyLabelAssignment(db, { ...request, expected: again.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) }) expect(unchanged.affected_agent_ids).toEqual([]) expect(unchanged.items.map((row) => row.generation)).toEqual(again.items.map((row) => row.generation)) const replace = { selector: { agent_ids: ['a0'] }, mode: 'replace' as const, set_ids: ['new'] } const replacement = previewLabelAssignment(db, replace) applyLabelAssignment(db, { ...replace, expected: replacement.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) }) expect(repos.listSetsForAgent(db, 'a0').map((row) => row.setId)).toEqual(['new']) }) it('requires exact materialized membership and generations; later labels never autoassign', () => { const { db } = memory() const label = createAgentLabel(db, { name: 'Canaries' }) putAgentLabels(db, 'a0', [label.id]) const request = { selector: { label_ids: [label.id] }, mode: 'replace' as const, set_ids: ['new'] } const preview = previewLabelAssignment(db, request) const expected = preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) putAgentLabels(db, 'a1', [label.id]) expect(() => applyLabelAssignment(db, { ...request, expected })).toThrow('membership') expect(repos.listSetsForAgent(db, 'a0')).toHaveLength(0) putAgentLabels(db, 'a1', []) applyLabelAssignment(db, { ...request, expected }) putAgentLabels(db, 'a1', [label.id]) expect(repos.listSetsForAgent(db, 'a1')).toHaveLength(0) expect(() => applyLabelAssignment(db, { ...request, expected })).toThrow('generation') }) it('rejects invalid sets and pinned targets before writing any agent', () => { const { db } = memory() const request = { selector: { agent_ids: ['a0', 'a1'] }, mode: 'replace' as const, set_ids: ['new'] } const preview = previewLabelAssignment(db, request) const expected = preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) expect(() => applyLabelAssignment(db, { ...request, set_ids: ['new', 'missing'], expected })).toThrow('Policy set not found') expect(repos.listSetsForAgent(db, 'a0')).toHaveLength(0) const snapshot = createPolicySnapshot(db, 'a2', 'Empty') startPolicyRollout(db, { snapshot_id: snapshot.id, targets: [{ agent_id: 'a1', expected_generation: 1 }], canary_ids: ['a1'] }) expect(() => applyLabelAssignment(db, { ...request, expected })).toThrow('Release') expect(repos.listSetsForAgent(db, 'a0')).toHaveLength(0) expect(repos.getAgent(db, 'a0')!.policyGeneration).toBe(1) }) it('supports exactly 100 selected agents and rejects a 101-agent label group without truncation', () => { const { db } = memory(101) const label = createAgentLabel(db, { name: 'Large group' }) for (let index = 0; index < 100; index++) putAgentLabels(db, `a${index}`, [label.id]) const request = { selector: { label_ids: [label.id] }, mode: 'add' as const, set_ids: ['new'] } const preview = previewLabelAssignment(db, request) expect(preview.total).toBe(100) const applied = applyLabelAssignment(db, { ...request, expected: preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) }) expect(applied.affected_agent_ids).toHaveLength(100) expect(repos.listSetsForAgent(db, 'a100')).toHaveLength(0) putAgentLabels(db, 'a100', [label.id]) expect(() => previewLabelAssignment(db, request)).toThrow('100 agents') expect(repos.listSetsForAgent(db, 'a100')).toHaveLength(0) expect(previewLabelAssignmentSchema.safeParse({ ...request, selector: { agent_ids: ['a0'], label_ids: [label.id] } }).success).toBe(false) expect(applyLabelAssignmentSchema.safeParse({ ...request, expected: [{ agent_id: 'a0', generation: 1 }, { agent_id: 'a0', generation: 1 }] }).success).toBe(false) }) it('provides HTTP label CRUD, AND fleet filtering and permission separation', async () => { const app = await buildApp({ memory: true, config: { ...loadConfig(), authRequired: false, staticDir: null, logLevel: 'error' } }) try { await app.ready() for (const id of ['a', 'b']) repos.insertAgent(app.db, { id, name: id, tokenPrefix: id, tokenHash: id }) const one = await app.inject({ method: 'POST', url: '/api/v1/agent-labels', payload: { name: 'One' } }) const two = await app.inject({ method: 'POST', url: '/api/v1/agent-labels', payload: { name: 'Two' } }) expect(one.statusCode).toBe(201) expect(one.json().name_key).toBeUndefined() await app.inject({ method: 'PUT', url: '/api/v1/agents/a/labels', payload: { label_ids: [one.json().id, two.json().id] } }) await app.inject({ method: 'PUT', url: '/api/v1/agents/b/labels', payload: { label_ids: [one.json().id] } }) const filtered = await app.inject({ url: `/api/v1/agents?label_ids=${one.json().id},${two.json().id}` }) expect(filtered.json().items.map((row: { id: string }) => row.id)).toEqual(['a']) expect((await app.inject({ url: '/api/v1/agents?label_ids=' })).json().items).toHaveLength(2) expect(permissionForRequest('POST', '/api/v1/agent-labels/preview-assignment')).toBe('fw:policies:read') expect(permissionForRequest('POST', '/api/v1/agent-labels/assign-policy-sets')).toBe('fw:policies:write') expect(permissionForRequest('POST', '/api/v1/agent-labels')).toBe('fw:agents:write') } finally { await app.close() } }) })