fix: resolve strict typecheck fallout in setup, routing, and tests
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+38
-6
@@ -14,8 +14,40 @@ interface EmbeddingConfig {
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export interface SetupConfig {
|
||||
log_level?: string;
|
||||
models: Record<string, ProviderConfig & Record<string, unknown>>;
|
||||
server: {
|
||||
port?: number;
|
||||
localhost?: boolean;
|
||||
token?: string;
|
||||
lock?: boolean;
|
||||
tailscale?: { serve?: boolean };
|
||||
} & Record<string, unknown>;
|
||||
hooks?: Record<string, unknown>;
|
||||
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<Record<string, unknown>>;
|
||||
webhooks?: Array<Record<string, unknown>>;
|
||||
gmail?: { enabled?: boolean };
|
||||
gcal?: { enabled?: boolean };
|
||||
gdocs?: { enabled?: boolean };
|
||||
gdrive?: { enabled?: boolean };
|
||||
gtasks?: { enabled?: boolean };
|
||||
heartbeat?: { enabled?: boolean };
|
||||
} & Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export class ConfigBuilder {
|
||||
private config: Record<string, unknown>;
|
||||
private config: SetupConfig;
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
@@ -32,13 +64,13 @@ export class ConfigBuilder {
|
||||
|
||||
static fromObject(obj: Record<string, unknown>): 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<string, unknown>;
|
||||
const entry: Record<string, unknown> = { provider: cfg.provider, model: cfg.model };
|
||||
const models = (this.config.models ?? {}) as SetupConfig['models'];
|
||||
const entry: ProviderConfig & Record<string, unknown> = { 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<string, unknown> {
|
||||
return structuredClone(this.config) as Record<string, unknown>;
|
||||
build(): SetupConfig {
|
||||
return structuredClone(this.config) as SetupConfig;
|
||||
}
|
||||
|
||||
toYaml(): string {
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export function renderSummary(config: Record<string, unknown>): 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, unknown>): 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');}
|
||||
|
||||
+38
-34
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Parameters<typeof execFile>[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 });
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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.' },
|
||||
];
|
||||
|
||||
@@ -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.' },
|
||||
];
|
||||
|
||||
@@ -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<string, unknown>)['baseURL']).toBe('https://example.com/v1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -400,7 +400,7 @@ describe('ToolExecutor', () => {
|
||||
const fakeManager = {
|
||||
getOrCreate: async () => fakeSandbox,
|
||||
} as { getOrCreate: (sessionId: string) => Promise<typeof fakeSandbox> };
|
||||
executor.setSandboxManager(fakeManager);
|
||||
executor.setSandboxManager(fakeManager as unknown as Parameters<typeof executor.setSandboxManager>[0]);
|
||||
|
||||
const result = await executor.execute('shell.exec', { command: 'echo hi' }, {
|
||||
executionEnvironment: 'sandbox',
|
||||
|
||||
@@ -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<string>()),
|
||||
getEffectiveProfile: vi.fn<() => 'full'>(() => 'full'),
|
||||
} as unknown as ToolPolicy;
|
||||
reg.setPolicy(mockPolicy);
|
||||
|
||||
const cloned = reg.clone();
|
||||
|
||||
Reference in New Issue
Block a user