From e16c0bc2c7a9a55b4f68848d9ea874c7de92377b Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 15 Feb 2026 22:49:45 -0800 Subject: [PATCH] audit follow-up: reduce warning hotspots in automation and gateway tests --- .../2026-02-16-codebase-audit-report.md | 15 +++-- docs/plans/state.json | 9 ++- src/automation/cron.test.ts | 28 ++++++---- src/automation/heartbeat.test.ts | 55 +++++++++++++------ src/automation/webhooks.test.ts | 42 +++++++------- src/frontends/tui/minimal.login.test.ts | 51 +++++++++++++---- src/gateway/tailscale.test.ts | 42 ++++++++------ 7 files changed, 157 insertions(+), 85 deletions(-) diff --git a/docs/plans/analysis/2026-02-16-codebase-audit-report.md b/docs/plans/analysis/2026-02-16-codebase-audit-report.md index 49fd2d6..0342602 100644 --- a/docs/plans/analysis/2026-02-16-codebase-audit-report.md +++ b/docs/plans/analysis/2026-02-16-codebase-audit-report.md @@ -20,7 +20,7 @@ Scope: Production-risk-first audit of bugs, code improvements, and feature oppor - ✅ F-015 addressed: retry defaults no longer classify timeout-style failures as non-retryable, improving resilience for transient timeout conditions. - ✅ F-011 addressed: Slack user-name resolution now uses bounded TTL+LRU caching to prevent unbounded growth. - ✅ F-013 addressed: shared channel utilities now cover reset normalization/building plus reusable allowlist, mention-gating, and pairing-gating flows across Discord/Slack/WhatsApp adapters. -- ◑ F-004 partially addressed: lint error baseline is restored (`pnpm lint` now passes with 0 errors) and warning burn-down has progressed from `466` to `323`; additional warning debt remains. +- ◑ F-004 partially addressed: lint error baseline is restored (`pnpm lint` now passes with 0 errors) and warning burn-down has progressed from `466` to `247`; additional warning debt remains. ## Executive Summary @@ -28,7 +28,7 @@ Current health snapshot: - `pnpm typecheck`: passing - `pnpm build`: passing - `pnpm test:run`: passing (`140/140` files, `1773/1773` tests) -- `pnpm lint`: passing with warnings only (`0 errors`, `323 warnings`) +- `pnpm lint`: passing with warnings only (`0 errors`, `247 warnings`) Top conclusions: - A critical Web UI security issue exists in markdown rendering (unsanitized HTML insertion). @@ -126,7 +126,7 @@ Remediation update (2026-02-16): - Severity: Medium - Impact: CI noise, reduced confidence in static analysis, and slower defect detection. - Evidence: - - `pnpm -s lint` => `0 errors`, `323 warnings` + - `pnpm -s lint` => `0 errors`, `247 warnings` - Error concentration: - `src/daemon/models.ts` (90 errors) - `src/cli/tui.ts` (25 errors) @@ -145,10 +145,15 @@ Remediation update (2026-02-16): Remediation update (2026-02-16): - Stage 1 complete: fixed all error-level ESLint violations in impacted high-error files so `pnpm lint` now passes with `0` errors. -- Stage 2 in progress: warning-burn-down reduced to `323` warnings via targeted hotspot cleanup in: +- Stage 2 in progress: warning-burn-down reduced to `247` warnings via targeted hotspot cleanup in: - `src/gateway/handlers/handlers.test.ts` - `src/daemon/routing.test.ts` - `src/frontends/tui/minimal.test.ts` + - `src/gateway/tailscale.test.ts` + - `src/automation/webhooks.test.ts` + - `src/automation/cron.test.ts` + - `src/automation/heartbeat.test.ts` + - `src/frontends/tui/minimal.login.test.ts` ### F-005 Medium: ESLint browser globals mismatch causes avoidable UI lint failures @@ -453,7 +458,7 @@ pnpm -s lint Observed outcomes: - Typecheck/build/test: passing. -- Lint: passing with warnings only (`0` errors, `323` warnings). +- Lint: passing with warnings only (`0` errors, `247` warnings). Historical pre-remediation lint error concentration snapshot: - `src/daemon/models.ts`: 90 errors diff --git a/docs/plans/state.json b/docs/plans/state.json index a10844a..afe2e4f 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2652,7 +2652,7 @@ "status": "in_progress", "date": "2026-02-16", "updated": "2026-02-16", - "summary": "Continued stage-2 lint warning reduction with hotspot-focused cleanup in `gateway/handlers/handlers.test.ts`, `daemon/routing.test.ts`, and `frontends/tui/minimal.test.ts`. Replaced broad `any` casts with typed helper casts/unknown-path accessors and removed non-null assertions in routing tests. Warning count reduced from 466 to 323 (143 warnings burned down) with lint/test suites still green.", + "summary": "Continued stage-2 lint warning reduction with hotspot-focused cleanup in `gateway/handlers/handlers.test.ts`, `daemon/routing.test.ts`, `frontends/tui/minimal.test.ts`, `gateway/tailscale.test.ts`, `automation/webhooks.test.ts`, `automation/cron.test.ts`, `automation/heartbeat.test.ts`, and `frontends/tui/minimal.login.test.ts`. Replaced broad `any` casts with typed helper casts/unknown-path accessors and removed non-null assertions in high-warning tests. Warning count reduced from 466 to 247 (219 warnings burned down) with lint/test suites still green.", "files_modified": [ "src/tools/builtin/browser/tools.test.ts", "src/channels/telegram/adapter.test.ts", @@ -2662,9 +2662,14 @@ "src/gateway/handlers/handlers.test.ts", "src/daemon/routing.test.ts", "src/frontends/tui/minimal.test.ts", + "src/gateway/tailscale.test.ts", + "src/automation/webhooks.test.ts", + "src/automation/cron.test.ts", + "src/automation/heartbeat.test.ts", + "src/frontends/tui/minimal.login.test.ts", "docs/plans/analysis/2026-02-16-codebase-audit-report.md" ], - "test_status": "pnpm test:run src/channels/utils.test.ts src/channels/discord/adapter.test.ts src/channels/slack/adapter.test.ts src/channels/whatsapp/adapter.test.ts src/daemon/routing.test.ts src/gateway/handlers/handlers.test.ts src/frontends/tui/minimal.test.ts + pnpm lint passing (0 errors, 323 warnings)" + "test_status": "pnpm test:run src/channels/utils.test.ts src/channels/discord/adapter.test.ts src/channels/slack/adapter.test.ts src/channels/whatsapp/adapter.test.ts src/daemon/routing.test.ts src/gateway/handlers/handlers.test.ts src/frontends/tui/minimal.test.ts src/gateway/tailscale.test.ts src/automation/webhooks.test.ts src/automation/cron.test.ts src/automation/heartbeat.test.ts src/frontends/tui/minimal.login.test.ts + pnpm lint passing (0 errors, 247 warnings)" } }, "overall_progress": { diff --git a/src/automation/cron.test.ts b/src/automation/cron.test.ts index cc52911..08a4785 100644 --- a/src/automation/cron.test.ts +++ b/src/automation/cron.test.ts @@ -3,6 +3,10 @@ import { CronScheduler } from './cron.js'; import type { CronJobConfig } from '../config/schema.js'; import type { InboundMessage } from '../channels/types.js'; +function asCronChannelRegistry(value: unknown): ConstructorParameters[1] { + return value as ConstructorParameters[1]; +} + function makeCronJob(overrides?: Partial): CronJobConfig { return { name: 'test-job', @@ -31,19 +35,19 @@ describe('CronScheduler', () => { }); it('implements ChannelAdapter interface', () => { - scheduler = new CronScheduler([], mockChannelRegistry as any); + scheduler = new CronScheduler([], asCronChannelRegistry(mockChannelRegistry)); expect(scheduler.name).toBe('cron'); expect(scheduler.status).toBe('disconnected'); }); it('status changes to connected after connect()', async () => { - scheduler = new CronScheduler([], mockChannelRegistry as any); + scheduler = new CronScheduler([], asCronChannelRegistry(mockChannelRegistry)); await scheduler.connect(); expect(scheduler.status).toBe('connected'); }); it('status changes to disconnected after disconnect()', async () => { - scheduler = new CronScheduler([], mockChannelRegistry as any); + scheduler = new CronScheduler([], asCronChannelRegistry(mockChannelRegistry)); await scheduler.connect(); await scheduler.disconnect(); expect(scheduler.status).toBe('disconnected'); @@ -51,7 +55,7 @@ describe('CronScheduler', () => { it('skips disabled jobs', async () => { const jobs = [makeCronJob({ enabled: false })]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -63,7 +67,7 @@ describe('CronScheduler', () => { it('fires a message when triggerJob is called', async () => { const jobs = [makeCronJob()]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -80,7 +84,7 @@ describe('CronScheduler', () => { it('uses isolated sender IDs when delivery mode is isolated_job', async () => { const jobs = [makeCronJob()]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any, 'isolated_job'); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry), 'isolated_job'); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -100,7 +104,7 @@ describe('CronScheduler', () => { mockChannelRegistry.get.mockReturnValue(mockOutputAdapter); const jobs = [makeCronJob()]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); await scheduler.connect(); await scheduler.send('test-job', { text: 'Agent response' }); @@ -114,7 +118,7 @@ describe('CronScheduler', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const jobs = [makeCronJob()]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); await scheduler.connect(); await scheduler.send('test-job', { text: 'Agent response' }); @@ -127,7 +131,7 @@ describe('CronScheduler', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const jobs = [makeCronJob()]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); await scheduler.connect(); await scheduler.send('nonexistent-job', { text: 'response' }); @@ -138,7 +142,7 @@ describe('CronScheduler', () => { it('triggerJob includes model_tier in metadata when configured', () => { const jobs = [makeCronJob({ model_tier: 'fast' })]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -151,7 +155,7 @@ describe('CronScheduler', () => { it('triggerJob metadata.modelTier is undefined when not configured', () => { const jobs = [makeCronJob()]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -167,7 +171,7 @@ describe('CronScheduler', () => { makeCronJob({ name: 'job-a' }), makeCronJob({ name: 'job-b', enabled: false }), ]; - scheduler = new CronScheduler(jobs, mockChannelRegistry as any); + scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry)); const names = scheduler.getJobNames(); expect(names).toEqual(['job-a', 'job-b']); diff --git a/src/automation/heartbeat.test.ts b/src/automation/heartbeat.test.ts index c988ced..3e8acf7 100644 --- a/src/automation/heartbeat.test.ts +++ b/src/automation/heartbeat.test.ts @@ -1,7 +1,8 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, afterEach } from 'vitest'; import { HeartbeatMonitor, parseInterval } from './heartbeat.js'; import type { HeartbeatDeps } from './heartbeat.js'; import type { HeartbeatConfig } from '../config/schema.js'; +import type { ChannelAdapter } from '../channels/types.js'; function makeConfig(overrides?: Partial): HeartbeatConfig { return { @@ -21,8 +22,8 @@ function makeDeps(overrides?: Partial): HeartbeatDeps { modelRouter: { getTier: () => 'default' }, channelLister: { list: () => [ - { name: 'telegram', status: 'connected' } as any, - { name: 'webchat', status: 'connected' } as any, + makeChannelAdapter('telegram', 'connected'), + makeChannelAdapter('webchat', 'connected'), ], }, memoryDir: '/tmp/flynn-test-memory', @@ -32,6 +33,17 @@ function makeDeps(overrides?: Partial): HeartbeatDeps { }; } +function makeChannelAdapter(name: string, status: ChannelAdapter['status']): ChannelAdapter { + return { + name, + status, + connect: async () => {}, + disconnect: async () => {}, + send: async () => {}, + onMessage: () => {}, + }; +} + describe('parseInterval', () => { it('parses seconds', () => { expect(parseInterval('60s')).toBe(60000); @@ -150,8 +162,8 @@ describe('HeartbeatMonitor', () => { const lastResult = monitor.getLastResult(); expect(lastResult).toBeDefined(); - expect(lastResult!.checks).toHaveLength(1); - expect(lastResult!.timestamp).toBeGreaterThan(0); + expect(lastResult?.checks).toHaveLength(1); + expect(lastResult?.timestamp ?? 0).toBeGreaterThan(0); }); it('notification sent after failure_threshold consecutive failures', async () => { @@ -294,7 +306,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'model')!; + const check = result.checks.find((c) => c.name === 'model'); + if (!check) {throw new Error('Expected model check result');} expect(check.healthy).toBe(true); expect(check.message).toContain('fast'); }); @@ -307,7 +320,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'model')!; + const check = result.checks.find((c) => c.name === 'model'); + if (!check) {throw new Error('Expected model check result');} expect(check.healthy).toBe(false); }); }); @@ -318,15 +332,16 @@ describe('HeartbeatMonitor', () => { config: makeConfig({ checks: ['channels'] }), channelLister: { list: () => [ - { name: 'telegram', status: 'connected' } as any, - { name: 'webchat', status: 'disconnected' } as any, + makeChannelAdapter('telegram', 'connected'), + makeChannelAdapter('webchat', 'disconnected'), ], }, }); monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'channels')!; + const check = result.checks.find((c) => c.name === 'channels'); + if (!check) {throw new Error('Expected channels check result');} expect(check.healthy).toBe(true); expect(check.message).toContain('1/2 connected'); expect(check.message).toContain('webchat'); @@ -337,14 +352,15 @@ describe('HeartbeatMonitor', () => { config: makeConfig({ checks: ['channels'] }), channelLister: { list: () => [ - { name: 'telegram', status: 'disconnected' } as any, + makeChannelAdapter('telegram', 'disconnected'), ], }, }); monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'channels')!; + const check = result.checks.find((c) => c.name === 'channels'); + if (!check) {throw new Error('Expected channels check result');} expect(check.healthy).toBe(false); }); }); @@ -358,7 +374,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'memory')!; + const check = result.checks.find((c) => c.name === 'memory'); + if (!check) {throw new Error('Expected memory check result');} expect(check.healthy).toBe(true); expect(check.message).toContain('disabled'); }); @@ -371,7 +388,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'memory')!; + const check = result.checks.find((c) => c.name === 'memory'); + if (!check) {throw new Error('Expected memory check result');} expect(check.healthy).toBe(false); }); }); @@ -385,7 +403,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'disk')!; + const check = result.checks.find((c) => c.name === 'disk'); + if (!check) {throw new Error('Expected disk check result');} expect(check.healthy).toBe(true); expect(check.message).toContain('MB available'); }); @@ -398,7 +417,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'disk')!; + const check = result.checks.find((c) => c.name === 'disk'); + if (!check) {throw new Error('Expected disk check result');} expect(check.healthy).toBe(false); expect(check.message).toContain('Low disk space'); }); @@ -411,7 +431,8 @@ describe('HeartbeatMonitor', () => { monitor = new HeartbeatMonitor(deps); const result = await monitor.runChecks(); - const check = result.checks.find((c) => c.name === 'disk')!; + const check = result.checks.find((c) => c.name === 'disk'); + if (!check) {throw new Error('Expected disk check result');} expect(check.healthy).toBe(false); }); }); diff --git a/src/automation/webhooks.test.ts b/src/automation/webhooks.test.ts index 70a7975..36e93a0 100644 --- a/src/automation/webhooks.test.ts +++ b/src/automation/webhooks.test.ts @@ -6,6 +6,10 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { createHmac } from 'crypto'; import { EventEmitter } from 'events'; +function asWebhookChannelLookup(value: unknown): ConstructorParameters[1] { + return value as ConstructorParameters[1]; +} + function makeWebhook(overrides?: Partial): WebhookConfig { return { name: 'test-hook', @@ -18,8 +22,8 @@ function makeWebhook(overrides?: Partial): WebhookConfig { /** Create a mock IncomingMessage that emits the given body. */ function mockRequest(body: string, headers: Record = {}): IncomingMessage { - const emitter = new EventEmitter(); - (emitter as any).headers = headers; + const emitter = new EventEmitter() as EventEmitter & Partial; + emitter.headers = headers; // Simulate data arriving next tick process.nextTick(() => { emitter.emit('data', Buffer.from(body)); @@ -30,7 +34,7 @@ function mockRequest(body: string, headers: Record = {}): Incomi /** Create a mock ServerResponse that captures writeHead and end calls. */ function mockResponse(): ServerResponse & { statusCode_: number; body_: string; headers_: Record } { - const res: any = { + const res = { statusCode_: 0, body_: '', headers_: {}, @@ -44,7 +48,7 @@ function mockResponse(): ServerResponse & { statusCode_: number; body_: string; return res; }, }; - return res; + return res as ServerResponse & { statusCode_: number; body_: string; headers_: Record }; } describe('WebhookHandler', () => { @@ -64,19 +68,19 @@ describe('WebhookHandler', () => { }); it('implements ChannelAdapter interface', () => { - handler = new WebhookHandler([], mockChannelRegistry as any); + handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry)); expect(handler.name).toBe('webhook'); expect(handler.status).toBe('disconnected'); }); it('status changes to connected after connect()', async () => { - handler = new WebhookHandler([], mockChannelRegistry as any); + handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); expect(handler.status).toBe('connected'); }); it('status changes to disconnected after disconnect()', async () => { - handler = new WebhookHandler([], mockChannelRegistry as any); + handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); await handler.disconnect(); expect(handler.status).toBe('disconnected'); @@ -87,7 +91,7 @@ describe('WebhookHandler', () => { makeWebhook({ name: 'hook-a' }), makeWebhook({ name: 'hook-b', enabled: false }), ]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); const names = handler.getWebhookNames(); expect(names).toEqual(['hook-a', 'hook-b']); @@ -95,7 +99,7 @@ describe('WebhookHandler', () => { it('handleRequest produces correct InboundMessage', async () => { const webhooks = [makeWebhook()]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); const messages: InboundMessage[] = []; handler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -116,7 +120,7 @@ describe('WebhookHandler', () => { it('handleRequest uses isolated sender IDs when delivery mode is isolated_job', async () => { const webhooks = [makeWebhook()]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any, 'isolated_job'); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry), 'isolated_job'); const messages: InboundMessage[] = []; handler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -134,7 +138,7 @@ describe('WebhookHandler', () => { }); it('returns false for unknown webhook', async () => { - handler = new WebhookHandler([], mockChannelRegistry as any); + handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); const req = mockRequest('test'); @@ -148,7 +152,7 @@ describe('WebhookHandler', () => { it('returns false for disabled webhook', async () => { const webhooks = [makeWebhook({ enabled: false })]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); const req = mockRequest('test'); @@ -163,7 +167,7 @@ describe('WebhookHandler', () => { it('verifies valid HMAC signature', async () => { const secret = 'my-secret-key'; const webhooks = [makeWebhook({ secret })]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); const messages: InboundMessage[] = []; handler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -185,7 +189,7 @@ describe('WebhookHandler', () => { it('rejects invalid HMAC signature', async () => { const secret = 'my-secret-key'; const webhooks = [makeWebhook({ secret })]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); const messages: InboundMessage[] = []; handler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -204,7 +208,7 @@ describe('WebhookHandler', () => { it('rejects missing HMAC signature when secret is configured', async () => { const secret = 'my-secret-key'; const webhooks = [makeWebhook({ secret })]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); const messages: InboundMessage[] = []; handler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -222,7 +226,7 @@ describe('WebhookHandler', () => { it('rejects oversized payloads with 413', async () => { const webhooks = [makeWebhook()]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any, 'shared_session', 16); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry), 'shared_session', 16); const messages: InboundMessage[] = []; handler.onMessage((msg: InboundMessage) => messages.push(msg)); @@ -245,7 +249,7 @@ describe('WebhookHandler', () => { mockChannelRegistry.get.mockReturnValue(mockOutputAdapter); const webhooks = [makeWebhook()]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); await handler.send('test-hook', { text: 'Agent response' }); @@ -259,7 +263,7 @@ describe('WebhookHandler', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const webhooks = [makeWebhook()]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); await handler.send('test-hook', { text: 'Agent response' }); @@ -272,7 +276,7 @@ describe('WebhookHandler', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const webhooks = [makeWebhook()]; - handler = new WebhookHandler(webhooks, mockChannelRegistry as any); + handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry)); await handler.connect(); await handler.send('nonexistent-hook', { text: 'response' }); diff --git a/src/frontends/tui/minimal.login.test.ts b/src/frontends/tui/minimal.login.test.ts index 066f3ed..36340ea 100644 --- a/src/frontends/tui/minimal.login.test.ts +++ b/src/frontends/tui/minimal.login.test.ts @@ -1,4 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { ManagedSession } from '../../session/index.js'; +import type { ModelClient } from '../../models/types.js'; +import type { ModelRouter } from '../../models/router.js'; const { mockLoadStoredAnthropicAuth, @@ -33,6 +36,30 @@ vi.mock('node:readline', () => ({ emitKeypressEvents: vi.fn(), })); +function asSession(value: unknown): ManagedSession { + return value as ManagedSession; +} + +function asModelClient(value: unknown): ModelClient { + return value as ModelClient; +} + +function asModelRouter(value: unknown): ModelRouter { + return value as ModelRouter; +} + +function minimalTuiPrivates(value: unknown): { + rl: { pause: () => void; resume: () => void }; + prompt: (text: string) => Promise; + handleLoginCommand: (provider: string) => Promise; +} { + return value as { + rl: { pause: () => void; resume: () => void }; + prompt: (text: string) => Promise; + handleLoginCommand: (provider: string) => Promise; + }; +} + describe('MinimalTui login re-auth confirmation', () => { beforeEach(() => { vi.clearAllMocks(); @@ -57,20 +84,20 @@ describe('MinimalTui login re-auth confirmation', () => { }; const tui = new MinimalTui({ - session: mockSession as any, - modelClient: {} as any, - modelRouter: {} as any, + session: asSession(mockSession), + modelClient: asModelClient({}), + modelRouter: asModelRouter({}), systemPrompt: 'test', }); - (tui as any).rl = { pause: vi.fn(), resume: vi.fn() }; - const promptMock = vi.spyOn(tui as any, 'prompt') + minimalTuiPrivates(tui).rl = { pause: vi.fn(), resume: vi.fn() }; + const promptMock = vi.spyOn(minimalTuiPrivates(tui), 'prompt') .mockResolvedValueOnce('') // default -> API key path .mockResolvedValueOnce('n'); // confirmation const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); - await (tui as any).handleLoginCommand('anthropic'); + await minimalTuiPrivates(tui).handleLoginCommand('anthropic'); expect(promptMock).toHaveBeenCalled(); expect(mockStoreAnthropicAuth).not.toHaveBeenCalled(); @@ -98,23 +125,23 @@ describe('MinimalTui login re-auth confirmation', () => { }; const tui = new MinimalTui({ - session: mockSession as any, - modelClient: {} as any, - modelRouter: {} as any, + session: asSession(mockSession), + modelClient: asModelClient({}), + modelRouter: asModelRouter({}), systemPrompt: 'test', }); const pause = vi.fn(); const resume = vi.fn(); - (tui as any).rl = { pause, resume }; - vi.spyOn(tui as any, 'prompt') + minimalTuiPrivates(tui).rl = { pause, resume }; + vi.spyOn(minimalTuiPrivates(tui), 'prompt') .mockResolvedValueOnce('') // default -> API key path .mockResolvedValueOnce('y'); // confirmation const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); - await (tui as any).handleLoginCommand('anthropic'); + await minimalTuiPrivates(tui).handleLoginCommand('anthropic'); expect(mockStoreAnthropicAuth).toHaveBeenCalledWith('new-anthropic-key'); expect(pause).toHaveBeenCalled(); diff --git a/src/gateway/tailscale.test.ts b/src/gateway/tailscale.test.ts index 2ab7347..1a5043d 100644 --- a/src/gateway/tailscale.test.ts +++ b/src/gateway/tailscale.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'; import { execFile } from 'child_process'; +import type { ChildProcess } from 'child_process'; // Mock child_process before importing module vi.mock('child_process', () => ({ @@ -7,6 +8,11 @@ vi.mock('child_process', () => ({ })); const mockExecFile = vi.mocked(execFile); +type ExecFileCallback = (error: Error | null, stdout: string, stderr: string) => void; + +function mockChildProcess(): ChildProcess { + return {} as ChildProcess; +} describe('tailscale', () => { // Import after mocking @@ -28,13 +34,13 @@ describe('tailscale', () => { describe('isTailscaleAvailable', () => { it('returns available when tailscale CLI works', async () => { mockExecFile - .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, '1.62.0', ''); - return {} as any; + return mockChildProcess(); }) - .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, '{}', ''); - return {} as any; + return mockChildProcess(); }); const result = await isTailscaleAvailable(); @@ -43,9 +49,9 @@ describe('tailscale', () => { }); it('returns unavailable when tailscale CLI fails', async () => { - mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(new Error('command not found'), '', 'command not found'); - return {} as any; + return mockChildProcess(); }); const result = await isTailscaleAvailable(); @@ -58,14 +64,14 @@ describe('tailscale', () => { it('calls tailscale serve with correct args', async () => { mockExecFile // serve command - .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, '', ''); - return {} as any; + return mockChildProcess(); }) // status for hostname - .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), ''); - return {} as any; + return mockChildProcess(); }); const url = await startTailscaleServe({ localPort: 18800 }); @@ -78,13 +84,13 @@ describe('tailscale', () => { it('uses custom serve port', async () => { mockExecFile - .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, '', ''); - return {} as any; + return mockChildProcess(); }) - .mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + .mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), ''); - return {} as any; + return mockChildProcess(); }); const url = await startTailscaleServe({ localPort: 18800, servePort: 8443 }); @@ -97,9 +103,9 @@ describe('tailscale', () => { describe('stopTailscaleServe', () => { it('calls tailscale serve off', async () => { - mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(null, '', ''); - return {} as any; + return mockChildProcess(); }); await stopTailscaleServe({ localPort: 18800 }); @@ -111,9 +117,9 @@ describe('tailscale', () => { }); it('does not throw on failure', async () => { - mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => { + mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => { callback(new Error('failed'), '', 'failed'); - return {} as any; + return mockChildProcess(); }); // Should not throw