From fe132bca44dfdd8c1be2ce2525cbb24b1a4c71eb Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 13:08:31 +1000 Subject: [PATCH 1/8] test: add SDK feature parity batch 2 tests (#347/#341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 39 tests covering 5 SDK features that were previously untested: - #27 Manual Ceremonies: trigger types, schedule, hooks, defineSquad composition - #28 Ceremony Cooldown: schedule-gated cadence, multi-ceremony schedules - #36 Human Team Members: agent status lifecycle (active/inactive/retired) - #49 Constraint Budget: ask_user rate limiter, file-write path guards, shell command restrictions, combined constraints, defineHooks builder - #50 Multi-Agent Artifact: per-artifact lockout, handoff, HookPipeline integration All tests exercise real SDK implementations (builders, HookPipeline, ReviewerLockoutHook) — no stubs. --- test/sdk-feature-parity-batch2.test.ts | 680 +++++++++++++++++++++++++ 1 file changed, 680 insertions(+) create mode 100644 test/sdk-feature-parity-batch2.test.ts diff --git a/test/sdk-feature-parity-batch2.test.ts b/test/sdk-feature-parity-batch2.test.ts new file mode 100644 index 000000000..64b2d5c78 --- /dev/null +++ b/test/sdk-feature-parity-batch2.test.ts @@ -0,0 +1,680 @@ +/** + * SDK Feature Parity Tests — Batch 2 (Issue #347 / #341) + * + * Tests for SDK features that were previously marked "⚠️ Needs Setup" in the + * feature parity matrix. These exercise real SDK implementations: + * + * #27 Manual Ceremonies — ceremony trigger types and config composition + * #28 Ceremony Cooldown — ceremony schedule & re-trigger gating + * #36 Human Team Members — agent status lifecycle and roster composition + * #49 Constraint Budget — ask_user rate limiting, file-write path guards + * #50 Multi-Agent Artifact — artifact-level lockout coordination + * + * @see test/sdk-feature-parity.test.ts — Batch 1 (worktree, lockout, deadlock, confidence) + * @see test/feature-parity.test.ts — Integration tests (coordinator, casting, etc.) + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { + HookPipeline, + ReviewerLockoutHook, + type PreToolUseContext, + type PolicyConfig, +} from '../packages/squad-sdk/src/hooks/index.js'; +import type { + CeremonyDefinition, + AgentDefinition, + HooksDefinition, + SquadSDKConfig, +} from '../packages/squad-sdk/src/builders/types.js'; +import { + defineTeam, + defineAgent, + defineRouting, + defineCeremony, + defineHooks, + defineCasting, + defineSquad, +} from '../packages/squad-sdk/src/builders/index.js'; + +// ============================================================================= +// Feature #27: Manual Ceremonies +// ============================================================================= + +describe('SDK Feature: Manual Ceremonies (#27)', () => { + it('defineCeremony() accepts trigger: "manual"', () => { + const ceremony = defineCeremony({ + name: 'design-review', + trigger: 'manual', + participants: ['edie', 'fenster', 'hockney'], + agenda: 'Review architecture decisions and trade-offs', + }); + + expect(ceremony.name).toBe('design-review'); + expect(ceremony.trigger).toBe('manual'); + expect(ceremony.participants).toHaveLength(3); + expect(ceremony.agenda).toContain('architecture'); + }); + + it('defineCeremony() accepts trigger: "pr-merged"', () => { + const ceremony = defineCeremony({ + name: 'post-merge-review', + trigger: 'pr-merged', + participants: ['hockney'], + agenda: 'Run regression tests and update coverage report', + }); + + expect(ceremony.trigger).toBe('pr-merged'); + }); + + it('defineCeremony() accepts trigger: "schedule" with cron', () => { + const ceremony = defineCeremony({ + name: 'standup', + trigger: 'schedule', + schedule: '0 9 * * 1-5', + participants: ['edie', 'fenster', 'hockney'], + agenda: 'Yesterday / Today / Blockers', + }); + + expect(ceremony.trigger).toBe('schedule'); + expect(ceremony.schedule).toBe('0 9 * * 1-5'); + }); + + it('ceremony with hooks fires named hooks', () => { + const ceremony = defineCeremony({ + name: 'retrospective', + trigger: 'manual', + participants: ['edie', 'fenster'], + agenda: 'What went well? What to improve?', + hooks: ['pre-retro-gather-metrics', 'post-retro-create-issues'], + }); + + expect(ceremony.hooks).toHaveLength(2); + expect(ceremony.hooks).toContain('pre-retro-gather-metrics'); + expect(ceremony.hooks).toContain('post-retro-create-issues'); + }); + + it('defineSquad() composes multiple ceremonies including manual', () => { + const config = defineSquad({ + team: defineTeam({ name: 'Alpha', members: ['edie', 'hockney'] }), + agents: [ + defineAgent({ name: 'edie', role: 'TypeScript Engineer' }), + defineAgent({ name: 'hockney', role: 'Tester' }), + ], + ceremonies: [ + defineCeremony({ + name: 'standup', + trigger: 'schedule', + schedule: '0 9 * * 1-5', + participants: ['edie', 'hockney'], + }), + defineCeremony({ + name: 'design-review', + trigger: 'manual', + participants: ['edie'], + agenda: 'Review pending architecture decisions', + }), + ], + }); + + expect(config.ceremonies).toHaveLength(2); + const manual = config.ceremonies!.find(c => c.trigger === 'manual'); + expect(manual).toBeDefined(); + expect(manual!.name).toBe('design-review'); + + const scheduled = config.ceremonies!.find(c => c.trigger === 'schedule'); + expect(scheduled).toBeDefined(); + expect(scheduled!.schedule).toBe('0 9 * * 1-5'); + }); + + it('ceremony participants can reference any agent in the squad', () => { + const config = defineSquad({ + team: defineTeam({ name: 'Squad', members: ['edie', 'fenster', 'hockney'] }), + agents: [ + defineAgent({ name: 'edie', role: 'Lead' }), + defineAgent({ name: 'fenster', role: 'Tester' }), + defineAgent({ name: 'hockney', role: 'Developer' }), + ], + ceremonies: [ + defineCeremony({ + name: 'full-team-sync', + trigger: 'manual', + participants: ['edie', 'fenster', 'hockney'], + agenda: 'Cross-team sync', + }), + ], + }); + + const ceremony = config.ceremonies![0]; + // All participants should be valid agent names from the squad + for (const p of ceremony.participants!) { + const agent = config.agents.find(a => a.name === p); + expect(agent).toBeDefined(); + } + }); +}); + +// ============================================================================= +// Feature #28: Ceremony Cooldown (Schedule-Gated Re-Trigger) +// ============================================================================= + +describe('SDK Feature: Ceremony Cooldown (#28)', () => { + it('ceremony with schedule defines a cadence (preventing over-triggering)', () => { + const ceremony = defineCeremony({ + name: 'standup', + trigger: 'schedule', + schedule: '0 9 * * 1-5', + participants: ['edie'], + agenda: 'Yesterday / Today / Blockers', + }); + + // Schedule field enables cadence — the ceremony should only fire on schedule + expect(ceremony.schedule).toBe('0 9 * * 1-5'); + expect(ceremony.trigger).toBe('schedule'); + }); + + it('ceremony without schedule has no cadence restriction', () => { + const ceremony = defineCeremony({ + name: 'ad-hoc-review', + trigger: 'manual', + participants: ['edie'], + }); + + expect(ceremony.schedule).toBeUndefined(); + expect(ceremony.trigger).toBe('manual'); + }); + + it('multiple ceremonies can have different schedules', () => { + const ceremonies = [ + defineCeremony({ name: 'standup', trigger: 'schedule', schedule: '0 9 * * 1-5' }), + defineCeremony({ name: 'retro', trigger: 'schedule', schedule: '0 15 * * 5' }), + defineCeremony({ name: 'planning', trigger: 'schedule', schedule: '0 10 * * 1' }), + ]; + + const schedules = ceremonies.map(c => c.schedule); + // All have distinct schedules + expect(new Set(schedules).size).toBe(3); + }); + + it('defineSquad() validates ceremonies with schedules in full config', () => { + const config = defineSquad({ + team: defineTeam({ name: 'CooldownTeam', members: ['edie'] }), + agents: [defineAgent({ name: 'edie', role: 'Engineer' })], + ceremonies: [ + defineCeremony({ + name: 'daily-standup', + trigger: 'schedule', + schedule: '0 9 * * 1-5', + participants: ['edie'], + agenda: 'Daily sync', + }), + ], + }); + + expect(config.ceremonies![0].schedule).toBe('0 9 * * 1-5'); + }); +}); + +// ============================================================================= +// Feature #36: Human Team Members +// ============================================================================= + +describe('SDK Feature: Human Team Members (#36)', () => { + it('defineAgent() accepts status: "active" for active agents', () => { + const agent = defineAgent({ name: 'edie', role: 'Engineer', status: 'active' }); + expect(agent.status).toBe('active'); + }); + + it('defineAgent() accepts status: "inactive" for paused agents', () => { + const agent = defineAgent({ name: 'alice', role: 'Product Lead', status: 'inactive' }); + expect(agent.status).toBe('inactive'); + }); + + it('defineAgent() accepts status: "retired" for removed agents', () => { + const agent = defineAgent({ name: 'old-bot', role: 'Archivist', status: 'retired' }); + expect(agent.status).toBe('retired'); + }); + + it('squad config can include agents with mixed statuses', () => { + const config = defineSquad({ + team: defineTeam({ name: 'Mixed', members: ['edie', 'alice', 'old-bot'] }), + agents: [ + defineAgent({ name: 'edie', role: 'Engineer', status: 'active' }), + defineAgent({ name: 'alice', role: 'Product Lead', status: 'inactive' }), + defineAgent({ name: 'old-bot', role: 'Archivist', status: 'retired' }), + ], + }); + + const active = config.agents.filter(a => a.status === 'active'); + const inactive = config.agents.filter(a => a.status === 'inactive'); + const retired = config.agents.filter(a => a.status === 'retired'); + + expect(active).toHaveLength(1); + expect(inactive).toHaveLength(1); + expect(retired).toHaveLength(1); + }); + + it('agent without status defaults to undefined (implicit active)', () => { + const agent = defineAgent({ name: 'fenster', role: 'Tester' }); + expect(agent.status).toBeUndefined(); + }); + + it('routing rules can reference agents regardless of status', () => { + const config = defineSquad({ + team: defineTeam({ name: 'Routed', members: ['edie', 'hockney'] }), + agents: [ + defineAgent({ name: 'edie', role: 'Engineer', status: 'active' }), + defineAgent({ name: 'hockney', role: 'Tester', status: 'inactive' }), + ], + routing: defineRouting({ + rules: [ + { pattern: 'test-*', agents: ['hockney'] }, + { pattern: 'feature-*', agents: ['edie'] }, + ], + defaultAgent: 'edie', + }), + }); + + // Routing rules exist for both active and inactive agents + expect(config.routing!.rules).toHaveLength(2); + const testRule = config.routing!.rules.find(r => r.pattern === 'test-*'); + expect(testRule!.agents).toContain('hockney'); + }); + + it('team members list can include descriptive roles for human oversight', () => { + const config = defineSquad({ + team: defineTeam({ + name: 'HumanLed', + members: ['edie', 'fenster', 'project-manager'], + description: 'Team with human oversight', + }), + agents: [ + defineAgent({ name: 'edie', role: 'Engineer' }), + defineAgent({ name: 'fenster', role: 'Tester' }), + defineAgent({ name: 'project-manager', role: 'Product Lead', status: 'inactive' }), + ], + }); + + expect(config.team.members).toContain('project-manager'); + const pm = config.agents.find(a => a.name === 'project-manager'); + expect(pm!.role).toBe('Product Lead'); + expect(pm!.status).toBe('inactive'); + }); +}); + +// ============================================================================= +// Feature #49: Constraint Budget +// ============================================================================= + +describe('SDK Feature: Constraint Budget (#49)', () => { + let pipeline: HookPipeline; + + describe('ask_user rate limiting', () => { + beforeEach(() => { + pipeline = new HookPipeline({ maxAskUserPerSession: 3 }); + }); + + it('allows ask_user calls within budget', async () => { + const ctx: PreToolUseContext = { + toolName: 'ask_user', + arguments: { question: 'Which DB?' }, + agentName: 'edie', + sessionId: 'session-1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + + it('blocks ask_user when budget is exhausted', async () => { + const ctx: PreToolUseContext = { + toolName: 'ask_user', + arguments: { question: 'Question?' }, + agentName: 'edie', + sessionId: 'session-1', + }; + + // Exhaust the budget + for (let i = 0; i < 3; i++) { + const r = await pipeline.runPreToolHooks(ctx); + expect(r.action).toBe('allow'); + } + + // 4th call should be blocked + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('block'); + expect(result.reason).toMatch(/rate limit/i); + }); + + it('tracks ask_user budget per session independently', async () => { + const session1: PreToolUseContext = { + toolName: 'ask_user', + arguments: { question: 'Q?' }, + agentName: 'edie', + sessionId: 'session-1', + }; + const session2: PreToolUseContext = { + ...session1, + sessionId: 'session-2', + }; + + // Exhaust session-1 budget + for (let i = 0; i < 3; i++) { + await pipeline.runPreToolHooks(session1); + } + + // session-1 blocked, session-2 still allowed + const r1 = await pipeline.runPreToolHooks(session1); + expect(r1.action).toBe('block'); + + const r2 = await pipeline.runPreToolHooks(session2); + expect(r2.action).toBe('allow'); + }); + + it('non-ask_user tools are unaffected by rate limit', async () => { + const ctx: PreToolUseContext = { + toolName: 'edit', + arguments: { path: 'src/foo.ts' }, + agentName: 'edie', + sessionId: 'session-1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + }); + + describe('file-write path constraints', () => { + beforeEach(() => { + pipeline = new HookPipeline({ + allowedWritePaths: ['src/**', 'test/**', '.squad/**'], + }); + }); + + it('allows writes to permitted paths', async () => { + const ctx: PreToolUseContext = { + toolName: 'edit', + arguments: { path: 'src/components/auth.ts' }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + + it('allows writes to test paths', async () => { + const ctx: PreToolUseContext = { + toolName: 'create', + arguments: { path: 'test/auth.test.ts' }, + agentName: 'fenster', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + + it('blocks writes to paths outside allowed globs', async () => { + const ctx: PreToolUseContext = { + toolName: 'edit', + arguments: { path: 'package.json' }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('block'); + expect(result.reason).toMatch(/does not match allowed paths/); + }); + + it('allows .squad/ path writes', async () => { + const ctx: PreToolUseContext = { + toolName: 'create', + arguments: { path: '.squad/agents/edie/charter.md' }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + + it('read operations bypass write guards', async () => { + const ctx: PreToolUseContext = { + toolName: 'read', + arguments: { path: '/etc/passwd' }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + }); + + describe('shell command restrictions', () => { + beforeEach(() => { + pipeline = new HookPipeline({ + blockedCommands: ['rm -rf', 'DROP TABLE'], + }); + }); + + it('blocks dangerous shell commands', async () => { + const ctx: PreToolUseContext = { + toolName: 'bash', + arguments: { command: 'rm -rf /important/data' }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('block'); + expect(result.reason).toMatch(/rm -rf/); + }); + + it('allows safe shell commands', async () => { + const ctx: PreToolUseContext = { + toolName: 'bash', + arguments: { command: 'npm test' }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('allow'); + }); + + it('blocks SQL injection patterns', async () => { + const ctx: PreToolUseContext = { + toolName: 'bash', + arguments: { command: "psql -c 'DROP TABLE users;'" }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPreToolHooks(ctx); + expect(result.action).toBe('block'); + }); + }); + + describe('combined constraints', () => { + it('enforces multiple constraint types simultaneously', async () => { + pipeline = new HookPipeline({ + maxAskUserPerSession: 2, + allowedWritePaths: ['src/**'], + blockedCommands: ['rm -rf'], + reviewerLockout: true, + }); + + // File write to allowed path — OK + const write = await pipeline.runPreToolHooks({ + toolName: 'edit', + arguments: { path: 'src/app.ts' }, + agentName: 'edie', + sessionId: 's1', + }); + expect(write.action).toBe('allow'); + + // File write to disallowed path — blocked + const blocked = await pipeline.runPreToolHooks({ + toolName: 'edit', + arguments: { path: 'dist/bundle.js' }, + agentName: 'edie', + sessionId: 's1', + }); + expect(blocked.action).toBe('block'); + + // ask_user within budget — OK + const ask1 = await pipeline.runPreToolHooks({ + toolName: 'ask_user', + arguments: { question: 'Q1?' }, + agentName: 'edie', + sessionId: 's1', + }); + expect(ask1.action).toBe('allow'); + }); + }); + + describe('defineHooks() builder integration', () => { + it('defineHooks() accepts and validates constraint fields', () => { + const hooks = defineHooks({ + allowedWritePaths: ['src/**', 'test/**'], + blockedCommands: ['rm -rf'], + maxAskUser: 5, + scrubPii: true, + reviewerLockout: true, + }); + + expect(hooks.allowedWritePaths).toEqual(['src/**', 'test/**']); + expect(hooks.blockedCommands).toEqual(['rm -rf']); + expect(hooks.maxAskUser).toBe(5); + expect(hooks.scrubPii).toBe(true); + expect(hooks.reviewerLockout).toBe(true); + }); + + it('defineHooks() passes through without setting defaults', () => { + const hooks = defineHooks({}); + // defineHooks is a validator, not a defaulter — undefined fields stay undefined + expect(hooks.maxAskUser).toBeUndefined(); + }); + + it('defineSquad() with hooks constraints composes correctly', () => { + const config = defineSquad({ + team: defineTeam({ name: 'Constrained', members: ['edie'] }), + agents: [defineAgent({ name: 'edie', role: 'Engineer' })], + hooks: defineHooks({ + maxAskUser: 2, + allowedWritePaths: ['src/**'], + blockedCommands: ['rm -rf /'], + scrubPii: true, + }), + }); + + expect(config.hooks).toBeDefined(); + expect(config.hooks!.maxAskUser).toBe(2); + expect(config.hooks!.allowedWritePaths).toEqual(['src/**']); + }); + }); +}); + +// ============================================================================= +// Feature #50: Multi-Agent Artifact Coordination +// ============================================================================= + +describe('SDK Feature: Multi-Agent Artifact Coordination (#50)', () => { + let lockout: ReviewerLockoutHook; + + beforeEach(() => { + lockout = new ReviewerLockoutHook(); + }); + + it('artifact lockout tracks per-artifact per-agent', () => { + lockout.lockout('architecture.md', 'edie'); + lockout.lockout('architecture.md', 'fenster'); + lockout.lockout('tests.md', 'hockney'); + + expect(lockout.isLockedOut('architecture.md', 'edie')).toBe(true); + expect(lockout.isLockedOut('architecture.md', 'fenster')).toBe(true); + expect(lockout.isLockedOut('architecture.md', 'hockney')).toBe(false); + expect(lockout.isLockedOut('tests.md', 'hockney')).toBe(true); + expect(lockout.isLockedOut('tests.md', 'edie')).toBe(false); + }); + + it('getLockedAgents() lists all agents locked from an artifact', () => { + lockout.lockout('design-doc.md', 'edie'); + lockout.lockout('design-doc.md', 'fenster'); + + const locked = lockout.getLockedAgents('design-doc.md'); + expect(locked).toHaveLength(2); + expect(locked).toContain('edie'); + expect(locked).toContain('fenster'); + }); + + it('clearLockout() enables handoff to next contributor', () => { + lockout.lockout('api-spec.md', 'edie'); + expect(lockout.isLockedOut('api-spec.md', 'edie')).toBe(true); + + lockout.clearLockout('api-spec.md'); + expect(lockout.isLockedOut('api-spec.md', 'edie')).toBe(false); + }); + + it('multi-agent artifact workflow: write → lockout → handoff', () => { + // Agent 1 writes their section, then gets locked out + lockout.lockout('prd.md', 'edie'); + + // Agent 2 can still contribute + expect(lockout.isLockedOut('prd.md', 'fenster')).toBe(false); + + // Agent 2 finishes their section, gets locked out + lockout.lockout('prd.md', 'fenster'); + + // Agent 3 handles final review + expect(lockout.isLockedOut('prd.md', 'hockney')).toBe(false); + + // After all contributions complete, clear for next iteration + lockout.clearLockout('prd.md'); + expect(lockout.getLockedAgents('prd.md')).toHaveLength(0); + }); + + it('artifact lockout integrates with HookPipeline', async () => { + const pipeline = new HookPipeline({ reviewerLockout: true }); + const rlHook = pipeline.getReviewerLockout(); + + // Lock out edie from the artifact path + rlHook.lockout('src/auth', 'edie'); + + // edie tries to edit a file in the locked artifact scope + const result = await pipeline.runPreToolHooks({ + toolName: 'edit', + arguments: { path: 'src/auth/login.ts' }, + agentName: 'edie', + sessionId: 's1', + }); + + expect(result.action).toBe('block'); + expect(result.reason).toMatch(/lockout/i); + + // Different agent can still edit + const otherResult = await pipeline.runPreToolHooks({ + toolName: 'edit', + arguments: { path: 'src/auth/login.ts' }, + agentName: 'fenster', + sessionId: 's1', + }); + + expect(otherResult.action).toBe('allow'); + }); + + it('multiple artifacts can be tracked independently', () => { + lockout.lockout('frontend/app.tsx', 'edie'); + lockout.lockout('backend/server.ts', 'fenster'); + lockout.lockout('docs/readme.md', 'hockney'); + + expect(lockout.isLockedOut('frontend/app.tsx', 'edie')).toBe(true); + expect(lockout.isLockedOut('frontend/app.tsx', 'fenster')).toBe(false); + expect(lockout.isLockedOut('backend/server.ts', 'fenster')).toBe(true); + expect(lockout.isLockedOut('backend/server.ts', 'edie')).toBe(false); + expect(lockout.isLockedOut('docs/readme.md', 'hockney')).toBe(true); + }); +}); From a329a2e4076ae075af5eec2d974f3397e3bd98aa Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 13:08:47 +1000 Subject: [PATCH 2/8] chore: add changeset for parity batch 2 tests --- .changeset/sdk-parity-batch2-tests.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sdk-parity-batch2-tests.md diff --git a/.changeset/sdk-parity-batch2-tests.md b/.changeset/sdk-parity-batch2-tests.md new file mode 100644 index 000000000..a46a285e9 --- /dev/null +++ b/.changeset/sdk-parity-batch2-tests.md @@ -0,0 +1,5 @@ +--- +"@bradygaster/squad-sdk": patch +--- + +Add SDK feature parity batch 2 tests (#347/#341) — 39 tests covering manual ceremonies, ceremony cooldown, human team members, constraint budget enforcement, and multi-agent artifact coordination From a0f445eb3d0b519595eb052535204740c218e2d1 Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 13:22:55 +1000 Subject: [PATCH 3/8] test: add builder validation edge-case tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strengthen ceremony, agent, and hooks builder tests with 17 negative/ edge-case tests that verify intended validation behavior: - defineCeremony: rejects empty name, non-string trigger/schedule, non-array participants/hooks, null config - defineAgent: rejects invalid status enum, empty name/role, non-string status, non-object config - defineHooks: rejects non-array allowedWritePaths/blockedCommands, non-number maxAskUser, non-boolean scrubPii/reviewerLockout, null config These transform weak pass-through contract tests into proper behavioral tests — verifying the builders actively reject invalid input. --- test/sdk-feature-parity-batch2.test.ts | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/sdk-feature-parity-batch2.test.ts b/test/sdk-feature-parity-batch2.test.ts index 64b2d5c78..c4dfdc14f 100644 --- a/test/sdk-feature-parity-batch2.test.ts +++ b/test/sdk-feature-parity-batch2.test.ts @@ -35,6 +35,7 @@ import { defineHooks, defineCasting, defineSquad, + BuilderValidationError, } from '../packages/squad-sdk/src/builders/index.js'; // ============================================================================= @@ -127,6 +128,42 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { expect(scheduled!.schedule).toBe('0 9 * * 1-5'); }); + it('defineCeremony() throws on missing name', () => { + expect(() => + defineCeremony({ name: '', trigger: 'manual' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineCeremony() throws on non-string trigger', () => { + expect(() => + defineCeremony({ name: 'bad', trigger: 42 } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineCeremony() throws on non-array participants', () => { + expect(() => + defineCeremony({ name: 'bad', participants: 'edie' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineCeremony() throws on non-string schedule', () => { + expect(() => + defineCeremony({ name: 'bad', schedule: 42 } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineCeremony() throws on non-array hooks', () => { + expect(() => + defineCeremony({ name: 'bad', hooks: 'hook-one' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineCeremony() throws on non-object config', () => { + expect(() => + defineCeremony(null as any), + ).toThrow(BuilderValidationError); + }); + it('ceremony participants can reference any agent in the squad', () => { const config = defineSquad({ team: defineTeam({ name: 'Squad', members: ['edie', 'fenster', 'hockney'] }), @@ -259,6 +296,39 @@ describe('SDK Feature: Human Team Members (#36)', () => { expect(agent.status).toBeUndefined(); }); + it('defineAgent() throws on invalid status value', () => { + expect(() => + defineAgent({ name: 'bad', role: 'Tester', status: 'suspended' } as any), + ).toThrow(BuilderValidationError); + expect(() => + defineAgent({ name: 'bad', role: 'Tester', status: 'suspended' } as any), + ).toThrow(/must be one of/); + }); + + it('defineAgent() throws on empty name', () => { + expect(() => + defineAgent({ name: '', role: 'Engineer' }), + ).toThrow(BuilderValidationError); + }); + + it('defineAgent() throws on empty role', () => { + expect(() => + defineAgent({ name: 'edie', role: '' }), + ).toThrow(BuilderValidationError); + }); + + it('defineAgent() throws on non-string status', () => { + expect(() => + defineAgent({ name: 'edie', role: 'Engineer', status: true } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineAgent() throws on non-object config', () => { + expect(() => + defineAgent('not-an-object' as any), + ).toThrow(BuilderValidationError); + }); + it('routing rules can reference agents regardless of status', () => { const config = defineSquad({ team: defineTeam({ name: 'Routed', members: ['edie', 'hockney'] }), @@ -559,6 +629,42 @@ describe('SDK Feature: Constraint Budget (#49)', () => { expect(hooks.maxAskUser).toBeUndefined(); }); + it('defineHooks() throws on non-array allowedWritePaths', () => { + expect(() => + defineHooks({ allowedWritePaths: 'src/**' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineHooks() throws on non-array blockedCommands', () => { + expect(() => + defineHooks({ blockedCommands: 'rm -rf' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineHooks() throws on non-number maxAskUser', () => { + expect(() => + defineHooks({ maxAskUser: 'three' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineHooks() throws on non-boolean scrubPii', () => { + expect(() => + defineHooks({ scrubPii: 'yes' } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineHooks() throws on non-boolean reviewerLockout', () => { + expect(() => + defineHooks({ reviewerLockout: 1 } as any), + ).toThrow(BuilderValidationError); + }); + + it('defineHooks() throws on non-object config', () => { + expect(() => + defineHooks(null as any), + ).toThrow(BuilderValidationError); + }); + it('defineSquad() with hooks constraints composes correctly', () => { const config = defineSquad({ team: defineTeam({ name: 'Constrained', members: ['edie'] }), From 0a567ef8078346a86934b6894c7d466a79470e02 Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 13:38:21 +1000 Subject: [PATCH 4/8] test: add PII scrubber tests + fix changeset count Add 8 PII scrubbing tests exercising PostToolUse hooks: - Email redaction in strings (single and multiple) - Nested object recursive scrubbing - Array element scrubbing - Mixed types (numbers, null, booleans preserved) - Deep nesting with mixed types - Disabled scrubPii flag bypass - Clean strings pass through unchanged Update changeset description to reflect 64 total tests. --- .changeset/sdk-parity-batch2-tests.md | 2 +- test/sdk-feature-parity-batch2.test.ts | 144 +++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/.changeset/sdk-parity-batch2-tests.md b/.changeset/sdk-parity-batch2-tests.md index a46a285e9..c80c1d082 100644 --- a/.changeset/sdk-parity-batch2-tests.md +++ b/.changeset/sdk-parity-batch2-tests.md @@ -2,4 +2,4 @@ "@bradygaster/squad-sdk": patch --- -Add SDK feature parity batch 2 tests (#347/#341) — 39 tests covering manual ceremonies, ceremony cooldown, human team members, constraint budget enforcement, and multi-agent artifact coordination +Add SDK feature parity batch 2 tests (#347/#341) — 56+ tests covering manual ceremonies, ceremony cooldown, human team members, constraint budget enforcement (rate limiter, file-write guards, shell restrictions, PII scrubbing), and multi-agent artifact coordination diff --git a/test/sdk-feature-parity-batch2.test.ts b/test/sdk-feature-parity-batch2.test.ts index c4dfdc14f..0b5cb6738 100644 --- a/test/sdk-feature-parity-batch2.test.ts +++ b/test/sdk-feature-parity-batch2.test.ts @@ -19,6 +19,7 @@ import { HookPipeline, ReviewerLockoutHook, type PreToolUseContext, + type PostToolUseContext, type PolicyConfig, } from '../packages/squad-sdk/src/hooks/index.js'; import type { @@ -606,6 +607,149 @@ describe('SDK Feature: Constraint Budget (#49)', () => { }); }); + describe('PII scrubbing (postToolUse)', () => { + beforeEach(() => { + pipeline = new HookPipeline({ scrubPii: true }); + }); + + it('redacts email addresses from string results', async () => { + const ctx: PostToolUseContext = { + toolName: 'bash', + arguments: { command: 'git log' }, + result: 'Author: Edie Finneran ', + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPostToolHooks(ctx); + expect(result.result).toBe('Author: Edie Finneran <[EMAIL_REDACTED]>'); + expect(result.result).not.toContain('edie@example.com'); + }); + + it('redacts multiple email addresses in a single string', async () => { + const ctx: PostToolUseContext = { + toolName: 'bash', + arguments: { command: 'cat AUTHORS' }, + result: 'edie@example.com, fenster@corp.io, hockney@test.org', + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPostToolHooks(ctx); + expect(result.result).toBe('[EMAIL_REDACTED], [EMAIL_REDACTED], [EMAIL_REDACTED]'); + }); + + it('leaves strings without emails unchanged', async () => { + const ctx: PostToolUseContext = { + toolName: 'bash', + arguments: { command: 'echo hello' }, + result: 'Hello world — no PII here', + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPostToolHooks(ctx); + expect(result.result).toBe('Hello world — no PII here'); + }); + + it('scrubs emails from nested object results', async () => { + const ctx: PostToolUseContext = { + toolName: 'read', + arguments: { path: 'config.json' }, + result: { + author: 'edie@example.com', + metadata: { + reviewer: 'fenster@corp.io', + notes: 'Reviewed by fenster@corp.io on Monday', + }, + }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPostToolHooks(ctx); + const scrubbed = result.result as any; + expect(scrubbed.author).toBe('[EMAIL_REDACTED]'); + expect(scrubbed.metadata.reviewer).toBe('[EMAIL_REDACTED]'); + expect(scrubbed.metadata.notes).toBe('Reviewed by [EMAIL_REDACTED] on Monday'); + }); + + it('scrubs emails from arrays', async () => { + const ctx: PostToolUseContext = { + toolName: 'read', + arguments: { path: 'team.json' }, + result: ['edie@example.com', 'fenster@corp.io', 'no-email-here'], + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPostToolHooks(ctx); + const scrubbed = result.result as string[]; + expect(scrubbed[0]).toBe('[EMAIL_REDACTED]'); + expect(scrubbed[1]).toBe('[EMAIL_REDACTED]'); + expect(scrubbed[2]).toBe('no-email-here'); + }); + + it('handles non-string/non-object results (numbers, null)', async () => { + const numCtx: PostToolUseContext = { + toolName: 'bash', + arguments: { command: 'wc -l' }, + result: 42, + agentName: 'edie', + sessionId: 's1', + }; + const numResult = await pipeline.runPostToolHooks(numCtx); + expect(numResult.result).toBe(42); + + const nullCtx: PostToolUseContext = { + ...numCtx, + result: null, + }; + const nullResult = await pipeline.runPostToolHooks(nullCtx); + expect(nullResult.result).toBeNull(); + }); + + it('does not scrub when scrubPii is disabled', async () => { + const noScrubPipeline = new HookPipeline({ scrubPii: false }); + const ctx: PostToolUseContext = { + toolName: 'bash', + arguments: { command: 'git log' }, + result: 'Author: edie@example.com', + agentName: 'edie', + sessionId: 's1', + }; + + const result = await noScrubPipeline.runPostToolHooks(ctx); + expect(result.result).toBe('Author: edie@example.com'); + }); + + it('scrubs deeply nested objects with mixed types', async () => { + const ctx: PostToolUseContext = { + toolName: 'read', + arguments: { path: 'data.json' }, + result: { + users: [ + { name: 'Edie', email: 'edie@example.com', age: 30 }, + { name: 'Fenster', email: 'fenster@corp.io', active: true }, + ], + count: 2, + note: null, + }, + agentName: 'edie', + sessionId: 's1', + }; + + const result = await pipeline.runPostToolHooks(ctx); + const scrubbed = result.result as any; + expect(scrubbed.users[0].email).toBe('[EMAIL_REDACTED]'); + expect(scrubbed.users[0].name).toBe('Edie'); + expect(scrubbed.users[0].age).toBe(30); + expect(scrubbed.users[1].email).toBe('[EMAIL_REDACTED]'); + expect(scrubbed.users[1].active).toBe(true); + expect(scrubbed.count).toBe(2); + }); + }); + describe('defineHooks() builder integration', () => { it('defineHooks() accepts and validates constraint fields', () => { const hooks = defineHooks({ From 0a3604706e88f892ea337b550ddb260fb0abc5fb Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 14:15:57 +1000 Subject: [PATCH 5/8] docs: add 50-feature SDK test matrix audit (#347/#341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive audit mapping all 50 SDK features to existing test files: - 32 verified (HIGH confidence) — 64% - 11 partial (MEDIUM confidence) — 22% - 7 gaps identified (LOW confidence) — 14% - Combined verified+partial: 43/50 (86%) Identifies specific gap areas: drop-box pattern, directive capture, eager execution, PRD intake, lead decomposition, scribe git commits, and MCP integration. Recommends path to reach 90%+ threshold. --- .changeset/sdk-parity-batch2-tests.md | 2 +- test/SDK-FEATURE-TEST-MATRIX.md | 179 ++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 test/SDK-FEATURE-TEST-MATRIX.md diff --git a/.changeset/sdk-parity-batch2-tests.md b/.changeset/sdk-parity-batch2-tests.md index c80c1d082..a31b59fa6 100644 --- a/.changeset/sdk-parity-batch2-tests.md +++ b/.changeset/sdk-parity-batch2-tests.md @@ -2,4 +2,4 @@ "@bradygaster/squad-sdk": patch --- -Add SDK feature parity batch 2 tests (#347/#341) — 56+ tests covering manual ceremonies, ceremony cooldown, human team members, constraint budget enforcement (rate limiter, file-write guards, shell restrictions, PII scrubbing), and multi-agent artifact coordination +Add SDK feature parity batch 2 — 64 tests covering manual ceremonies, ceremony cooldown, human team members, constraint budget enforcement (rate limiter, file-write guards, shell restrictions, PII scrubbing), multi-agent artifact coordination, and builder validation edge cases. Includes full 50-feature test matrix audit (32 verified, 11 partial, 7 gaps identified — 86% coverage toward #347/#341 quality gate). diff --git a/test/SDK-FEATURE-TEST-MATRIX.md b/test/SDK-FEATURE-TEST-MATRIX.md new file mode 100644 index 000000000..a4debb1c2 --- /dev/null +++ b/test/SDK-FEATURE-TEST-MATRIX.md @@ -0,0 +1,179 @@ +# SDK Feature Test Matrix + +> Audit of test coverage across all 50 SDK features for issue [#347](https://github.com/bradygaster/squad/issues/347) / [#341](https://github.com/bradygaster/squad/issues/341). +> +> **Generated from:** `upstream/dev` (commit `0d86da3`) + PR #425 (64 additional tests) + +## Summary + +| Status | Count | Percentage | +|--------|-------|------------| +| ✅ Verified (HIGH confidence) | 32 | 64% | +| 🟡 Partial (MEDIUM confidence) | 11 | 22% | +| ❌ No coverage (LOW confidence) | 7 | 14% | +| **Total** | **50** | — | + +**Verified + Partial = 43/50 (86%)** — approaching the 90%+ threshold from the PRD. + +--- + +## Full Matrix + +### Init & Setup (Features 1–6) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 1 | `squad init` creates `.squad/` structure | ✅ | init.test.ts, cli/init.test.ts | "initSquad", "CLI: init command" | ~52 | +| 2 | `squad init --sdk` creates `squad.config.ts` | ✅ | init-sdk.test.ts, cli/init.test.ts | "squad init --sdk flag" | ~32 | +| 3 | Team casting / universe selection | ✅ | casting.test.ts, cast-parser.test.ts | "CastingEngine", "castTeam — usual-suspects / oceans-eleven" | ~45 | +| 4 | Casting state files | ✅ | casting-history.test.ts | "CastingHistory", "recordCast", "serialization" | ~24 | +| 5 | `.gitattributes` merge drivers | ✅ | init.test.ts, cli/init.test.ts | "should create .gitattributes for merge drivers" | ~3 | +| 6 | Config stays in sync with team.md | ✅ | agent-doc.test.ts, config-integration.test.ts, config.test.ts | "syncDocToConfig", "syncConfigToDoc", "detectDrift" | ~117 | + +### Team Management (Features 7–11) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 7 | Adding team members (markdown) | ✅ | agent-doc.test.ts | "syncDocToConfig" — create stub agent, update tools | ~35 | +| 8 | Adding members updates config | ✅ | agent-doc.test.ts, config-integration.test.ts | "syncConfigToDoc", integration pipeline | ~95 | +| 9 | Removing team members | 🟡 | agent-doc.test.ts | Handles empty/missing sections; no explicit removal test | ~35 | +| 10 | Removing members updates config | 🟡 | agent-doc.test.ts, config-integration.test.ts | Inferred from sync tests, not explicitly tested | ~95 | +| 11 | Plugin marketplace check | ✅ | marketplace.test.ts, marketplace-advanced.test.ts, marketplace-integration.test.ts | "validateManifest", "searchMarketplace", "validateEntry" | ~159 | + +### Spawning (Features 12–17) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 12 | Agent spawn with charter inline | ✅ | shell.test.ts, coordinator.test.ts, agent-session-manager.test.ts | "Spawn infrastructure", "spawn()", "loadAgentCharter" | ~115 | +| 13 | Model selection (4-layer) | ✅ | agents.test.ts, models.test.ts, model-fallback.test.ts | "ModelRegistry", "getFallbackChain", "Cross-tier fallback" | ~58 | +| 14 | Response mode selection | ✅ | response-tiers.test.ts, direct-response.test.ts | "selectResponseTier", "DirectResponseHandler" | ~60 | +| 15 | Parallel fan-out | ✅ | fan-out.test.ts, coordinator.test.ts | "spawnParallel", "aggregateSessionEvents" | ~55 | +| 16 | Drop-box pattern | ❌ | — | No dedicated tests found | 0 | +| 17 | Skill-aware routing | 🟡 | coordinator-routing.test.ts, skills.test.ts | "matchSkills", routing integration | ~56 | + +### Routing (Features 18–21) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 18 | Routing table | ✅ | routing.test.ts, coordinator-routing.test.ts | "parseRoutingMarkdown", "matchRoute", "matchIssueLabels" | ~45 | +| 19 | Directive capture | ❌ | — | No dedicated tests found | 0 | +| 20 | Eager execution | ❌ | — | No dedicated tests found | 0 | +| 21 | Orchestration logging | 🟡 | otel-coordinator-traces.test.ts | "Coordinator routing tracing", "span hierarchy" | ~12 | + +### Issues (Features 22–25) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 22 | GitHub repo connection | ✅ | platform-adapter.test.ts | "detectPlatformFromUrl", "parseGitHubRemote" | ~92 | +| 23 | Issue triage | ✅ | ralph-triage.test.ts, platform-adapter.test.ts | "triageIssue()", "triage parity" | ~121 | +| 24 | Issue→PR lifecycle | 🟡 | platform-adapter.test.ts | "WorkItem type", "PlatformAdapter createWorkItem" | ~92 | +| 25 | Label routing | ✅ | streams.test.ts, routing.test.ts | "getSubSquadLabelFilter", "filterIssuesBySubSquad", "matchIssueLabels" | ~62 | + +### Ceremonies (Features 26–28) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 26 | Auto-triggered ceremonies | ✅ | builders.test.ts, sdk-feature-parity-batch2.test.ts | "defineCeremony()" — pr-merged & schedule triggers | ~12 | +| 27 | Manual ceremonies | ✅ | builders.test.ts, sdk-feature-parity-batch2.test.ts | "SDK Feature: Manual Ceremonies (#27)" | ~12 | +| 28 | Ceremony cooldown | ✅ | sdk-feature-parity-batch2.test.ts | "SDK Feature: Ceremony Cooldown (#28)" | ~4 | + +### Ralph (Features 29–32) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 29 | Ralph activation | 🟡 | ralph-board.test.ts, ralph-monitor.test.ts | "watch board reporting", "board state compatibility" | ~42 | +| 30 | Work-check cycle | 🟡 | ralph-monitor.test.ts, ralph-board.test.ts | "event handling", "healthCheck()", "getStatus()" | ~15 | +| 31 | Idle-watch mode | 🟡 | ralph-monitor.test.ts, cli/watch.test.ts | "getStatus()", "CLI: watch command" | ~10 | +| 32 | Watch mode | ✅ | cli/watch.test.ts, squad-observer.test.ts | "CLI: watch command", "SquadObserver" | ~20 | + +### PRD Mode (Features 33–35) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 33 | PRD intake | ❌ | — | No dedicated tests found | 0 | +| 34 | Lead decomposition | ❌ | — | No dedicated tests found | 0 | +| 35 | Work item routing | ✅ | coordinator-routing.test.ts, routing.test.ts | "Coordinator route()", routing rules compilation | ~45 | + +### Human & Copilot (Features 36–38) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 36 | Human team members | ✅ | sdk-feature-parity-batch2.test.ts, human-journeys.test.ts | "SDK Feature: Human Team Members (#36)", journey roster coverage | ~20 | +| 37 | @copilot member | ✅ | cli/copilot.test.ts, cli/copilot-bridge.test.ts | "CLI: copilot command", "CLI: copilot-bridge command" | ~14 | +| 38 | @copilot auto-assign | 🟡 | cli/copilot.test.ts, type-extensions.test.ts | "--auto-assign test", "ParsedAgent: autoAssign field" | ~4 | + +### Skills (Features 39–41) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 39 | Skill extraction | ✅ | cli/extract.test.ts, skills.test.ts | "CLI: extract command", "SkillRegistry" | ~32 | +| 40 | Skill confidence lifecycle | ✅ | sdk-feature-parity.test.ts, skills.test.ts | "SDK Feature: Skill Confidence Lifecycle", "matchSkills" | ~9 | +| 41 | Skills directory structure | ✅ | skill-source.test.ts, skills.test.ts | "LocalSkillSource", "SkillSourceRegistry" | ~26 | + +### Worktree & Git (Features 42–44) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 42 | Worktree awareness | ✅ | sdk-feature-parity.test.ts, dual-root-resolver.test.ts | "SDK Feature: Worktree Awareness", "resolveSquadPaths()" | ~21 | +| 43 | Scribe git commits | ❌ | — | No dedicated tests found | 0 | +| 44 | Merge drivers | 🟡 | init.test.ts | "initSquad" — merge drivers test | ~1 | + +### Reviewer (Features 45–46) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 45 | Reviewer lockout | ✅ | sdk-feature-parity.test.ts, sdk-feature-parity-batch2.test.ts | "SDK Feature: Reviewer Lockout", lockout integration | ~12 | +| 46 | Deadlock handling | 🟡 | sdk-feature-parity.test.ts | "SDK Feature: Deadlock Handling" | ~2 | + +### Platform (Features 47–50) + +| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | +|---|---------|--------|-------------|---------------------|-------| +| 47 | Client compatibility | ✅ | client.test.ts, adapter-client.test.ts, compat-v041.test.ts | "SquadClient", "SessionPool", compat tests | ~104 | +| 48 | MCP integration | ❌ | — | No dedicated tests found | 0 | +| 49 | Constraint budget | ✅ | sdk-feature-parity-batch2.test.ts, hooks.test.ts | "SDK Feature: Constraint Budget (#49)", hook pipeline tests | ~60 | +| 50 | Multi-agent artifact | ✅ | sdk-feature-parity-batch2.test.ts | "SDK Feature: Multi-Agent Artifact Coordination (#50)" | ~6 | + +--- + +## Gap Analysis + +### ❌ Untested Features (7) + +| # | Feature | Notes | +|---|---------|-------| +| 16 | Drop-box pattern | No implementation or tests found — may be a planned feature | +| 19 | Directive capture | No test coverage; may be embedded in routing logic | +| 20 | Eager execution | No test coverage; may be embedded in coordinator logic | +| 33 | PRD intake | No test coverage; PRD mode features may be runtime-only | +| 34 | Lead decomposition | No test coverage; requires investigation into implementation status | +| 43 | Scribe git commits | No dedicated tests; Scribe agent behavior is charter-driven | +| 48 | MCP integration | No dedicated tests; platform adapter tests cover adjacent ground | + +### 🟡 Partially Tested Features (11) + +| # | Feature | Gap | +|---|---------|-----| +| 9 | Removing team members | Sync tests exist but no explicit "remove member" scenario | +| 10 | Removing members updates config | Inferred from sync, not explicitly tested | +| 17 | Skill-aware routing | Skill matching tested, but routing-to-skill integration is implicit | +| 21 | Orchestration logging | OTel traces tested, but orchestration-specific logging is thin | +| 24 | Issue→PR lifecycle | Platform adapter types tested, but full lifecycle flow isn't | +| 29 | Ralph activation | Board/monitor tested, but activation trigger logic is thin | +| 30 | Work-check cycle | Monitor tested, but cycle timing/state transitions aren't | +| 31 | Idle-watch mode | Watch command tested, but idle-to-active transition isn't | +| 38 | @copilot auto-assign | Type exists, CLI flag exists, but assignment logic isn't tested | +| 44 | Merge drivers | Init creates `.gitattributes`, but driver behavior isn't tested | +| 46 | Deadlock handling | Feature parity test exists but only ~2 tests — thin coverage | + +--- + +## Recommendations to Reach 90%+ + +To move from 86% (43/50) to 90%+ (45/50), the most impactful additions would be: + +1. **Feature 48 (MCP integration)** — Add tests for MCP protocol handling if implementation exists +2. **Feature 43 (Scribe git commits)** — Add tests for Scribe's commit formatting/behavior +3. **Feature 16 (Drop-box pattern)** — Determine if this is implemented; if so, add tests + +Features 19, 20, 33, 34 may be runtime/behavioral features that are difficult to unit test, or may not yet have SDK-level implementations. From fec96bd6a9f4869e962420e05e5ef7f38e9b05c3 Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 14:32:56 +1000 Subject: [PATCH 6/8] chore: remove SDK-FEATURE-TEST-MATRIX.md Duplicated and partially contradicted the existing feature parity matrix in issue #341. The unit test coverage audit belongs in issue comments, not as a repo file competing with the authoritative matrix. --- test/SDK-FEATURE-TEST-MATRIX.md | 179 -------------------------------- 1 file changed, 179 deletions(-) delete mode 100644 test/SDK-FEATURE-TEST-MATRIX.md diff --git a/test/SDK-FEATURE-TEST-MATRIX.md b/test/SDK-FEATURE-TEST-MATRIX.md deleted file mode 100644 index a4debb1c2..000000000 --- a/test/SDK-FEATURE-TEST-MATRIX.md +++ /dev/null @@ -1,179 +0,0 @@ -# SDK Feature Test Matrix - -> Audit of test coverage across all 50 SDK features for issue [#347](https://github.com/bradygaster/squad/issues/347) / [#341](https://github.com/bradygaster/squad/issues/341). -> -> **Generated from:** `upstream/dev` (commit `0d86da3`) + PR #425 (64 additional tests) - -## Summary - -| Status | Count | Percentage | -|--------|-------|------------| -| ✅ Verified (HIGH confidence) | 32 | 64% | -| 🟡 Partial (MEDIUM confidence) | 11 | 22% | -| ❌ No coverage (LOW confidence) | 7 | 14% | -| **Total** | **50** | — | - -**Verified + Partial = 43/50 (86%)** — approaching the 90%+ threshold from the PRD. - ---- - -## Full Matrix - -### Init & Setup (Features 1–6) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 1 | `squad init` creates `.squad/` structure | ✅ | init.test.ts, cli/init.test.ts | "initSquad", "CLI: init command" | ~52 | -| 2 | `squad init --sdk` creates `squad.config.ts` | ✅ | init-sdk.test.ts, cli/init.test.ts | "squad init --sdk flag" | ~32 | -| 3 | Team casting / universe selection | ✅ | casting.test.ts, cast-parser.test.ts | "CastingEngine", "castTeam — usual-suspects / oceans-eleven" | ~45 | -| 4 | Casting state files | ✅ | casting-history.test.ts | "CastingHistory", "recordCast", "serialization" | ~24 | -| 5 | `.gitattributes` merge drivers | ✅ | init.test.ts, cli/init.test.ts | "should create .gitattributes for merge drivers" | ~3 | -| 6 | Config stays in sync with team.md | ✅ | agent-doc.test.ts, config-integration.test.ts, config.test.ts | "syncDocToConfig", "syncConfigToDoc", "detectDrift" | ~117 | - -### Team Management (Features 7–11) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 7 | Adding team members (markdown) | ✅ | agent-doc.test.ts | "syncDocToConfig" — create stub agent, update tools | ~35 | -| 8 | Adding members updates config | ✅ | agent-doc.test.ts, config-integration.test.ts | "syncConfigToDoc", integration pipeline | ~95 | -| 9 | Removing team members | 🟡 | agent-doc.test.ts | Handles empty/missing sections; no explicit removal test | ~35 | -| 10 | Removing members updates config | 🟡 | agent-doc.test.ts, config-integration.test.ts | Inferred from sync tests, not explicitly tested | ~95 | -| 11 | Plugin marketplace check | ✅ | marketplace.test.ts, marketplace-advanced.test.ts, marketplace-integration.test.ts | "validateManifest", "searchMarketplace", "validateEntry" | ~159 | - -### Spawning (Features 12–17) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 12 | Agent spawn with charter inline | ✅ | shell.test.ts, coordinator.test.ts, agent-session-manager.test.ts | "Spawn infrastructure", "spawn()", "loadAgentCharter" | ~115 | -| 13 | Model selection (4-layer) | ✅ | agents.test.ts, models.test.ts, model-fallback.test.ts | "ModelRegistry", "getFallbackChain", "Cross-tier fallback" | ~58 | -| 14 | Response mode selection | ✅ | response-tiers.test.ts, direct-response.test.ts | "selectResponseTier", "DirectResponseHandler" | ~60 | -| 15 | Parallel fan-out | ✅ | fan-out.test.ts, coordinator.test.ts | "spawnParallel", "aggregateSessionEvents" | ~55 | -| 16 | Drop-box pattern | ❌ | — | No dedicated tests found | 0 | -| 17 | Skill-aware routing | 🟡 | coordinator-routing.test.ts, skills.test.ts | "matchSkills", routing integration | ~56 | - -### Routing (Features 18–21) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 18 | Routing table | ✅ | routing.test.ts, coordinator-routing.test.ts | "parseRoutingMarkdown", "matchRoute", "matchIssueLabels" | ~45 | -| 19 | Directive capture | ❌ | — | No dedicated tests found | 0 | -| 20 | Eager execution | ❌ | — | No dedicated tests found | 0 | -| 21 | Orchestration logging | 🟡 | otel-coordinator-traces.test.ts | "Coordinator routing tracing", "span hierarchy" | ~12 | - -### Issues (Features 22–25) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 22 | GitHub repo connection | ✅ | platform-adapter.test.ts | "detectPlatformFromUrl", "parseGitHubRemote" | ~92 | -| 23 | Issue triage | ✅ | ralph-triage.test.ts, platform-adapter.test.ts | "triageIssue()", "triage parity" | ~121 | -| 24 | Issue→PR lifecycle | 🟡 | platform-adapter.test.ts | "WorkItem type", "PlatformAdapter createWorkItem" | ~92 | -| 25 | Label routing | ✅ | streams.test.ts, routing.test.ts | "getSubSquadLabelFilter", "filterIssuesBySubSquad", "matchIssueLabels" | ~62 | - -### Ceremonies (Features 26–28) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 26 | Auto-triggered ceremonies | ✅ | builders.test.ts, sdk-feature-parity-batch2.test.ts | "defineCeremony()" — pr-merged & schedule triggers | ~12 | -| 27 | Manual ceremonies | ✅ | builders.test.ts, sdk-feature-parity-batch2.test.ts | "SDK Feature: Manual Ceremonies (#27)" | ~12 | -| 28 | Ceremony cooldown | ✅ | sdk-feature-parity-batch2.test.ts | "SDK Feature: Ceremony Cooldown (#28)" | ~4 | - -### Ralph (Features 29–32) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 29 | Ralph activation | 🟡 | ralph-board.test.ts, ralph-monitor.test.ts | "watch board reporting", "board state compatibility" | ~42 | -| 30 | Work-check cycle | 🟡 | ralph-monitor.test.ts, ralph-board.test.ts | "event handling", "healthCheck()", "getStatus()" | ~15 | -| 31 | Idle-watch mode | 🟡 | ralph-monitor.test.ts, cli/watch.test.ts | "getStatus()", "CLI: watch command" | ~10 | -| 32 | Watch mode | ✅ | cli/watch.test.ts, squad-observer.test.ts | "CLI: watch command", "SquadObserver" | ~20 | - -### PRD Mode (Features 33–35) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 33 | PRD intake | ❌ | — | No dedicated tests found | 0 | -| 34 | Lead decomposition | ❌ | — | No dedicated tests found | 0 | -| 35 | Work item routing | ✅ | coordinator-routing.test.ts, routing.test.ts | "Coordinator route()", routing rules compilation | ~45 | - -### Human & Copilot (Features 36–38) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 36 | Human team members | ✅ | sdk-feature-parity-batch2.test.ts, human-journeys.test.ts | "SDK Feature: Human Team Members (#36)", journey roster coverage | ~20 | -| 37 | @copilot member | ✅ | cli/copilot.test.ts, cli/copilot-bridge.test.ts | "CLI: copilot command", "CLI: copilot-bridge command" | ~14 | -| 38 | @copilot auto-assign | 🟡 | cli/copilot.test.ts, type-extensions.test.ts | "--auto-assign test", "ParsedAgent: autoAssign field" | ~4 | - -### Skills (Features 39–41) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 39 | Skill extraction | ✅ | cli/extract.test.ts, skills.test.ts | "CLI: extract command", "SkillRegistry" | ~32 | -| 40 | Skill confidence lifecycle | ✅ | sdk-feature-parity.test.ts, skills.test.ts | "SDK Feature: Skill Confidence Lifecycle", "matchSkills" | ~9 | -| 41 | Skills directory structure | ✅ | skill-source.test.ts, skills.test.ts | "LocalSkillSource", "SkillSourceRegistry" | ~26 | - -### Worktree & Git (Features 42–44) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 42 | Worktree awareness | ✅ | sdk-feature-parity.test.ts, dual-root-resolver.test.ts | "SDK Feature: Worktree Awareness", "resolveSquadPaths()" | ~21 | -| 43 | Scribe git commits | ❌ | — | No dedicated tests found | 0 | -| 44 | Merge drivers | 🟡 | init.test.ts | "initSquad" — merge drivers test | ~1 | - -### Reviewer (Features 45–46) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 45 | Reviewer lockout | ✅ | sdk-feature-parity.test.ts, sdk-feature-parity-batch2.test.ts | "SDK Feature: Reviewer Lockout", lockout integration | ~12 | -| 46 | Deadlock handling | 🟡 | sdk-feature-parity.test.ts | "SDK Feature: Deadlock Handling" | ~2 | - -### Platform (Features 47–50) - -| # | Feature | Status | Test File(s) | Key Describe Blocks | Tests | -|---|---------|--------|-------------|---------------------|-------| -| 47 | Client compatibility | ✅ | client.test.ts, adapter-client.test.ts, compat-v041.test.ts | "SquadClient", "SessionPool", compat tests | ~104 | -| 48 | MCP integration | ❌ | — | No dedicated tests found | 0 | -| 49 | Constraint budget | ✅ | sdk-feature-parity-batch2.test.ts, hooks.test.ts | "SDK Feature: Constraint Budget (#49)", hook pipeline tests | ~60 | -| 50 | Multi-agent artifact | ✅ | sdk-feature-parity-batch2.test.ts | "SDK Feature: Multi-Agent Artifact Coordination (#50)" | ~6 | - ---- - -## Gap Analysis - -### ❌ Untested Features (7) - -| # | Feature | Notes | -|---|---------|-------| -| 16 | Drop-box pattern | No implementation or tests found — may be a planned feature | -| 19 | Directive capture | No test coverage; may be embedded in routing logic | -| 20 | Eager execution | No test coverage; may be embedded in coordinator logic | -| 33 | PRD intake | No test coverage; PRD mode features may be runtime-only | -| 34 | Lead decomposition | No test coverage; requires investigation into implementation status | -| 43 | Scribe git commits | No dedicated tests; Scribe agent behavior is charter-driven | -| 48 | MCP integration | No dedicated tests; platform adapter tests cover adjacent ground | - -### 🟡 Partially Tested Features (11) - -| # | Feature | Gap | -|---|---------|-----| -| 9 | Removing team members | Sync tests exist but no explicit "remove member" scenario | -| 10 | Removing members updates config | Inferred from sync, not explicitly tested | -| 17 | Skill-aware routing | Skill matching tested, but routing-to-skill integration is implicit | -| 21 | Orchestration logging | OTel traces tested, but orchestration-specific logging is thin | -| 24 | Issue→PR lifecycle | Platform adapter types tested, but full lifecycle flow isn't | -| 29 | Ralph activation | Board/monitor tested, but activation trigger logic is thin | -| 30 | Work-check cycle | Monitor tested, but cycle timing/state transitions aren't | -| 31 | Idle-watch mode | Watch command tested, but idle-to-active transition isn't | -| 38 | @copilot auto-assign | Type exists, CLI flag exists, but assignment logic isn't tested | -| 44 | Merge drivers | Init creates `.gitattributes`, but driver behavior isn't tested | -| 46 | Deadlock handling | Feature parity test exists but only ~2 tests — thin coverage | - ---- - -## Recommendations to Reach 90%+ - -To move from 86% (43/50) to 90%+ (45/50), the most impactful additions would be: - -1. **Feature 48 (MCP integration)** — Add tests for MCP protocol handling if implementation exists -2. **Feature 43 (Scribe git commits)** — Add tests for Scribe's commit formatting/behavior -3. **Feature 16 (Drop-box pattern)** — Determine if this is implemented; if so, add tests - -Features 19, 20, 33, 34 may be runtime/behavioral features that are difficult to unit test, or may not yet have SDK-level implementations. From a8020ce4b695577cefabce94eb88b35d0d6b3d4b Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 14:33:12 +1000 Subject: [PATCH 7/8] =?UTF-8?q?chore:=20fix=20changeset=20=E2=80=94=20remo?= =?UTF-8?q?ve=20matrix=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/sdk-parity-batch2-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/sdk-parity-batch2-tests.md b/.changeset/sdk-parity-batch2-tests.md index a31b59fa6..5a06207d4 100644 --- a/.changeset/sdk-parity-batch2-tests.md +++ b/.changeset/sdk-parity-batch2-tests.md @@ -2,4 +2,4 @@ "@bradygaster/squad-sdk": patch --- -Add SDK feature parity batch 2 — 64 tests covering manual ceremonies, ceremony cooldown, human team members, constraint budget enforcement (rate limiter, file-write guards, shell restrictions, PII scrubbing), multi-agent artifact coordination, and builder validation edge cases. Includes full 50-feature test matrix audit (32 verified, 11 partial, 7 gaps identified — 86% coverage toward #347/#341 quality gate). +Add SDK feature parity batch 2 — 64 tests covering manual ceremonies (#27), ceremony cooldown (#28), human team members (#36), constraint budget enforcement (#49 — rate limiter, file-write guards, shell restrictions, PII scrubbing), multi-agent artifact coordination (#50), and builder validation edge cases. From 6c7b959353668c67b095c91f236ef51a77b9b085 Mon Sep 17 00:00:00 2001 From: williamhallatt Date: Mon, 16 Mar 2026 14:50:23 +1000 Subject: [PATCH 8/8] test: replace cast names with test fixtures per Product Isolation Rule Batch 1 (sdk-feature-parity.test.ts) correctly uses test-agent-1/test-agent-2. Batch 2 was using real cast names (edie, fenster, hockney). Updated to use test-agent-{1,2,3} and test-specific email addresses to follow the same convention and the Product Isolation Rule. --- test/sdk-feature-parity-batch2.test.ts | 230 ++++++++++++------------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/test/sdk-feature-parity-batch2.test.ts b/test/sdk-feature-parity-batch2.test.ts index 0b5cb6738..1edcec2a0 100644 --- a/test/sdk-feature-parity-batch2.test.ts +++ b/test/sdk-feature-parity-batch2.test.ts @@ -48,7 +48,7 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { const ceremony = defineCeremony({ name: 'design-review', trigger: 'manual', - participants: ['edie', 'fenster', 'hockney'], + participants: ['test-agent-1', 'test-agent-2', 'test-agent-3'], agenda: 'Review architecture decisions and trade-offs', }); @@ -62,7 +62,7 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { const ceremony = defineCeremony({ name: 'post-merge-review', trigger: 'pr-merged', - participants: ['hockney'], + participants: ['test-agent-3'], agenda: 'Run regression tests and update coverage report', }); @@ -74,7 +74,7 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { name: 'standup', trigger: 'schedule', schedule: '0 9 * * 1-5', - participants: ['edie', 'fenster', 'hockney'], + participants: ['test-agent-1', 'test-agent-2', 'test-agent-3'], agenda: 'Yesterday / Today / Blockers', }); @@ -86,7 +86,7 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { const ceremony = defineCeremony({ name: 'retrospective', trigger: 'manual', - participants: ['edie', 'fenster'], + participants: ['test-agent-1', 'test-agent-2'], agenda: 'What went well? What to improve?', hooks: ['pre-retro-gather-metrics', 'post-retro-create-issues'], }); @@ -98,22 +98,22 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { it('defineSquad() composes multiple ceremonies including manual', () => { const config = defineSquad({ - team: defineTeam({ name: 'Alpha', members: ['edie', 'hockney'] }), + team: defineTeam({ name: 'Alpha', members: ['test-agent-1', 'test-agent-3'] }), agents: [ - defineAgent({ name: 'edie', role: 'TypeScript Engineer' }), - defineAgent({ name: 'hockney', role: 'Tester' }), + defineAgent({ name: 'test-agent-1', role: 'TypeScript Engineer' }), + defineAgent({ name: 'test-agent-3', role: 'Tester' }), ], ceremonies: [ defineCeremony({ name: 'standup', trigger: 'schedule', schedule: '0 9 * * 1-5', - participants: ['edie', 'hockney'], + participants: ['test-agent-1', 'test-agent-3'], }), defineCeremony({ name: 'design-review', trigger: 'manual', - participants: ['edie'], + participants: ['test-agent-1'], agenda: 'Review pending architecture decisions', }), ], @@ -143,7 +143,7 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { it('defineCeremony() throws on non-array participants', () => { expect(() => - defineCeremony({ name: 'bad', participants: 'edie' } as any), + defineCeremony({ name: 'bad', participants: 'test-agent-1' } as any), ).toThrow(BuilderValidationError); }); @@ -167,17 +167,17 @@ describe('SDK Feature: Manual Ceremonies (#27)', () => { it('ceremony participants can reference any agent in the squad', () => { const config = defineSquad({ - team: defineTeam({ name: 'Squad', members: ['edie', 'fenster', 'hockney'] }), + team: defineTeam({ name: 'Squad', members: ['test-agent-1', 'test-agent-2', 'test-agent-3'] }), agents: [ - defineAgent({ name: 'edie', role: 'Lead' }), - defineAgent({ name: 'fenster', role: 'Tester' }), - defineAgent({ name: 'hockney', role: 'Developer' }), + defineAgent({ name: 'test-agent-1', role: 'Lead' }), + defineAgent({ name: 'test-agent-2', role: 'Tester' }), + defineAgent({ name: 'test-agent-3', role: 'Developer' }), ], ceremonies: [ defineCeremony({ name: 'full-team-sync', trigger: 'manual', - participants: ['edie', 'fenster', 'hockney'], + participants: ['test-agent-1', 'test-agent-2', 'test-agent-3'], agenda: 'Cross-team sync', }), ], @@ -202,7 +202,7 @@ describe('SDK Feature: Ceremony Cooldown (#28)', () => { name: 'standup', trigger: 'schedule', schedule: '0 9 * * 1-5', - participants: ['edie'], + participants: ['test-agent-1'], agenda: 'Yesterday / Today / Blockers', }); @@ -215,7 +215,7 @@ describe('SDK Feature: Ceremony Cooldown (#28)', () => { const ceremony = defineCeremony({ name: 'ad-hoc-review', trigger: 'manual', - participants: ['edie'], + participants: ['test-agent-1'], }); expect(ceremony.schedule).toBeUndefined(); @@ -236,14 +236,14 @@ describe('SDK Feature: Ceremony Cooldown (#28)', () => { it('defineSquad() validates ceremonies with schedules in full config', () => { const config = defineSquad({ - team: defineTeam({ name: 'CooldownTeam', members: ['edie'] }), - agents: [defineAgent({ name: 'edie', role: 'Engineer' })], + team: defineTeam({ name: 'CooldownTeam', members: ['test-agent-1'] }), + agents: [defineAgent({ name: 'test-agent-1', role: 'Engineer' })], ceremonies: [ defineCeremony({ name: 'daily-standup', trigger: 'schedule', schedule: '0 9 * * 1-5', - participants: ['edie'], + participants: ['test-agent-1'], agenda: 'Daily sync', }), ], @@ -259,27 +259,27 @@ describe('SDK Feature: Ceremony Cooldown (#28)', () => { describe('SDK Feature: Human Team Members (#36)', () => { it('defineAgent() accepts status: "active" for active agents', () => { - const agent = defineAgent({ name: 'edie', role: 'Engineer', status: 'active' }); + const agent = defineAgent({ name: 'test-agent-1', role: 'Engineer', status: 'active' }); expect(agent.status).toBe('active'); }); it('defineAgent() accepts status: "inactive" for paused agents', () => { - const agent = defineAgent({ name: 'alice', role: 'Product Lead', status: 'inactive' }); + const agent = defineAgent({ name: 'test-agent-4', role: 'Product Lead', status: 'inactive' }); expect(agent.status).toBe('inactive'); }); it('defineAgent() accepts status: "retired" for removed agents', () => { - const agent = defineAgent({ name: 'old-bot', role: 'Archivist', status: 'retired' }); + const agent = defineAgent({ name: 'test-agent-retired', role: 'Archivist', status: 'retired' }); expect(agent.status).toBe('retired'); }); it('squad config can include agents with mixed statuses', () => { const config = defineSquad({ - team: defineTeam({ name: 'Mixed', members: ['edie', 'alice', 'old-bot'] }), + team: defineTeam({ name: 'Mixed', members: ['test-agent-1', 'test-agent-4', 'test-agent-retired'] }), agents: [ - defineAgent({ name: 'edie', role: 'Engineer', status: 'active' }), - defineAgent({ name: 'alice', role: 'Product Lead', status: 'inactive' }), - defineAgent({ name: 'old-bot', role: 'Archivist', status: 'retired' }), + defineAgent({ name: 'test-agent-1', role: 'Engineer', status: 'active' }), + defineAgent({ name: 'test-agent-4', role: 'Product Lead', status: 'inactive' }), + defineAgent({ name: 'test-agent-retired', role: 'Archivist', status: 'retired' }), ], }); @@ -293,7 +293,7 @@ describe('SDK Feature: Human Team Members (#36)', () => { }); it('agent without status defaults to undefined (implicit active)', () => { - const agent = defineAgent({ name: 'fenster', role: 'Tester' }); + const agent = defineAgent({ name: 'test-agent-2', role: 'Tester' }); expect(agent.status).toBeUndefined(); }); @@ -314,13 +314,13 @@ describe('SDK Feature: Human Team Members (#36)', () => { it('defineAgent() throws on empty role', () => { expect(() => - defineAgent({ name: 'edie', role: '' }), + defineAgent({ name: 'test-agent-1', role: '' }), ).toThrow(BuilderValidationError); }); it('defineAgent() throws on non-string status', () => { expect(() => - defineAgent({ name: 'edie', role: 'Engineer', status: true } as any), + defineAgent({ name: 'test-agent-1', role: 'Engineer', status: true } as any), ).toThrow(BuilderValidationError); }); @@ -332,42 +332,42 @@ describe('SDK Feature: Human Team Members (#36)', () => { it('routing rules can reference agents regardless of status', () => { const config = defineSquad({ - team: defineTeam({ name: 'Routed', members: ['edie', 'hockney'] }), + team: defineTeam({ name: 'Routed', members: ['test-agent-1', 'test-agent-3'] }), agents: [ - defineAgent({ name: 'edie', role: 'Engineer', status: 'active' }), - defineAgent({ name: 'hockney', role: 'Tester', status: 'inactive' }), + defineAgent({ name: 'test-agent-1', role: 'Engineer', status: 'active' }), + defineAgent({ name: 'test-agent-3', role: 'Tester', status: 'inactive' }), ], routing: defineRouting({ rules: [ - { pattern: 'test-*', agents: ['hockney'] }, - { pattern: 'feature-*', agents: ['edie'] }, + { pattern: 'test-*', agents: ['test-agent-3'] }, + { pattern: 'feature-*', agents: ['test-agent-1'] }, ], - defaultAgent: 'edie', + defaultAgent: 'test-agent-1', }), }); // Routing rules exist for both active and inactive agents expect(config.routing!.rules).toHaveLength(2); const testRule = config.routing!.rules.find(r => r.pattern === 'test-*'); - expect(testRule!.agents).toContain('hockney'); + expect(testRule!.agents).toContain('test-agent-3'); }); it('team members list can include descriptive roles for human oversight', () => { const config = defineSquad({ team: defineTeam({ name: 'HumanLed', - members: ['edie', 'fenster', 'project-manager'], + members: ['test-agent-1', 'test-agent-2', 'test-pm'], description: 'Team with human oversight', }), agents: [ - defineAgent({ name: 'edie', role: 'Engineer' }), - defineAgent({ name: 'fenster', role: 'Tester' }), - defineAgent({ name: 'project-manager', role: 'Product Lead', status: 'inactive' }), + defineAgent({ name: 'test-agent-1', role: 'Engineer' }), + defineAgent({ name: 'test-agent-2', role: 'Tester' }), + defineAgent({ name: 'test-pm', role: 'Product Lead', status: 'inactive' }), ], }); - expect(config.team.members).toContain('project-manager'); - const pm = config.agents.find(a => a.name === 'project-manager'); + expect(config.team.members).toContain('test-pm'); + const pm = config.agents.find(a => a.name === 'test-pm'); expect(pm!.role).toBe('Product Lead'); expect(pm!.status).toBe('inactive'); }); @@ -389,7 +389,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'ask_user', arguments: { question: 'Which DB?' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 'session-1', }; @@ -401,7 +401,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'ask_user', arguments: { question: 'Question?' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 'session-1', }; @@ -421,7 +421,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const session1: PreToolUseContext = { toolName: 'ask_user', arguments: { question: 'Q?' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 'session-1', }; const session2: PreToolUseContext = { @@ -446,7 +446,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'edit', arguments: { path: 'src/foo.ts' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 'session-1', }; @@ -466,7 +466,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'edit', arguments: { path: 'src/components/auth.ts' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -478,7 +478,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'create', arguments: { path: 'test/auth.test.ts' }, - agentName: 'fenster', + agentName: 'test-agent-2', sessionId: 's1', }; @@ -490,7 +490,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'edit', arguments: { path: 'package.json' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -502,8 +502,8 @@ describe('SDK Feature: Constraint Budget (#49)', () => { it('allows .squad/ path writes', async () => { const ctx: PreToolUseContext = { toolName: 'create', - arguments: { path: '.squad/agents/edie/charter.md' }, - agentName: 'edie', + arguments: { path: '.squad/agents/test-agent-1/charter.md' }, + agentName: 'test-agent-1', sessionId: 's1', }; @@ -515,7 +515,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'read', arguments: { path: '/etc/passwd' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -535,7 +535,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'bash', arguments: { command: 'rm -rf /important/data' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -548,7 +548,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'bash', arguments: { command: 'npm test' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -560,7 +560,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PreToolUseContext = { toolName: 'bash', arguments: { command: "psql -c 'DROP TABLE users;'" }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -582,7 +582,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const write = await pipeline.runPreToolHooks({ toolName: 'edit', arguments: { path: 'src/app.ts' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }); expect(write.action).toBe('allow'); @@ -591,7 +591,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const blocked = await pipeline.runPreToolHooks({ toolName: 'edit', arguments: { path: 'dist/bundle.js' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }); expect(blocked.action).toBe('block'); @@ -600,7 +600,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ask1 = await pipeline.runPreToolHooks({ toolName: 'ask_user', arguments: { question: 'Q1?' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }); expect(ask1.action).toBe('allow'); @@ -616,22 +616,22 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PostToolUseContext = { toolName: 'bash', arguments: { command: 'git log' }, - result: 'Author: Edie Finneran ', - agentName: 'edie', + result: 'Author: Test User ', + agentName: 'test-agent-1', sessionId: 's1', }; const result = await pipeline.runPostToolHooks(ctx); - expect(result.result).toBe('Author: Edie Finneran <[EMAIL_REDACTED]>'); - expect(result.result).not.toContain('edie@example.com'); + expect(result.result).toBe('Author: Test User <[EMAIL_REDACTED]>'); + expect(result.result).not.toContain('testuser1@example.com'); }); it('redacts multiple email addresses in a single string', async () => { const ctx: PostToolUseContext = { toolName: 'bash', arguments: { command: 'cat AUTHORS' }, - result: 'edie@example.com, fenster@corp.io, hockney@test.org', - agentName: 'edie', + result: 'testuser1@example.com, testuser2@example.com, testuser3@example.com', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -644,7 +644,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { toolName: 'bash', arguments: { command: 'echo hello' }, result: 'Hello world — no PII here', - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -657,13 +657,13 @@ describe('SDK Feature: Constraint Budget (#49)', () => { toolName: 'read', arguments: { path: 'config.json' }, result: { - author: 'edie@example.com', + author: 'testuser1@example.com', metadata: { - reviewer: 'fenster@corp.io', - notes: 'Reviewed by fenster@corp.io on Monday', + reviewer: 'testuser2@example.com', + notes: 'Reviewed by testuser2@example.com on Monday', }, }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; @@ -678,8 +678,8 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PostToolUseContext = { toolName: 'read', arguments: { path: 'team.json' }, - result: ['edie@example.com', 'fenster@corp.io', 'no-email-here'], - agentName: 'edie', + result: ['testuser1@example.com', 'testuser2@example.com', 'no-email-here'], + agentName: 'test-agent-1', sessionId: 's1', }; @@ -695,7 +695,7 @@ describe('SDK Feature: Constraint Budget (#49)', () => { toolName: 'bash', arguments: { command: 'wc -l' }, result: 42, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; const numResult = await pipeline.runPostToolHooks(numCtx); @@ -714,13 +714,13 @@ describe('SDK Feature: Constraint Budget (#49)', () => { const ctx: PostToolUseContext = { toolName: 'bash', arguments: { command: 'git log' }, - result: 'Author: edie@example.com', - agentName: 'edie', + result: 'Author: testuser1@example.com', + agentName: 'test-agent-1', sessionId: 's1', }; const result = await noScrubPipeline.runPostToolHooks(ctx); - expect(result.result).toBe('Author: edie@example.com'); + expect(result.result).toBe('Author: testuser1@example.com'); }); it('scrubs deeply nested objects with mixed types', async () => { @@ -729,20 +729,20 @@ describe('SDK Feature: Constraint Budget (#49)', () => { arguments: { path: 'data.json' }, result: { users: [ - { name: 'Edie', email: 'edie@example.com', age: 30 }, - { name: 'Fenster', email: 'fenster@corp.io', active: true }, + { name: 'TestUser1', email: 'testuser1@example.com', age: 30 }, + { name: 'TestUser2', email: 'testuser2@example.com', active: true }, ], count: 2, note: null, }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }; const result = await pipeline.runPostToolHooks(ctx); const scrubbed = result.result as any; expect(scrubbed.users[0].email).toBe('[EMAIL_REDACTED]'); - expect(scrubbed.users[0].name).toBe('Edie'); + expect(scrubbed.users[0].name).toBe('TestUser1'); expect(scrubbed.users[0].age).toBe(30); expect(scrubbed.users[1].email).toBe('[EMAIL_REDACTED]'); expect(scrubbed.users[1].active).toBe(true); @@ -811,8 +811,8 @@ describe('SDK Feature: Constraint Budget (#49)', () => { it('defineSquad() with hooks constraints composes correctly', () => { const config = defineSquad({ - team: defineTeam({ name: 'Constrained', members: ['edie'] }), - agents: [defineAgent({ name: 'edie', role: 'Engineer' })], + team: defineTeam({ name: 'Constrained', members: ['test-agent-1'] }), + agents: [defineAgent({ name: 'test-agent-1', role: 'Engineer' })], hooks: defineHooks({ maxAskUser: 2, allowedWritePaths: ['src/**'], @@ -840,47 +840,47 @@ describe('SDK Feature: Multi-Agent Artifact Coordination (#50)', () => { }); it('artifact lockout tracks per-artifact per-agent', () => { - lockout.lockout('architecture.md', 'edie'); - lockout.lockout('architecture.md', 'fenster'); - lockout.lockout('tests.md', 'hockney'); + lockout.lockout('architecture.md', 'test-agent-1'); + lockout.lockout('architecture.md', 'test-agent-2'); + lockout.lockout('tests.md', 'test-agent-3'); - expect(lockout.isLockedOut('architecture.md', 'edie')).toBe(true); - expect(lockout.isLockedOut('architecture.md', 'fenster')).toBe(true); - expect(lockout.isLockedOut('architecture.md', 'hockney')).toBe(false); - expect(lockout.isLockedOut('tests.md', 'hockney')).toBe(true); - expect(lockout.isLockedOut('tests.md', 'edie')).toBe(false); + expect(lockout.isLockedOut('architecture.md', 'test-agent-1')).toBe(true); + expect(lockout.isLockedOut('architecture.md', 'test-agent-2')).toBe(true); + expect(lockout.isLockedOut('architecture.md', 'test-agent-3')).toBe(false); + expect(lockout.isLockedOut('tests.md', 'test-agent-3')).toBe(true); + expect(lockout.isLockedOut('tests.md', 'test-agent-1')).toBe(false); }); it('getLockedAgents() lists all agents locked from an artifact', () => { - lockout.lockout('design-doc.md', 'edie'); - lockout.lockout('design-doc.md', 'fenster'); + lockout.lockout('design-doc.md', 'test-agent-1'); + lockout.lockout('design-doc.md', 'test-agent-2'); const locked = lockout.getLockedAgents('design-doc.md'); expect(locked).toHaveLength(2); - expect(locked).toContain('edie'); - expect(locked).toContain('fenster'); + expect(locked).toContain('test-agent-1'); + expect(locked).toContain('test-agent-2'); }); it('clearLockout() enables handoff to next contributor', () => { - lockout.lockout('api-spec.md', 'edie'); - expect(lockout.isLockedOut('api-spec.md', 'edie')).toBe(true); + lockout.lockout('api-spec.md', 'test-agent-1'); + expect(lockout.isLockedOut('api-spec.md', 'test-agent-1')).toBe(true); lockout.clearLockout('api-spec.md'); - expect(lockout.isLockedOut('api-spec.md', 'edie')).toBe(false); + expect(lockout.isLockedOut('api-spec.md', 'test-agent-1')).toBe(false); }); it('multi-agent artifact workflow: write → lockout → handoff', () => { // Agent 1 writes their section, then gets locked out - lockout.lockout('prd.md', 'edie'); + lockout.lockout('prd.md', 'test-agent-1'); // Agent 2 can still contribute - expect(lockout.isLockedOut('prd.md', 'fenster')).toBe(false); + expect(lockout.isLockedOut('prd.md', 'test-agent-2')).toBe(false); // Agent 2 finishes their section, gets locked out - lockout.lockout('prd.md', 'fenster'); + lockout.lockout('prd.md', 'test-agent-2'); // Agent 3 handles final review - expect(lockout.isLockedOut('prd.md', 'hockney')).toBe(false); + expect(lockout.isLockedOut('prd.md', 'test-agent-3')).toBe(false); // After all contributions complete, clear for next iteration lockout.clearLockout('prd.md'); @@ -891,14 +891,14 @@ describe('SDK Feature: Multi-Agent Artifact Coordination (#50)', () => { const pipeline = new HookPipeline({ reviewerLockout: true }); const rlHook = pipeline.getReviewerLockout(); - // Lock out edie from the artifact path - rlHook.lockout('src/auth', 'edie'); + // Lock out test-agent-1 from the artifact path + rlHook.lockout('src/auth', 'test-agent-1'); - // edie tries to edit a file in the locked artifact scope + // test-agent-1 tries to edit a file in the locked artifact scope const result = await pipeline.runPreToolHooks({ toolName: 'edit', arguments: { path: 'src/auth/login.ts' }, - agentName: 'edie', + agentName: 'test-agent-1', sessionId: 's1', }); @@ -909,7 +909,7 @@ describe('SDK Feature: Multi-Agent Artifact Coordination (#50)', () => { const otherResult = await pipeline.runPreToolHooks({ toolName: 'edit', arguments: { path: 'src/auth/login.ts' }, - agentName: 'fenster', + agentName: 'test-agent-2', sessionId: 's1', }); @@ -917,14 +917,14 @@ describe('SDK Feature: Multi-Agent Artifact Coordination (#50)', () => { }); it('multiple artifacts can be tracked independently', () => { - lockout.lockout('frontend/app.tsx', 'edie'); - lockout.lockout('backend/server.ts', 'fenster'); - lockout.lockout('docs/readme.md', 'hockney'); - - expect(lockout.isLockedOut('frontend/app.tsx', 'edie')).toBe(true); - expect(lockout.isLockedOut('frontend/app.tsx', 'fenster')).toBe(false); - expect(lockout.isLockedOut('backend/server.ts', 'fenster')).toBe(true); - expect(lockout.isLockedOut('backend/server.ts', 'edie')).toBe(false); - expect(lockout.isLockedOut('docs/readme.md', 'hockney')).toBe(true); + lockout.lockout('frontend/app.tsx', 'test-agent-1'); + lockout.lockout('backend/server.ts', 'test-agent-2'); + lockout.lockout('docs/readme.md', 'test-agent-3'); + + expect(lockout.isLockedOut('frontend/app.tsx', 'test-agent-1')).toBe(true); + expect(lockout.isLockedOut('frontend/app.tsx', 'test-agent-2')).toBe(false); + expect(lockout.isLockedOut('backend/server.ts', 'test-agent-2')).toBe(true); + expect(lockout.isLockedOut('backend/server.ts', 'test-agent-1')).toBe(false); + expect(lockout.isLockedOut('docs/readme.md', 'test-agent-3')).toBe(true); }); });