Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
"scripts": {
"dev": "node --env-file=.env --import tsx/esm --watch src/index.ts",
"dev:web": "cd web && npx vite",
"build": "tsc && npm run build:copy-yaml && npm run build:copy-task-templates",
"build": "tsc && npm run build:copy-yaml && npm run build:copy-system-prompts && npm run build:copy-task-templates",
"build:copy-yaml": "mkdir -p dist/agents/definitions && cp src/agents/definitions/*.yaml dist/agents/definitions/",
"build:copy-system-prompts": "mkdir -p dist/agents/prompts && cp -r src/agents/prompts/templates dist/agents/prompts/",
"build:copy-task-templates": "mkdir -p dist/agents/prompts/task-templates && cp src/agents/prompts/task-templates/*.eta dist/agents/prompts/task-templates/",
"build:web": "cd web && npm run build",
"start": "node dist/index.js",
Expand Down
81 changes: 56 additions & 25 deletions tests/unit/agents/shared/modelResolution.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { AgentDefinition } from '../../../../src/agents/definitions/schema.js';
import type { CascadeConfig, ProjectConfig } from '../../../../src/types/index.js';

/**
* Creates a valid mock AgentDefinition with optional prompt overrides.
* Uses minimal valid values for all required fields.
*/
function mockAgentDefinition(prompts?: AgentDefinition['prompts']): AgentDefinition {
return {
identity: { emoji: '🤖', label: 'Test', roleHint: 'test', initialMessage: 'Hi' },
capabilities: {
canEditFiles: false,
canCreatePR: false,
canUpdateChecklists: false,
isReadOnly: true,
},
tools: { sets: [], sdkTools: 'readOnly' },
strategies: {
contextPipeline: [],
taskPromptBuilder: 'workItem',
gadgetBuilder: 'workItem',
},
backend: { enableStopHooks: false, needsGitHubToken: false },
compaction: 'default',
hint: 'test',
trailingMessage: undefined,
integrations: { required: [], optional: [] },
prompts,
};
}

// Mock readContextFiles
vi.mock('../../../../src/agents/utils/setup.js', () => ({
readContextFiles: vi.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -57,7 +86,7 @@ beforeAll(async () => {

beforeEach(() => {
// Reset to default (no custom prompt)
vi.mocked(resolveAgentDefinition).mockResolvedValue({ prompts: undefined } as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(mockAgentDefinition(undefined));
});

function makeProject(overrides: Partial<ProjectConfig> = {}): ProjectConfig {
Expand Down Expand Up @@ -94,7 +123,7 @@ function makeConfig(overrides: Partial<CascadeConfig['defaults']> = {}): Cascade
describe('resolveModelConfig', () => {
describe('prompt resolution chain', () => {
it('uses .eta file when no custom prompts in definition', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({ prompts: undefined } as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(mockAgentDefinition(undefined));

const result = await resolveModelConfig({
agentType: 'splitting',
Expand All @@ -108,9 +137,11 @@ describe('resolveModelConfig', () => {
});

it('uses definition systemPrompt when configured', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: { systemPrompt: 'You are a custom splitting agent for <%= it.baseBranch %>.' },
} as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({
systemPrompt: 'You are a custom splitting agent for <%= it.baseBranch %>.',
}),
);

const result = await resolveModelConfig({
agentType: 'splitting',
Expand All @@ -124,9 +155,9 @@ describe('resolveModelConfig', () => {
});

it('falls back to .eta when definition has no systemPrompt', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: { taskPrompt: 'Only task prompt configured.' },
} as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({ taskPrompt: 'Only task prompt configured.' }),
);

const result = await resolveModelConfig({
agentType: 'splitting',
Expand Down Expand Up @@ -154,9 +185,9 @@ describe('resolveModelConfig', () => {
});

it('resolves includes in custom prompts via dbPartials', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: { systemPrompt: 'Custom: <%~ include("partials/custom") %>' },
} as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({ systemPrompt: 'Custom: <%~ include("partials/custom") %>' }),
);
const dbPartials = new Map([['custom', 'Injected partial content']]);

const result = await resolveModelConfig({
Expand Down Expand Up @@ -256,9 +287,9 @@ describe('resolveModelConfig', () => {
});

it('renders task prompt from definition', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: { taskPrompt: 'Custom task for <%= it.cardId %>.' },
} as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({ taskPrompt: 'Custom task for <%= it.cardId %>.' }),
);

const result = await resolveModelConfig({
agentType: 'splitting',
Expand All @@ -272,11 +303,11 @@ describe('resolveModelConfig', () => {
});

it('renders task-specific variables from agentInput', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: {
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({
taskPrompt: 'Comment by @<%= it.commentAuthor %>: <%= it.commentText %>',
},
} as any);
}),
);

const result = await resolveModelConfig({
agentType: 'respond-to-planning-comment',
Expand All @@ -293,12 +324,12 @@ describe('resolveModelConfig', () => {
});

it('renders PR-specific variables from agentInput in task prompt override', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: {
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({
taskPrompt:
'PR #<%= it.prNumber %>, file: <%= it.commentPath %>, body: <%= it.commentBody %>',
},
} as any);
}),
);

const result = await resolveModelConfig({
agentType: 'respond-to-pr-comment',
Expand All @@ -319,9 +350,9 @@ describe('resolveModelConfig', () => {
});

it('returns undefined taskPrompt when definition has no taskPrompt', async () => {
vi.mocked(resolveAgentDefinition).mockResolvedValue({
prompts: { systemPrompt: 'Only system prompt configured.' },
} as any);
vi.mocked(resolveAgentDefinition).mockResolvedValue(
mockAgentDefinition({ systemPrompt: 'Only system prompt configured.' }),
);

const result = await resolveModelConfig({
agentType: 'splitting',
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/db/repositories/configMapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,6 @@ describe('mapProjectRow', () => {
},
];
const result = mapProjectRow(makeInput({ projectAgentConfigs: agentConfigs }));
expect((result as any).prompts).toBeUndefined();
expect(Object.hasOwn(result, 'prompts')).toBe(false);
});
});
2 changes: 1 addition & 1 deletion tests/unit/db/repositories/configRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ describe('configRepository', () => {
expect(result).toBeDefined();
expect(result?.agentBackend?.overrides).toEqual({ implementation: 'claude-code' });
// prompts are no longer stored in agent_configs (moved to agent_definitions)
expect((result as any)?.prompts).toBeUndefined();
expect(result && Object.hasOwn(result, 'prompts')).toBe(false);
});

it('runs 5 sub-queries in parallel after initial project lookup', async () => {
Expand Down