From 0fc3aaa6dbb83f6e3c2c692f35affa2caa30ae5e Mon Sep 17 00:00:00 2001 From: zbigniew sobiecki Date: Sun, 1 Mar 2026 14:23:03 +0100 Subject: [PATCH] fix(build): copy system prompt templates to dist during build The `npm run build` script was missing a step to copy system prompt templates, causing blank System Prompt tabs in the Agent Definition Editor for non-Docker deployments. Changes: - Add `build:copy-system-prompts` script that recursively copies `src/agents/prompts/templates/` to `dist/agents/prompts/templates/` - Use `cp -r` for robustness (matches Docker COPY behavior) - Also fixes 11 lint warnings (noExplicitAny) in test files by: - Adding proper `mockAgentDefinition()` helper in modelResolution.test.ts - Using `Object.hasOwn()` instead of `as any` for property checks Co-Authored-By: Claude Opus 4.5 --- package.json | 3 +- .../agents/shared/modelResolution.test.ts | 81 +++++++++++++------ .../unit/db/repositories/configMapper.test.ts | 2 +- .../db/repositories/configRepository.test.ts | 2 +- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 67207898..9429da80 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/unit/agents/shared/modelResolution.test.ts b/tests/unit/agents/shared/modelResolution.test.ts index 3cee1c85..af89c39f 100644 --- a/tests/unit/agents/shared/modelResolution.test.ts +++ b/tests/unit/agents/shared/modelResolution.test.ts @@ -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([]), @@ -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 { @@ -94,7 +123,7 @@ function makeConfig(overrides: Partial = {}): 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', @@ -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', @@ -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', @@ -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({ @@ -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', @@ -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', @@ -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', @@ -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', diff --git a/tests/unit/db/repositories/configMapper.test.ts b/tests/unit/db/repositories/configMapper.test.ts index 070d32c8..97d8064a 100644 --- a/tests/unit/db/repositories/configMapper.test.ts +++ b/tests/unit/db/repositories/configMapper.test.ts @@ -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); }); }); diff --git a/tests/unit/db/repositories/configRepository.test.ts b/tests/unit/db/repositories/configRepository.test.ts index 582b88de..918026a9 100644 --- a/tests/unit/db/repositories/configRepository.test.ts +++ b/tests/unit/db/repositories/configRepository.test.ts @@ -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 () => {