diff --git a/docs/plans/state.json b/docs/plans/state.json index 3c8ed8c..3e0dacc 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2700,6 +2700,33 @@ "src/skills/installer.test.ts" ], "test_status": "pnpm exec eslint on edited files + pnpm lint passing (0 errors, 0 warnings)" + }, + "audit-followup-typecheck-strict-burndown": { + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Closed strict TypeScript fallout after lint cleanup by typing setup wizard config output (`SetupConfig`), tightening optional field handling in setup summary/tests, updating routing/tailscale/tool policy test mocks for strict compatibility, and fixing narrow production strictness in OpenAI/GitHub image URL mapping and file.list glob filtering.", + "files_modified": [ + "src/cli/setup/channels.test.ts", + "src/cli/setup/config.test.ts", + "src/cli/setup/config.ts", + "src/cli/setup/integration.test.ts", + "src/cli/setup/sections.test.ts", + "src/cli/setup/summary.ts", + "src/daemon/routing.test.ts", + "src/gateway/handlers/services.test.ts", + "src/gateway/tailscale.test.ts", + "src/models/github.ts", + "src/models/local/llamacpp.test.ts", + "src/models/local/ollama.test.ts", + "src/models/openai.baseurl.test.ts", + "src/models/openai.ts", + "src/tools/builtin/file-list.ts", + "src/tools/executor.test.ts", + "src/tools/registry.test.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm typecheck + pnpm test:run src/cli/setup/channels.test.ts src/cli/setup/config.test.ts src/cli/setup/integration.test.ts src/cli/setup/sections.test.ts src/daemon/routing.test.ts src/gateway/handlers/services.test.ts src/gateway/tailscale.test.ts src/models/local/llamacpp.test.ts src/models/local/ollama.test.ts src/models/openai.baseurl.test.ts src/tools/executor.test.ts src/tools/registry.test.ts passing" } }, "overall_progress": { diff --git a/src/cli/setup/channels.test.ts b/src/cli/setup/channels.test.ts index 962a48b..288c000 100644 --- a/src/cli/setup/channels.test.ts +++ b/src/cli/setup/channels.test.ts @@ -44,8 +44,8 @@ describe('setupChannels', () => { const builder = new ConfigBuilder(); await setupChannels(p, builder); const config = builder.build(); - expect(config.telegram.bot_token).toBe('123:ABC'); - expect(config.telegram.allowed_chat_ids).toEqual([12345, 67890]); + expect(config.telegram!.bot_token).toBe('123:ABC'); + expect(config.telegram!.allowed_chat_ids).toEqual([12345, 67890]); }); it('configures discord channel', async () => { @@ -54,7 +54,7 @@ describe('setupChannels', () => { const builder = new ConfigBuilder(); await setupChannels(p, builder); const config = builder.build(); - expect(config.discord.bot_token).toBe('MTIz.token'); - expect(config.discord.allowed_guild_ids).toEqual(['guild1', 'guild2']); + expect(config.discord!.bot_token).toBe('MTIz.token'); + expect(config.discord!.allowed_guild_ids).toEqual(['guild1', 'guild2']); }); }); diff --git a/src/cli/setup/config.test.ts b/src/cli/setup/config.test.ts index 33870b8..84b0768 100644 --- a/src/cli/setup/config.test.ts +++ b/src/cli/setup/config.test.ts @@ -17,8 +17,8 @@ describe('ConfigBuilder', () => { builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' }); builder.setTelegram('123:ABC', [12345]); const obj = builder.build(); - expect(obj.telegram.bot_token).toBe('123:ABC'); - expect(obj.telegram.allowed_chat_ids).toEqual([12345]); + expect(obj.telegram!.bot_token).toBe('123:ABC'); + expect(obj.telegram!.allowed_chat_ids).toEqual([12345]); }); it('adds discord channel', () => { @@ -26,8 +26,8 @@ describe('ConfigBuilder', () => { builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' }); builder.setDiscord('MTIz.test', ['guild1']); const obj = builder.build(); - expect(obj.discord.bot_token).toBe('MTIz.test'); - expect(obj.discord.allowed_guild_ids).toEqual(['guild1']); + expect(obj.discord!.bot_token).toBe('MTIz.test'); + expect(obj.discord!.allowed_guild_ids).toEqual(['guild1']); }); it('adds fast tier', () => { @@ -58,7 +58,7 @@ describe('ConfigBuilder', () => { const obj = builder.build(); expect(obj.models.default.provider).toBe('openai'); expect(obj.server.port).toBe(9999); - expect(obj.telegram.bot_token).toBe('123:ABC'); + expect(obj.telegram!.bot_token).toBe('123:ABC'); }); it('sets memory embedding config', () => { @@ -66,16 +66,16 @@ describe('ConfigBuilder', () => { builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' }); builder.setMemoryEmbedding({ provider: 'openai', api_key: 'sk-emb' }); const obj = builder.build(); - expect(obj.memory.embedding.enabled).toBe(true); - expect(obj.memory.embedding.provider).toBe('openai'); - expect(obj.memory.embedding.api_key).toBe('sk-emb'); + expect(obj.memory!.embedding!.enabled).toBe(true); + expect(obj.memory!.embedding!.provider).toBe('openai'); + expect(obj.memory!.embedding!.api_key).toBe('sk-emb'); }); it('sets sandbox enabled', () => { const builder = new ConfigBuilder(); builder.setSandboxEnabled(true); const obj = builder.build(); - expect(obj.sandbox.enabled).toBe(true); + expect(obj.sandbox!.enabled).toBe(true); }); it('sets gateway auth token', () => { diff --git a/src/cli/setup/config.ts b/src/cli/setup/config.ts index ba7499e..ddb37cb 100644 --- a/src/cli/setup/config.ts +++ b/src/cli/setup/config.ts @@ -14,8 +14,40 @@ interface EmbeddingConfig { endpoint?: string; } +export interface SetupConfig { + log_level?: string; + models: Record>; + server: { + port?: number; + localhost?: boolean; + token?: string; + lock?: boolean; + tailscale?: { serve?: boolean }; + } & Record; + hooks?: Record; + telegram?: { bot_token: string; allowed_chat_ids: number[] }; + discord?: { bot_token: string; allowed_guild_ids: string[] }; + slack?: { bot_token: string; app_token: string; signing_secret: string; allowed_channel_ids: string[] }; + whatsapp?: { allowed_numbers: string[] }; + memory?: { embedding?: { enabled?: boolean; provider?: string; api_key?: string; endpoint?: string } }; + sandbox?: { enabled?: boolean }; + pairing?: { enabled?: boolean }; + tools?: { profile?: string }; + automation?: { + cron?: Array>; + webhooks?: Array>; + gmail?: { enabled?: boolean }; + gcal?: { enabled?: boolean }; + gdocs?: { enabled?: boolean }; + gdrive?: { enabled?: boolean }; + gtasks?: { enabled?: boolean }; + heartbeat?: { enabled?: boolean }; + } & Record; + [key: string]: unknown; +} + export class ConfigBuilder { - private config: Record; + private config: SetupConfig; constructor() { this.config = { @@ -32,13 +64,13 @@ export class ConfigBuilder { static fromObject(obj: Record): ConfigBuilder { const builder = new ConfigBuilder(); - builder.config = structuredClone(obj); + builder.config = structuredClone(obj) as SetupConfig; return builder; } setProvider(tier: 'default' | 'fast' | 'complex' | 'local', cfg: ProviderConfig): void { - const models = (this.config.models ?? {}) as Record; - const entry: Record = { provider: cfg.provider, model: cfg.model }; + const models = (this.config.models ?? {}) as SetupConfig['models']; + const entry: ProviderConfig & Record = { provider: cfg.provider, model: cfg.model }; if (cfg.api_key) {entry.api_key = cfg.api_key;} if (cfg.auth_token) {entry.auth_token = cfg.auth_token;} if (cfg.endpoint) {entry.endpoint = cfg.endpoint;} @@ -155,8 +187,8 @@ export class ConfigBuilder { this.config.automation = automation; } - build(): Record { - return structuredClone(this.config) as Record; + build(): SetupConfig { + return structuredClone(this.config) as SetupConfig; } toYaml(): string { diff --git a/src/cli/setup/integration.test.ts b/src/cli/setup/integration.test.ts index be38563..34abbad 100644 --- a/src/cli/setup/integration.test.ts +++ b/src/cli/setup/integration.test.ts @@ -37,8 +37,8 @@ describe('first-run wizard integration', () => { expect(config.models.default.api_key).toBe('sk-ant-key'); expect(config.server.port).toBeDefined(); - const reparsed = parse(yaml); - expect(reparsed.models.default.provider).toBe('anthropic'); + const reparsed = parse(yaml) as { models?: { default?: { provider?: string } } }; + expect(reparsed.models!.default!.provider).toBe('anthropic'); }); it('produces valid config with ollama + telegram', async () => { @@ -60,7 +60,7 @@ describe('first-run wizard integration', () => { const config = builder.build(); expect(config.models.default.provider).toBe('ollama'); - expect(config.telegram.bot_token).toBe('123:ABCdef'); - expect(config.telegram.allowed_chat_ids).toEqual([12345678]); + expect(config.telegram!.bot_token).toBe('123:ABCdef'); + expect(config.telegram!.allowed_chat_ids).toEqual([12345678]); }); }); diff --git a/src/cli/setup/sections.test.ts b/src/cli/setup/sections.test.ts index 3789e7e..4c2fb09 100644 --- a/src/cli/setup/sections.test.ts +++ b/src/cli/setup/sections.test.ts @@ -37,8 +37,8 @@ describe('setupMemory', () => { builder.setProvider('default', { provider: 'openai', model: 'gpt-4.1', api_key: 'sk-test' }); await setupMemory(p, builder); const config = builder.build(); - expect(config.memory.embedding.enabled).toBe(true); - expect(config.memory.embedding.provider).toBe('openai'); + expect(config.memory!.embedding!.enabled).toBe(true); + expect(config.memory!.embedding!.provider).toBe('openai'); }); it('skips vector search when declined', async () => { @@ -58,8 +58,8 @@ describe('setupSecurity', () => { const builder = new ConfigBuilder(); await setupSecurity(p, builder); const config = builder.build(); - expect(config.sandbox.enabled).toBe(true); - expect(config.pairing.enabled).toBe(true); + expect(config.sandbox!.enabled).toBe(true); + expect(config.pairing!.enabled).toBe(true); }); }); diff --git a/src/cli/setup/summary.ts b/src/cli/setup/summary.ts index 4e5bf1d..54979c6 100644 --- a/src/cli/setup/summary.ts +++ b/src/cli/setup/summary.ts @@ -1,4 +1,6 @@ -export function renderSummary(config: Record): string { +import type { SetupConfig } from './config.js'; + +export function renderSummary(config: SetupConfig): string { const lines: string[] = []; const models = config.models ?? {}; @@ -22,8 +24,8 @@ export function renderSummary(config: Record): string { const auto = config.automation ?? {}; const autoFeatures: string[] = []; - if (auto.cron?.length > 0) {autoFeatures.push(`${auto.cron.length} cron jobs`);} - if (auto.webhooks?.length > 0) {autoFeatures.push('webhooks');} + if (Array.isArray(auto.cron) && auto.cron.length > 0) {autoFeatures.push(`${auto.cron.length} cron jobs`);} + if (Array.isArray(auto.webhooks) && auto.webhooks.length > 0) {autoFeatures.push('webhooks');} if (auto.gmail?.enabled) {autoFeatures.push('gmail');} if (auto.gcal?.enabled) {autoFeatures.push('gcal');} if (auto.gdocs?.enabled) {autoFeatures.push('gdocs');} diff --git a/src/daemon/routing.test.ts b/src/daemon/routing.test.ts index 0535e84..a727ebf 100644 --- a/src/daemon/routing.test.ts +++ b/src/daemon/routing.test.ts @@ -99,18 +99,18 @@ describe('daemon command fast-path integration', () => { const router = createMessageRouter({ sessionManager: { getSession: vi.fn(() => session), - } as MessageRouterDeps['sessionManager'], + } as unknown as MessageRouterDeps['sessionManager'], modelRouter: { getAvailableTiers: () => ['fast', 'default', 'complex', 'local'], getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }), getLabel: (tier: string) => tier, - } as MessageRouterDeps['modelRouter'], + } as unknown as MessageRouterDeps['modelRouter'], systemPrompt: 'test prompt', toolRegistry: { clone() { return this; }, register: vi.fn(), - } as MessageRouterDeps['toolRegistry'], - toolExecutor: {} as MessageRouterDeps['toolExecutor'], + } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], config: { agents: { primary_tier: 'default', @@ -126,16 +126,17 @@ describe('daemon command fast-path integration', () => { }, compaction: { enabled: false }, models: { default: { provider: 'anthropic', model: 'claude' } }, - } as MessageRouterDeps['config'], + } as unknown as MessageRouterDeps['config'], commandRegistry, }); - const reply = vi.fn(async () => {}); + const reply = vi.fn(async (_message: OutboundMessage) => {}); await router.handler({ id: 'm1', channel: 'telegram', senderId: 'user-1', text: '/reset', + timestamp: Date.now(), metadata: { isCommand: true, command: 'reset' }, } as MessageRouterInput, reply); @@ -163,18 +164,18 @@ describe('daemon command fast-path integration', () => { const router = createMessageRouter({ sessionManager: { getSession: vi.fn(() => session), - } as MessageRouterDeps['sessionManager'], + } as unknown as MessageRouterDeps['sessionManager'], modelRouter: { getAvailableTiers: () => ['fast', 'default', 'complex', 'local'], getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }), getLabel: (tier: string) => tier, - } as MessageRouterDeps['modelRouter'], + } as unknown as MessageRouterDeps['modelRouter'], systemPrompt: 'test prompt', toolRegistry: { clone() { return this; }, register: vi.fn(), - } as MessageRouterDeps['toolRegistry'], - toolExecutor: {} as MessageRouterDeps['toolExecutor'], + } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], config: { agents: { primary_tier: 'default', @@ -190,16 +191,17 @@ describe('daemon command fast-path integration', () => { }, compaction: { enabled: false }, models: { default: { provider: 'anthropic', model: 'claude' } }, - } as MessageRouterDeps['config'], + } as unknown as MessageRouterDeps['config'], commandRegistry, }); - const reply = vi.fn(async () => {}); + const reply = vi.fn(async (_message: OutboundMessage) => {}); await router.handler({ id: 'm4', channel: 'telegram', senderId: 'user-4', text: '/model fast', + timestamp: Date.now(), metadata: { isCommand: true, command: 'model', commandArgs: 'fast' }, } as MessageRouterInput, reply); @@ -247,18 +249,18 @@ describe('daemon command fast-path integration', () => { const router = createMessageRouter({ sessionManager: { getSession: vi.fn(() => session), - } as MessageRouterDeps['sessionManager'], + } as unknown as MessageRouterDeps['sessionManager'], modelRouter: { getAvailableTiers: () => ['fast', 'default', 'complex', 'local'], getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }), getLabel: (tier: string) => tier, - } as MessageRouterDeps['modelRouter'], + } as unknown as MessageRouterDeps['modelRouter'], systemPrompt: 'test prompt', toolRegistry: { clone() { return this; }, register: vi.fn(), - } as MessageRouterDeps['toolRegistry'], - toolExecutor: {} as MessageRouterDeps['toolExecutor'], + } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], config: { intents: { enabled: true }, agents: { @@ -275,7 +277,7 @@ describe('daemon command fast-path integration', () => { }, compaction: { enabled: false }, models: { default: { provider: 'anthropic', model: 'claude' } }, - } as MessageRouterDeps['config'], + } as unknown as MessageRouterDeps['config'], commandRegistry, intentRegistry, agentConfigRegistry, @@ -287,6 +289,7 @@ describe('daemon command fast-path integration', () => { channel: 'telegram', senderId: 'user-2', text: 'deploy backend now', + timestamp: Date.now(), metadata: { isCommand: true, command: 'reset' }, } as MessageRouterInput, vi.fn(async () => {})); @@ -340,18 +343,18 @@ describe('daemon command fast-path integration', () => { const router = createMessageRouter({ sessionManager: { getSession: vi.fn(() => session), - } as MessageRouterDeps['sessionManager'], + } as unknown as MessageRouterDeps['sessionManager'], modelRouter: { getAvailableTiers: () => ['fast', 'default', 'complex', 'local'], getAllLabels: () => ({ fast: 'fast', default: 'default', complex: 'complex', local: 'local' }), getLabel: (tier: string) => tier, - } as MessageRouterDeps['modelRouter'], + } as unknown as MessageRouterDeps['modelRouter'], systemPrompt: 'test prompt', toolRegistry: { clone() { return this; }, register: vi.fn(), - } as MessageRouterDeps['toolRegistry'], - toolExecutor: {} as MessageRouterDeps['toolExecutor'], + } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], config: { intents: { enabled: true }, agents: { @@ -368,7 +371,7 @@ describe('daemon command fast-path integration', () => { }, compaction: { enabled: false }, models: { default: { provider: 'anthropic', model: 'claude' } }, - } as MessageRouterDeps['config'], + } as unknown as MessageRouterDeps['config'], commandRegistry, intentRegistry, routingPolicy, @@ -381,6 +384,7 @@ describe('daemon command fast-path integration', () => { channel: 'telegram', senderId: 'user-3', text: 'deploy backend now', + timestamp: Date.now(), metadata: { isCommand: true, command: 'reset' }, } as MessageRouterInput, vi.fn(async () => {})); @@ -412,15 +416,15 @@ describe('daemon audio routing integration', () => { registerBuiltinCommands(commandRegistry); const router = createMessageRouter({ - sessionManager: { getSession: vi.fn(() => session) } as MessageRouterDeps['sessionManager'], + sessionManager: { getSession: vi.fn(() => session) } as unknown as MessageRouterDeps['sessionManager'], modelRouter: { getAvailableTiers: () => ['default'], getAllLabels: () => ({ default: 'default' }), getLabel: (tier: string) => tier, - } as MessageRouterDeps['modelRouter'], + } as unknown as MessageRouterDeps['modelRouter'], systemPrompt: 'test prompt', - toolRegistry: { clone() { return this; }, register: vi.fn() } as MessageRouterDeps['toolRegistry'], - toolExecutor: {} as MessageRouterDeps['toolExecutor'], + toolRegistry: { clone() { return this; }, register: vi.fn() } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], config: { agents: { primary_tier: 'default', @@ -438,11 +442,11 @@ describe('daemon audio routing integration', () => { // Anthropic doesn't support native audio; ensures routing hits the non-audio path. models: { default: { provider: 'anthropic', model: 'claude' } }, audio: { enabled: false }, - } as MessageRouterDeps['config'], + } as unknown as MessageRouterDeps['config'], commandRegistry, }); - const reply = vi.fn(async () => {}); + const reply = vi.fn(async (_message: OutboundMessage) => {}); await router.handler({ id: 'v1', channel: 'telegram', @@ -485,15 +489,15 @@ describe('daemon audio routing integration', () => { registerBuiltinCommands(commandRegistry); const router = createMessageRouter({ - sessionManager: { getSession: vi.fn(() => session) } as MessageRouterDeps['sessionManager'], + sessionManager: { getSession: vi.fn(() => session) } as unknown as MessageRouterDeps['sessionManager'], modelRouter: { getAvailableTiers: () => ['default'], getAllLabels: () => ({ default: 'default' }), getLabel: (tier: string) => tier, - } as MessageRouterDeps['modelRouter'], + } as unknown as MessageRouterDeps['modelRouter'], systemPrompt: 'test prompt', - toolRegistry: { clone() { return this; }, register: vi.fn() } as MessageRouterDeps['toolRegistry'], - toolExecutor: {} as MessageRouterDeps['toolExecutor'], + toolRegistry: { clone() { return this; }, register: vi.fn() } as unknown as MessageRouterDeps['toolRegistry'], + toolExecutor: {} as unknown as MessageRouterDeps['toolExecutor'], config: { agents: { primary_tier: 'default', @@ -513,11 +517,11 @@ describe('daemon audio routing integration', () => { enabled: true, provider: { type: 'openai', endpoint: 'https://example.com/v1/audio/transcriptions', api_key: 'sk-test', model: 'whisper-1' }, }, - } as MessageRouterDeps['config'], + } as unknown as MessageRouterDeps['config'], commandRegistry, }); - const reply = vi.fn(async () => {}); + const reply = vi.fn(async (_message: OutboundMessage) => {}); await router.handler({ id: 'v2', channel: 'telegram', diff --git a/src/gateway/handlers/services.test.ts b/src/gateway/handlers/services.test.ts index 190fa0c..2f49264 100644 --- a/src/gateway/handlers/services.test.ts +++ b/src/gateway/handlers/services.test.ts @@ -52,7 +52,7 @@ describe('discoverServices', () => { it('marks configured channels as disconnected when adapter is not registered', () => { const cfg = makeBaseConfig(); - withMutableConfig(cfg).telegram = { bot_token: 'x', allowed_chat_ids: [123] }; + withMutableConfig(cfg).telegram = { bot_token: 'x', allowed_chat_ids: [123], require_mention: false }; const reg = new ChannelRegistry(); const services = discoverServices(cfg, reg); @@ -62,7 +62,7 @@ describe('discoverServices', () => { it('uses adapter status when channel adapter is registered', () => { const cfg = makeBaseConfig(); - withMutableConfig(cfg).telegram = { bot_token: 'x', allowed_chat_ids: [123] }; + withMutableConfig(cfg).telegram = { bot_token: 'x', allowed_chat_ids: [123], require_mention: false }; const reg = new ChannelRegistry(); reg.register({ diff --git a/src/gateway/tailscale.test.ts b/src/gateway/tailscale.test.ts index 1a5043d..8490d4f 100644 --- a/src/gateway/tailscale.test.ts +++ b/src/gateway/tailscale.test.ts @@ -8,12 +8,23 @@ vi.mock('child_process', () => ({ })); const mockExecFile = vi.mocked(execFile); -type ExecFileCallback = (error: Error | null, stdout: string, stderr: string) => void; +type ExecFileCallback = NonNullable[3]>; function mockChildProcess(): ChildProcess { return {} as ChildProcess; } +function mockExecFileOnce( + impl: (callback: ExecFileCallback) => void, +): void { + mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback) => { + if (typeof callback === 'function') { + impl(callback as ExecFileCallback); + } + return mockChildProcess(); + }); +} + describe('tailscale', () => { // Import after mocking let isTailscaleAvailable: typeof import('./tailscale.js').isTailscaleAvailable; @@ -33,15 +44,8 @@ describe('tailscale', () => { describe('isTailscaleAvailable', () => { it('returns available when tailscale CLI works', async () => { - mockExecFile - .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, '1.62.0', ''); - return mockChildProcess(); - }) - .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, '{}', ''); - return mockChildProcess(); - }); + mockExecFileOnce((callback) => callback(null, '1.62.0', '')); + mockExecFileOnce((callback) => callback(null, '{}', '')); const result = await isTailscaleAvailable(); expect(result.available).toBe(true); @@ -49,10 +53,7 @@ describe('tailscale', () => { }); it('returns unavailable when tailscale CLI fails', async () => { - mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(new Error('command not found'), '', 'command not found'); - return mockChildProcess(); - }); + mockExecFileOnce((callback) => callback(new Error('command not found'), '', 'command not found')); const result = await isTailscaleAvailable(); expect(result.available).toBe(false); @@ -62,17 +63,8 @@ describe('tailscale', () => { describe('startTailscaleServe', () => { it('calls tailscale serve with correct args', async () => { - mockExecFile - // serve command - .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, '', ''); - return mockChildProcess(); - }) - // status for hostname - .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), ''); - return mockChildProcess(); - }); + mockExecFileOnce((callback) => callback(null, '', '')); + mockExecFileOnce((callback) => callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), '')); const url = await startTailscaleServe({ localPort: 18800 }); expect(url).toBe('https://myhost.tailnet.ts.net'); @@ -83,15 +75,8 @@ describe('tailscale', () => { }); it('uses custom serve port', async () => { - mockExecFile - .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, '', ''); - return mockChildProcess(); - }) - .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), ''); - return mockChildProcess(); - }); + mockExecFileOnce((callback) => callback(null, '', '')); + mockExecFileOnce((callback) => callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), '')); const url = await startTailscaleServe({ localPort: 18800, servePort: 8443 }); expect(url).toBe('https://myhost.tailnet.ts.net:8443'); @@ -103,10 +88,7 @@ describe('tailscale', () => { describe('stopTailscaleServe', () => { it('calls tailscale serve off', async () => { - mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(null, '', ''); - return mockChildProcess(); - }); + mockExecFileOnce((callback) => callback(null, '', '')); await stopTailscaleServe({ localPort: 18800 }); @@ -117,10 +99,7 @@ describe('tailscale', () => { }); it('does not throw on failure', async () => { - mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { - callback(new Error('failed'), '', 'failed'); - return mockChildProcess(); - }); + mockExecFileOnce((callback) => callback(new Error('failed'), '', 'failed')); // Should not throw await stopTailscaleServe({ localPort: 18800 }); diff --git a/src/models/github.ts b/src/models/github.ts index 53c0305..cd08d46 100644 --- a/src/models/github.ts +++ b/src/models/github.ts @@ -39,7 +39,7 @@ function toOpenAIContent(content: string | MessageContentPart[]): string | OpenA } const url = part.source.type === 'base64' ? `data:${part.source.media_type};base64,${part.source.data}` - : part.source.url; + : (part.source.url ?? ''); return { type: 'image_url', image_url: { url } }; } if (part.type === 'audio') { diff --git a/src/models/local/llamacpp.test.ts b/src/models/local/llamacpp.test.ts index 8021876..3af472c 100644 --- a/src/models/local/llamacpp.test.ts +++ b/src/models/local/llamacpp.test.ts @@ -405,7 +405,7 @@ describe('normalizeMessagesForLlamaCpp', () => { content: [ { type: 'text', text: 'Searching...' }, { type: 'tool_use', id: 'call_1', name: 'web.search', input: { query: 'news' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -433,13 +433,13 @@ describe('normalizeMessagesForLlamaCpp', () => { role: 'assistant', content: [ { type: 'tool_use', id: 'call_1', name: 'web.search', input: { query: 'news' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_1', content: 'Results here', is_error: false }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -460,14 +460,14 @@ describe('normalizeMessagesForLlamaCpp', () => { content: [ { type: 'tool_use', id: 'call_a', name: 'tool.a', input: {} }, { type: 'tool_use', id: 'call_b', name: 'tool.b', input: { x: 1 } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_a', content: 'A result' }, { type: 'tool_result', tool_use_id: 'call_b', content: 'B result' }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -492,13 +492,13 @@ describe('normalizeMessagesForLlamaCpp', () => { role: 'assistant', content: [ { type: 'tool_use', id: 'call_1', name: 'file.read', input: { path: '/tmp/x' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_1', content: 'File not found', is_error: true }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -518,13 +518,13 @@ describe('normalizeMessagesForLlamaCpp', () => { content: [ { type: 'text', text: 'Checking...' }, { type: 'tool_use', id: 'tc_0', name: 'weather.get', input: { city: 'NYC' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'tc_0', content: 'Sunny, 72F' }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'assistant', content: 'The weather in NYC is sunny, 72F.' }, ]; diff --git a/src/models/local/ollama.test.ts b/src/models/local/ollama.test.ts index d7a55e2..8f5c01e 100644 --- a/src/models/local/ollama.test.ts +++ b/src/models/local/ollama.test.ts @@ -375,7 +375,7 @@ describe('normalizeMessagesForOllama', () => { content: [ { type: 'text', text: 'Let me search for that.' }, { type: 'tool_use', id: 'call_1', name: 'web.search', input: { query: 'latest news' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -402,13 +402,13 @@ describe('normalizeMessagesForOllama', () => { role: 'assistant', content: [ { type: 'tool_use', id: 'call_1', name: 'web.search', input: { query: 'news' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_1', content: 'Breaking news: ...', is_error: false }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -430,14 +430,14 @@ describe('normalizeMessagesForOllama', () => { content: [ { type: 'tool_use', id: 'call_1', name: 'web.search', input: { query: 'a' } }, { type: 'tool_use', id: 'call_2', name: 'web.search', input: { query: 'b' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_1', content: 'Result A' }, { type: 'tool_result', tool_use_id: 'call_2', content: 'Result B' }, - ] as Message['content'], + ] as unknown as Message['content'], }, ]; @@ -457,13 +457,13 @@ describe('normalizeMessagesForOllama', () => { content: [ { type: 'text', text: 'Let me check.' }, { type: 'tool_use', id: 'tc_0', name: 'weather.get', input: { city: 'NYC' } }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'tc_0', content: 'Sunny, 72F' }, - ] as Message['content'], + ] as unknown as Message['content'], }, { role: 'assistant', content: 'The weather in NYC is sunny, 72F.' }, ]; diff --git a/src/models/openai.baseurl.test.ts b/src/models/openai.baseurl.test.ts index c78f4de..db7f604 100644 --- a/src/models/openai.baseurl.test.ts +++ b/src/models/openai.baseurl.test.ts @@ -23,6 +23,9 @@ describe('OpenAIClient', () => { }); expect(capturedOptions).toBeDefined(); - expect(capturedOptions.baseURL).toBe('https://example.com/v1'); + if (!capturedOptions) { + throw new Error('Expected OpenAI options to be captured'); + } + expect((capturedOptions as Record)['baseURL']).toBe('https://example.com/v1'); }); }); diff --git a/src/models/openai.ts b/src/models/openai.ts index 6fc0dad..29ed0c1 100644 --- a/src/models/openai.ts +++ b/src/models/openai.ts @@ -36,7 +36,7 @@ function toOpenAIContent(content: string | MessageContentPart[]): string | OpenA // OpenAI accepts data URIs or regular URLs const url = part.source.type === 'base64' ? `data:${part.source.media_type};base64,${part.source.data}` - : part.source.url; + : (part.source.url ?? ''); return { type: 'image_url', image_url: { url } }; } if (part.type === 'audio') { diff --git a/src/tools/builtin/file-list.ts b/src/tools/builtin/file-list.ts index 8be8db1..cef12ed 100644 --- a/src/tools/builtin/file-list.ts +++ b/src/tools/builtin/file-list.ts @@ -26,8 +26,9 @@ export const fileListTool: Tool = { const args = rawArgs as FileListArgs; try { let entries = readdirSync(args.path, { withFileTypes: true }); - if (args.pattern) { - entries = entries.filter(e => matchGlob(e.name, args.pattern)); + const pattern = args.pattern; + if (typeof pattern === 'string' && pattern.length > 0) { + entries = entries.filter(e => matchGlob(e.name, pattern)); } const output = entries .map(e => e.isDirectory() ? `${e.name}/` : e.name) diff --git a/src/tools/executor.test.ts b/src/tools/executor.test.ts index 09facbd..3595985 100644 --- a/src/tools/executor.test.ts +++ b/src/tools/executor.test.ts @@ -400,7 +400,7 @@ describe('ToolExecutor', () => { const fakeManager = { getOrCreate: async () => fakeSandbox, } as { getOrCreate: (sessionId: string) => Promise }; - executor.setSandboxManager(fakeManager); + executor.setSandboxManager(fakeManager as unknown as Parameters[0]); const result = await executor.execute('shell.exec', { command: 'echo hi' }, { executionEnvironment: 'sandbox', diff --git a/src/tools/registry.test.ts b/src/tools/registry.test.ts index d5cf2c7..3fad70e 100644 --- a/src/tools/registry.test.ts +++ b/src/tools/registry.test.ts @@ -115,12 +115,12 @@ describe('ToolRegistry', () => { it('inherits the policy from original', () => { const reg = new ToolRegistry(); - const mockPolicy: ToolPolicy = { + const mockPolicy = { filterTools: vi.fn((tools) => tools), isAllowed: vi.fn(() => true), - resolveAllowedNames: vi.fn(() => new Set()), - getEffectiveProfile: vi.fn(() => ({ profile: 'full', source: 'explicit' })), - }; + resolveAllowedNames: vi.fn(() => new Set()), + getEffectiveProfile: vi.fn<() => 'full'>(() => 'full'), + } as unknown as ToolPolicy; reg.setPolicy(mockPolicy); const cloned = reg.clone();