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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function readRepo(path: string): Promise<string> {
* `isImplemented(X) === true`
*
* Runtime-labeled platforms must ALSO be listed in
* `BOT_DELIVERY_PROVIDERS` (`packages/core/src/settings.ts`) so plan
* `BOT_DELIVERY_PROVIDERS` (`packages/core/src/bot-chat-settings.ts`) so plan
* reminders can target them. The reverse is NOT required: a platform
* can be delivery-capable without being `'runtime'` — WeChat is a
* delivery target via the optional local wechat-bridge, but its
Expand Down
9 changes: 3 additions & 6 deletions apps/desktop/src/main/network-settings-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import type { AppSettings } from '@maka/core';
import {
NETWORK_DEFAULTS,
maskSensitive,
type NetworkSettings as ContractNetworkSettings,
type RuntimeNetworkSettings,
} from '@maka/core/settings/network-settings';

type StoredNetworkSettings = AppSettings['network'];

export function toContractNetworkSettings(network: StoredNetworkSettings): ContractNetworkSettings {
export function toContractNetworkSettings(network: StoredNetworkSettings): RuntimeNetworkSettings {
const proxy = network.proxy;
return {
...NETWORK_DEFAULTS,
Expand All @@ -24,9 +24,7 @@ export function toContractNetworkSettings(network: StoredNetworkSettings): Contr
};
}



export function maskNetworkSettings(settings: ContractNetworkSettings): ContractNetworkSettings {
export function maskNetworkSettings(settings: RuntimeNetworkSettings): RuntimeNetworkSettings {
return {
...settings,
proxy: {
Expand All @@ -35,4 +33,3 @@ export function maskNetworkSettings(settings: ContractNetworkSettings): Contract
},
};
}

72 changes: 72 additions & 0 deletions packages/core/src/__tests__/bot-chat-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import {
createDefaultBotChatSettings,
mergeBotChatSettings,
normalizeBotChatSettings,
parseAllowedUserIdsFromText,
} from '../bot-chat-settings.js';

describe('bot chat settings owner', () => {
test('preserves provider-specific defaults', () => {
const settings = createDefaultBotChatSettings();

assert.equal(settings.channels.telegram.proxyUrl, 'http://127.0.0.1:7890');
assert.equal(settings.channels.wechat.webhookUrl, 'http://127.0.0.1:18400');
assert.equal(settings.channels.discord.readiness, 'scaffolded');
});

test('normalizes an explicitly patched allowlist without touching it on unrelated patches', () => {
const defaults = createDefaultBotChatSettings();
const withAllowlist = mergeBotChatSettings(defaults, {
channels: {
telegram: { allowedUserIds: [' 123 ', '456', '123', ''] },
},
});
const tokenPatched = mergeBotChatSettings(withAllowlist, {
channels: { telegram: { token: 'telegram-token' } },
});

assert.deepEqual(withAllowlist.channels.telegram.allowedUserIds, ['123', '456']);
assert.strictEqual(
tokenPatched.channels.telegram.allowedUserIds,
withAllowlist.channels.telegram.allowedUserIds,
);
});

test('preserves legacy readiness derivation and downgrade-only coercion', () => {
const legacy = createDefaultBotChatSettings();
delete (legacy.channels.telegram as Partial<typeof legacy.channels.telegram>).readiness;
legacy.channels.telegram.enabled = true;
legacy.channels.telegram.connected = true;
legacy.channels.telegram.token = 'telegram-token';

const legacyNormalized = normalizeBotChatSettings(legacy, legacy);
assert.equal(legacyNormalized.channels.telegram.readiness, 'credentials_valid');

legacyNormalized.channels.telegram.token = '';
legacyNormalized.channels.telegram.readiness = 'operational';
const cleared = normalizeBotChatSettings(legacyNormalized, legacyNormalized);
assert.equal(cleared.channels.telegram.readiness, 'scaffolded');

cleared.channels.telegram.token = 'new-token';
cleared.channels.telegram.readiness = 'scaffolded';
const credentialed = normalizeBotChatSettings(cleared, cleared);
assert.equal(credentialed.channels.telegram.readiness, 'scaffolded');
});

test('parses textarea allowlists with trim, deduplication, and the defensive cap', () => {
const raw = [
' 123 ',
'456',
'123',
'',
...Array.from({ length: 60 }, (_, i) => `user-${i}`),
].join('\n');
const parsed = parseAllowedUserIdsFromText(raw);

assert.equal(parsed.length, 50);
assert.deepEqual(parsed.slice(0, 3), ['123', '456', 'user-0']);
assert.equal(parsed.at(-1), 'user-47');
});
});
6 changes: 5 additions & 1 deletion packages/core/src/__tests__/plan-reminders.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { BOT_DELIVERY_PROVIDERS, BOT_PROVIDERS, isBotDeliveryProvider } from '../settings.js';
import {
BOT_DELIVERY_PROVIDERS,
BOT_PROVIDERS,
isBotDeliveryProvider,
} from '../bot-chat-settings.js';
import {
isPlanReminderDue,
nextPlanReminderStateAfterTrigger,
Expand Down
133 changes: 133 additions & 0 deletions packages/core/src/__tests__/settings-extraction-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { describe, test } from 'node:test';
import * as botChatSettings from '../bot-chat-settings.js';
import * as core from '../index.js';
import {
NETWORK_DEFAULTS,
type NetworkSettings as LegacyRuntimeNetworkSettings,
type RuntimeNetworkSettings,
} from '../settings/network-settings.js';
import * as settings from '../settings.js';
import type {
AppNetworkSettings,
AppSettings,
NetworkSettings as LegacyAppNetworkSettings,
} from '../settings.js';

const REPO_ROOT = resolveRepoRoot();

async function readRepo(path: string): Promise<string> {
return readFile(join(REPO_ROOT, path), 'utf8');
}

function resolveRepoRoot(): string {
const cwd = resolve(process.cwd());
if (existsSync(join(cwd, 'packages', 'core', 'src', 'settings.ts'))) return cwd;
const fromWorkspace = resolve(cwd, '..', '..');
if (existsSync(join(fromWorkspace, 'packages', 'core', 'src', 'settings.ts')))
return fromWorkspace;
return cwd;
}

describe('settings domain extraction contract', () => {
test('keeps the existing settings and root bot-chat exports compatible', () => {
assert.strictEqual(settings.BOT_READINESS_STATES, botChatSettings.BOT_READINESS_STATES);
assert.strictEqual(settings.BOT_PROVIDERS, botChatSettings.BOT_PROVIDERS);
assert.strictEqual(settings.BOT_DELIVERY_PROVIDERS, botChatSettings.BOT_DELIVERY_PROVIDERS);
assert.strictEqual(settings.MAX_ALLOWED_USER_IDS, botChatSettings.MAX_ALLOWED_USER_IDS);
assert.strictEqual(settings.createDefaultBotChannel, botChatSettings.createDefaultBotChannel);
assert.strictEqual(settings.hasBotChannelCredentials, botChatSettings.hasBotChannelCredentials);
assert.strictEqual(settings.normalizeAllowedUserIds, botChatSettings.normalizeAllowedUserIds);
assert.strictEqual(
settings.parseAllowedUserIdsFromText,
botChatSettings.parseAllowedUserIdsFromText,
);
assert.strictEqual(core.BOT_PROVIDERS, botChatSettings.BOT_PROVIDERS);
assert.strictEqual(core.createDefaultBotChannel, botChatSettings.createDefaultBotChannel);
});

test('gives persisted and runtime network contracts distinct canonical shapes', () => {
const persisted: AppNetworkSettings = settings.createDefaultSettings().network;
const legacy: LegacyAppNetworkSettings = persisted;
const canonicalAgain: AppNetworkSettings = legacy;
const runtime: RuntimeNetworkSettings = NETWORK_DEFAULTS;
const legacyRuntime: LegacyRuntimeNetworkSettings = runtime;
const canonicalRuntimeAgain: RuntimeNetworkSettings = legacyRuntime;
const fromAppSettings: AppSettings['network'] = canonicalAgain;

assert.equal('timeout' in persisted, false);
assert.equal(canonicalRuntimeAgain.timeout, 30_000);
assert.strictEqual(fromAppSettings, persisted);
});

test('keeps ownership in the leaf modules and composition in settings.ts', async () => {
const [aggregate, botOwner, webSearchOwner, networkOwner, barrel] = await Promise.all([
readRepo('packages/core/src/settings.ts'),
readRepo('packages/core/src/bot-chat-settings.ts'),
readRepo('packages/core/src/web-search.ts'),
readRepo('packages/core/src/settings/network-settings.ts'),
readRepo('packages/core/src/index.ts'),
]);

assert.match(aggregate, /from '\.\/bot-chat-settings\.js'/);
assert.match(aggregate, /botChat: createDefaultBotChatSettings\(\)/);
assert.match(aggregate, /botChat: mergeBotChatSettings\(current\.botChat, patch\.botChat\)/);
assert.match(aggregate, /botChat: normalizeBotChatSettings\(base\.botChat, value\.botChat\)/);
assert.doesNotMatch(aggregate, /export type BotProvider =/);
assert.doesNotMatch(aggregate, /export interface BotChannelSettings/);
assert.doesNotMatch(aggregate, /function normalizeBotChannel/);
assert.doesNotMatch(aggregate, /function coerceReadinessForCurrentState/);

assert.match(botOwner, /export interface BotChannelSettings/);
assert.match(botOwner, /function normalizeBotChannel/);
assert.match(botOwner, /function coerceReadinessForCurrentState/);
assert.doesNotMatch(botOwner, /from '\.\/settings\.js'/);
assert.doesNotMatch(botOwner, /from 'node:/);

assert.match(
aggregate,
/webSearch: mergeWebSearchSettings\(current\.webSearch, patch\.webSearch\)/,
);
assert.match(aggregate, /webSearch: normalizeWebSearchSettings\(base\.webSearch\)/);
assert.doesNotMatch(aggregate, /function mergeWebSearchSettings/);
assert.doesNotMatch(aggregate, /function normalizeWebSearchSettings/);
assert.match(webSearchOwner, /export function mergeWebSearchSettings/);
assert.match(webSearchOwner, /export function normalizeWebSearchSettings/);
assert.doesNotMatch(webSearchOwner, /from '\.\/settings\.js'/);

assert.match(aggregate, /export interface AppNetworkSettings/);
assert.match(aggregate, /export type NetworkSettings = AppNetworkSettings/);
assert.match(networkOwner, /export interface RuntimeNetworkSettings/);
assert.match(networkOwner, /export type NetworkSettings = RuntimeNetworkSettings/);

assert.match(barrel, /from '\.\/bot-chat-settings\.js'/);
assert.doesNotMatch(barrel, /mergeBotChatSettings|normalizeBotChatSettings/);
});

test('points package-local bot consumers at the owner instead of the aggregate', async () => {
const paths = [
'packages/core/src/bot-events.ts',
'packages/core/src/bot-onboarding.ts',
'packages/core/src/bot-platform-hints.ts',
'packages/core/src/capabilities.ts',
'packages/core/src/plan-reminders.ts',
];
const sources = await Promise.all(paths.map(readRepo));

for (const [index, source] of sources.entries()) {
assert.match(
source,
/from '\.\/bot-chat-settings\.js'/,
`${paths[index]} must import its bot contract from the owner`,
);
assert.doesNotMatch(
source,
/from '\.\/settings\.js'/,
`${paths[index]} must not depend on the aggregate settings module`,
);
}
});
});
116 changes: 9 additions & 107 deletions packages/core/src/__tests__/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,117 +587,19 @@ describe('open gateway settings contract', () => {
expect(patched.openGateway.token).toBe('stored-token');
});

test('web search credential status persists independently from masked key round-trips', () => {
const current = mergeSettings(createDefaultSettings(), {
webSearch: {
providers: {
tavily: {
apiKey: 'stored-key',
credentialStatus: 'valid',
credentialCheckedAt: '2026-05-29T00:00:00.000Z',
},
},
},
});

const patched = mergeSettings(current, {
webSearch: {
providers: {
tavily: {
apiKey: '••••••',
},
},
},
});

expect(patched.webSearch.providers.tavily.apiKey).toBe('stored-key');
expect(patched.webSearch.providers.tavily.credentialSource).toBe('saved');
expect(patched.webSearch.providers.tavily.credentialVersion).toBe(1);
expect(patched.webSearch.providers.tavily.credentialStatus).toBe('valid');
expect(patched.webSearch.providers.tavily.credentialCheckedAt).toBe('2026-05-29T00:00:00.000Z');
});

test('web search credential status resets when the saved key changes', () => {
const current = mergeSettings(createDefaultSettings(), {
webSearch: {
providers: {
tavily: {
apiKey: 'old-key',
credentialStatus: 'valid',
credentialCheckedAt: '2026-05-29T00:00:00.000Z',
},
},
},
});

const patched = mergeSettings(current, {
webSearch: {
providers: {
tavily: {
apiKey: 'new-key',
},
},
},
});

expect(patched.webSearch.providers.tavily.apiKey).toBe('new-key');
expect(patched.webSearch.providers.tavily.credentialSource).toBe('saved');
expect(patched.webSearch.providers.tavily.credentialVersion).toBe(2);
expect(patched.webSearch.providers.tavily.credentialStatus).toBe('untested');
expect(patched.webSearch.providers.tavily.credentialCheckedAt).toBeUndefined();
});

test('web search credential test result is ignored when it targets a stale key version', () => {
const current = mergeSettings(createDefaultSettings(), {
webSearch: {
providers: {
tavily: {
apiKey: 'current-key',
},
},
},
});
const updatedKey = mergeSettings(current, {
webSearch: {
providers: {
tavily: {
apiKey: 'newer-key',
},
},
},
});

const staleResult = mergeSettings(updatedKey, {
webSearch: {
providers: {
tavily: {
credentialVersion: current.webSearch.providers.tavily.credentialVersion,
credentialStatus: 'invalid_credentials',
credentialCheckedAt: '2026-05-29T00:00:00.000Z',
},
},
},
});
const freshResult = mergeSettings(updatedKey, {
test('delegates web search patches and persisted normalization to the web-search owner', () => {
const patched = mergeSettings(createDefaultSettings(), {
webSearch: {
providers: {
tavily: {
credentialVersion: updatedKey.webSearch.providers.tavily.credentialVersion,
credentialStatus: 'valid',
credentialCheckedAt: '2026-05-29T00:01:00.000Z',
},
},
enabled: true,
providers: { tavily: { apiKey: 'stored-key' } },
},
});
const normalized = normalizeSettings(patched);

expect(updatedKey.webSearch.providers.tavily.credentialVersion).toBe(2);
expect(staleResult.webSearch.providers.tavily.credentialSource).toBe('saved');
expect(staleResult.webSearch.providers.tavily.credentialStatus).toBe('untested');
expect(staleResult.webSearch.providers.tavily.credentialCheckedAt).toBeUndefined();
expect(freshResult.webSearch.providers.tavily.credentialStatus).toBe('valid');
expect(freshResult.webSearch.providers.tavily.credentialCheckedAt).toBe(
'2026-05-29T00:01:00.000Z',
);
expect(normalized.webSearch.enabled).toBe(true);
expect(normalized.webSearch.providers.tavily.apiKey).toBe('stored-key');
expect(normalized.webSearch.providers.tavily.credentialSource).toBe('saved');
expect(normalized.webSearch.providers.tavily.credentialVersion).toBe(1);
});

test('workspace instructions are visible settings and default to enabled', () => {
Expand Down
Loading
Loading