mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-20 05:11:48 +00:00
feat(channels): support telegram and imessage
- replace the Discord-only config with multi-channel official channels selection - add Telegram token handling and iMessage platform-aware runtime gating - expand CLI, dashboard, and tests for Telegram, Discord, and iMessage Refs #783
This commit is contained in:
1 parent
4bce1559dd
commit
6f1f032c63
19 files changed
+1416
-725
No files matched your search
@@ -1,133 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
DISCORD_CHANNEL_PLUGIN_SPEC,
|
||||
hasExplicitChannelsFlag,
|
||||
hasExplicitPermissionOverride,
|
||||
isDiscordChannelsSessionSupported,
|
||||
resolveDiscordChannelsSyncConfigDir,
|
||||
resolveDiscordChannelsLaunchPlan,
|
||||
} from '../../../src/channels/discord-channels-runtime';
|
||||
|
||||
describe('discord channels runtime planning', () => {
|
||||
it('supports only native Claude default/account sessions', () => {
|
||||
expect(isDiscordChannelsSessionSupported('claude', 'default')).toBe(true);
|
||||
expect(isDiscordChannelsSessionSupported('claude', 'account')).toBe(true);
|
||||
expect(isDiscordChannelsSessionSupported('claude', 'settings')).toBe(false);
|
||||
expect(isDiscordChannelsSessionSupported('droid', 'default')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects explicit channel and permission overrides', () => {
|
||||
expect(hasExplicitChannelsFlag(['--channels', 'plugin:other'])).toBe(true);
|
||||
expect(hasExplicitChannelsFlag([`--channels=${DISCORD_CHANNEL_PLUGIN_SPEC}`])).toBe(true);
|
||||
expect(hasExplicitChannelsFlag(['--permission-mode', 'acceptEdits'])).toBe(false);
|
||||
|
||||
expect(hasExplicitPermissionOverride(['--dangerously-skip-permissions'])).toBe(true);
|
||||
expect(hasExplicitPermissionOverride(['--permission-mode', 'acceptEdits'])).toBe(true);
|
||||
expect(hasExplicitPermissionOverride(['--permission-mode=acceptEdits'])).toBe(true);
|
||||
});
|
||||
|
||||
it('adds the official plugin flag and optional permission bypass when eligible', () => {
|
||||
const plan = resolveDiscordChannelsLaunchPlan({
|
||||
args: ['--verbose'],
|
||||
config: { enabled: true, unattended: true },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: true,
|
||||
tokenConfigured: true,
|
||||
});
|
||||
|
||||
expect(plan.applied).toBe(true);
|
||||
expect(plan.args).toEqual([
|
||||
'--verbose',
|
||||
'--channels',
|
||||
DISCORD_CHANNEL_PLUGIN_SPEC,
|
||||
'--dangerously-skip-permissions',
|
||||
]);
|
||||
expect(plan.appliedPermissionBypass).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps explicit permission choice and still adds the official channel when possible', () => {
|
||||
const plan = resolveDiscordChannelsLaunchPlan({
|
||||
args: ['--permission-mode', 'acceptEdits'],
|
||||
config: { enabled: true, unattended: true },
|
||||
target: 'claude',
|
||||
profileType: 'account',
|
||||
bunAvailable: true,
|
||||
tokenConfigured: true,
|
||||
});
|
||||
|
||||
expect(plan.applied).toBe(true);
|
||||
expect(plan.args).toEqual([
|
||||
'--permission-mode',
|
||||
'acceptEdits',
|
||||
'--channels',
|
||||
DISCORD_CHANNEL_PLUGIN_SPEC,
|
||||
]);
|
||||
expect(plan.appliedPermissionBypass).toBe(false);
|
||||
});
|
||||
|
||||
it('skips when the session is incompatible or prerequisites are missing', () => {
|
||||
const incompatible = resolveDiscordChannelsLaunchPlan({
|
||||
args: [],
|
||||
config: { enabled: true, unattended: false },
|
||||
target: 'claude',
|
||||
profileType: 'settings',
|
||||
bunAvailable: true,
|
||||
tokenConfigured: true,
|
||||
});
|
||||
const missingBun = resolveDiscordChannelsLaunchPlan({
|
||||
args: [],
|
||||
config: { enabled: true, unattended: false },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: false,
|
||||
tokenConfigured: true,
|
||||
});
|
||||
const missingToken = resolveDiscordChannelsLaunchPlan({
|
||||
args: [],
|
||||
config: { enabled: true, unattended: false },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: true,
|
||||
tokenConfigured: false,
|
||||
});
|
||||
|
||||
expect(incompatible.applied).toBe(false);
|
||||
expect(incompatible.skipMessage).toContain('native Claude default/account sessions');
|
||||
expect(missingBun.applied).toBe(false);
|
||||
expect(missingBun.skipMessage).toContain('Bun is not installed');
|
||||
expect(missingToken.applied).toBe(false);
|
||||
expect(missingToken.skipMessage).toContain('DISCORD_BOT_TOKEN is not configured');
|
||||
});
|
||||
|
||||
it('leaves explicit channel arguments untouched', () => {
|
||||
const plan = resolveDiscordChannelsLaunchPlan({
|
||||
args: ['--channels', 'plugin:custom'],
|
||||
config: { enabled: true, unattended: true },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: true,
|
||||
tokenConfigured: true,
|
||||
});
|
||||
|
||||
expect(plan.applied).toBe(false);
|
||||
expect(plan.args).toEqual(['--channels', 'plugin:custom']);
|
||||
expect(plan.skipMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to process.env.CLAUDE_CONFIG_DIR for sync when no explicit dir is passed', () => {
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
process.env.CLAUDE_CONFIG_DIR = '/tmp/external-claude-config';
|
||||
|
||||
try {
|
||||
expect(resolveDiscordChannelsSyncConfigDir()).toBe('/tmp/external-claude-config');
|
||||
expect(resolveDiscordChannelsSyncConfigDir('/tmp/explicit')).toBe('/tmp/explicit');
|
||||
} finally {
|
||||
if (originalConfigDir !== undefined) {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
clearConfiguredDiscordBotTokenEverywhere,
|
||||
clearConfiguredDiscordBotToken,
|
||||
getDiscordChannelsEnvPath,
|
||||
hasConfiguredDiscordBotToken,
|
||||
readConfiguredDiscordBotToken,
|
||||
readDiscordBotTokenFromEnvContent,
|
||||
setConfiguredDiscordBotToken,
|
||||
syncDiscordChannelsEnvToConfigDir,
|
||||
} from '../../../src/channels/discord-channels-store';
|
||||
|
||||
describe('discord channels token store', () => {
|
||||
let tempHome = '';
|
||||
let originalHome: string | undefined;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-discord-channels-'));
|
||||
originalHome = process.env.HOME;
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.HOME = tempHome;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome !== undefined) process.env.HOME = originalHome;
|
||||
else delete process.env.HOME;
|
||||
|
||||
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||
else delete process.env.CCS_HOME;
|
||||
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes and reads DISCORD_BOT_TOKEN from the canonical Claude channels env file', () => {
|
||||
const envPath = setConfiguredDiscordBotToken('discord-secret');
|
||||
|
||||
expect(envPath).toBe(path.join(tempHome, '.claude', 'channels', 'discord', '.env'));
|
||||
expect(hasConfiguredDiscordBotToken()).toBe(true);
|
||||
expect(readConfiguredDiscordBotToken()).toBe('discord-secret');
|
||||
expect(readDiscordBotTokenFromEnvContent(fs.readFileSync(envPath, 'utf8'))).toBe(
|
||||
'discord-secret'
|
||||
);
|
||||
});
|
||||
|
||||
it('removes only the token entry and deletes the file when nothing remains', () => {
|
||||
const envPath = getDiscordChannelsEnvPath();
|
||||
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
||||
fs.writeFileSync(envPath, '# comment\nDISCORD_BOT_TOKEN=secret\nOTHER_KEY=value\n', 'utf8');
|
||||
|
||||
clearConfiguredDiscordBotToken();
|
||||
expect(fs.readFileSync(envPath, 'utf8')).toBe('# comment\nOTHER_KEY=value\n');
|
||||
|
||||
clearConfiguredDiscordBotToken();
|
||||
fs.writeFileSync(envPath, 'DISCORD_BOT_TOKEN=secret\n', 'utf8');
|
||||
clearConfiguredDiscordBotToken();
|
||||
expect(fs.existsSync(envPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('syncs the canonical env file into an alternate CLAUDE_CONFIG_DIR for account sessions', () => {
|
||||
setConfiguredDiscordBotToken('discord-secret');
|
||||
|
||||
const targetConfigDir = path.join(tempHome, '.ccs', 'instances', 'work');
|
||||
const targetPath = path.join(targetConfigDir, 'channels', 'discord', '.env');
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, '# keep\nOTHER_KEY=value\n', 'utf8');
|
||||
|
||||
const result = syncDiscordChannelsEnvToConfigDir(targetConfigDir);
|
||||
|
||||
expect(result.synced).toBe(true);
|
||||
expect(result.targetPath).toBe(targetPath);
|
||||
expect(fs.readFileSync(targetPath, 'utf8')).toBe(
|
||||
'# keep\nOTHER_KEY=value\n\nDISCORD_BOT_TOKEN=discord-secret\n'
|
||||
);
|
||||
expect(fs.statSync(targetPath).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
|
||||
it('clears previously synced copies across managed Claude config dirs', () => {
|
||||
setConfiguredDiscordBotToken('discord-secret');
|
||||
|
||||
const instanceConfigDir = path.join(tempHome, '.ccs', 'instances', 'work');
|
||||
const instanceEnvPath = path.join(instanceConfigDir, 'channels', 'discord', '.env');
|
||||
|
||||
syncDiscordChannelsEnvToConfigDir(instanceConfigDir);
|
||||
expect(fs.existsSync(instanceEnvPath)).toBe(true);
|
||||
|
||||
const clearedPaths = clearConfiguredDiscordBotTokenEverywhere();
|
||||
|
||||
expect(clearedPaths).toContain(getDiscordChannelsEnvPath());
|
||||
expect(clearedPaths).toContain(instanceEnvPath);
|
||||
expect(fs.existsSync(getDiscordChannelsEnvPath())).toBe(false);
|
||||
expect(fs.existsSync(instanceEnvPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
OFFICIAL_CHANNELS,
|
||||
buildOfficialChannelsArgs,
|
||||
expandOfficialChannelSelection,
|
||||
hasExplicitChannelsFlag,
|
||||
hasExplicitPermissionOverride,
|
||||
isDiscordChannelsSessionSupported,
|
||||
resolveOfficialChannelsLaunchPlan,
|
||||
resolveOfficialChannelsSyncConfigDir,
|
||||
} from '../../../src/channels/official-channels-runtime';
|
||||
|
||||
describe('official channels runtime planning', () => {
|
||||
it('supports only native Claude default/account sessions', () => {
|
||||
expect(isDiscordChannelsSessionSupported('claude', 'default')).toBe(true);
|
||||
expect(isDiscordChannelsSessionSupported('claude', 'account')).toBe(true);
|
||||
expect(isDiscordChannelsSessionSupported('claude', 'settings')).toBe(false);
|
||||
expect(isDiscordChannelsSessionSupported('droid', 'default')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects explicit channel and permission overrides', () => {
|
||||
expect(hasExplicitChannelsFlag(['--channels', 'plugin:other'])).toBe(true);
|
||||
expect(
|
||||
hasExplicitChannelsFlag([`--channels=${OFFICIAL_CHANNELS.discord.pluginSpec}`])
|
||||
).toBe(true);
|
||||
expect(hasExplicitChannelsFlag(['--permission-mode', 'acceptEdits'])).toBe(false);
|
||||
|
||||
expect(hasExplicitPermissionOverride(['--dangerously-skip-permissions'])).toBe(true);
|
||||
expect(hasExplicitPermissionOverride(['--permission-mode', 'acceptEdits'])).toBe(true);
|
||||
expect(hasExplicitPermissionOverride(['--permission-mode=acceptEdits'])).toBe(true);
|
||||
});
|
||||
|
||||
it('expands channel selection and builds runtime argv in stable order', () => {
|
||||
expect(expandOfficialChannelSelection('all')).toEqual(['telegram', 'discord', 'imessage']);
|
||||
expect(expandOfficialChannelSelection('discord,telegram')).toEqual(['telegram', 'discord']);
|
||||
expect(buildOfficialChannelsArgs(['--verbose'], ['telegram', 'discord'], true)).toEqual([
|
||||
'--verbose',
|
||||
'--channels',
|
||||
OFFICIAL_CHANNELS.telegram.pluginSpec,
|
||||
OFFICIAL_CHANNELS.discord.pluginSpec,
|
||||
'--dangerously-skip-permissions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds all ready selected channels and optional permission bypass when eligible', () => {
|
||||
const plan = resolveOfficialChannelsLaunchPlan({
|
||||
args: ['--verbose'],
|
||||
config: { selected: ['telegram', 'discord'], unattended: true },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: true,
|
||||
channelReadiness: {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
imessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(plan.applied).toBe(true);
|
||||
expect(plan.appliedChannels).toEqual(['telegram', 'discord']);
|
||||
expect(plan.wantsPermissionBypass).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps explicit permission choice and still returns ready channels', () => {
|
||||
const plan = resolveOfficialChannelsLaunchPlan({
|
||||
args: ['--permission-mode', 'acceptEdits'],
|
||||
config: { selected: ['discord'], unattended: true },
|
||||
target: 'claude',
|
||||
profileType: 'account',
|
||||
bunAvailable: true,
|
||||
channelReadiness: {
|
||||
telegram: false,
|
||||
discord: true,
|
||||
imessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(plan.applied).toBe(true);
|
||||
expect(plan.appliedChannels).toEqual(['discord']);
|
||||
expect(plan.wantsPermissionBypass).toBe(false);
|
||||
});
|
||||
|
||||
it('skips incompatible sessions and reports per-channel readiness problems', () => {
|
||||
const incompatible = resolveOfficialChannelsLaunchPlan({
|
||||
args: [],
|
||||
config: { selected: ['discord'], unattended: false },
|
||||
target: 'claude',
|
||||
profileType: 'settings',
|
||||
bunAvailable: true,
|
||||
channelReadiness: {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
imessage: true,
|
||||
},
|
||||
});
|
||||
const missingBun = resolveOfficialChannelsLaunchPlan({
|
||||
args: [],
|
||||
config: { selected: ['discord'], unattended: false },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: false,
|
||||
channelReadiness: {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
imessage: true,
|
||||
},
|
||||
});
|
||||
const missingToken = resolveOfficialChannelsLaunchPlan({
|
||||
args: [],
|
||||
config: { selected: ['telegram', 'discord'], unattended: false },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: true,
|
||||
channelReadiness: {
|
||||
telegram: false,
|
||||
discord: true,
|
||||
imessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(incompatible.applied).toBe(false);
|
||||
expect(incompatible.skippedMessages.join(' ')).toContain('native Claude default/account sessions');
|
||||
expect(missingBun.applied).toBe(false);
|
||||
expect(missingBun.skippedMessages.join(' ')).toContain('Bun is not installed');
|
||||
expect(missingToken.applied).toBe(true);
|
||||
expect(missingToken.appliedChannels).toEqual(['discord']);
|
||||
expect(missingToken.skippedMessages.join(' ')).toContain('TELEGRAM_BOT_TOKEN is not configured');
|
||||
});
|
||||
|
||||
it('leaves explicit channel arguments untouched', () => {
|
||||
const plan = resolveOfficialChannelsLaunchPlan({
|
||||
args: ['--channels', 'plugin:custom'],
|
||||
config: { selected: ['discord'], unattended: true },
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
bunAvailable: true,
|
||||
channelReadiness: {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
imessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(plan.applied).toBe(false);
|
||||
expect(plan.appliedChannels).toEqual([]);
|
||||
expect(plan.skippedMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('falls back to process.env.CLAUDE_CONFIG_DIR for sync when no explicit dir is passed', () => {
|
||||
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
process.env.CLAUDE_CONFIG_DIR = '/tmp/external-claude-config';
|
||||
|
||||
try {
|
||||
expect(resolveOfficialChannelsSyncConfigDir()).toBe('/tmp/external-claude-config');
|
||||
expect(resolveOfficialChannelsSyncConfigDir('/tmp/explicit')).toBe('/tmp/explicit');
|
||||
} finally {
|
||||
if (originalConfigDir !== undefined) {
|
||||
process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
|
||||
} else {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
clearConfiguredOfficialChannelToken,
|
||||
clearConfiguredOfficialChannelTokensEverywhere,
|
||||
getOfficialChannelEnvPath,
|
||||
hasConfiguredOfficialChannelToken,
|
||||
readConfiguredOfficialChannelToken,
|
||||
readOfficialChannelTokenFromEnvContent,
|
||||
setConfiguredOfficialChannelToken,
|
||||
syncOfficialChannelEnvToConfigDir,
|
||||
} from '../../../src/channels/official-channels-store';
|
||||
|
||||
describe('official channels token store', () => {
|
||||
let tempHome = '';
|
||||
let originalHome: string | undefined;
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-discord-channels-'));
|
||||
originalHome = process.env.HOME;
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.HOME = tempHome;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHome !== undefined) process.env.HOME = originalHome;
|
||||
else delete process.env.HOME;
|
||||
|
||||
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||
else delete process.env.CCS_HOME;
|
||||
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes and reads DISCORD_BOT_TOKEN from the canonical Discord env file', () => {
|
||||
const envPath = setConfiguredOfficialChannelToken('discord', 'discord-secret');
|
||||
|
||||
expect(envPath).toBe(path.join(tempHome, '.claude', 'channels', 'discord', '.env'));
|
||||
expect(hasConfiguredOfficialChannelToken('discord')).toBe(true);
|
||||
expect(readConfiguredOfficialChannelToken('discord')).toBe('discord-secret');
|
||||
expect(readOfficialChannelTokenFromEnvContent('discord', fs.readFileSync(envPath, 'utf8'))).toBe(
|
||||
'discord-secret'
|
||||
);
|
||||
});
|
||||
|
||||
it('writes and reads TELEGRAM_BOT_TOKEN from the canonical Telegram env file', () => {
|
||||
const envPath = setConfiguredOfficialChannelToken('telegram', 'telegram-secret');
|
||||
|
||||
expect(envPath).toBe(path.join(tempHome, '.claude', 'channels', 'telegram', '.env'));
|
||||
expect(hasConfiguredOfficialChannelToken('telegram')).toBe(true);
|
||||
expect(readConfiguredOfficialChannelToken('telegram')).toBe('telegram-secret');
|
||||
});
|
||||
|
||||
it('removes only the channel token entry and deletes the file when nothing remains', () => {
|
||||
const envPath = getOfficialChannelEnvPath('discord');
|
||||
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
||||
fs.writeFileSync(envPath, '# comment\nDISCORD_BOT_TOKEN=secret\nOTHER_KEY=value\n', 'utf8');
|
||||
|
||||
clearConfiguredOfficialChannelToken('discord');
|
||||
expect(fs.readFileSync(envPath, 'utf8')).toBe('# comment\nOTHER_KEY=value\n');
|
||||
|
||||
clearConfiguredOfficialChannelToken('discord');
|
||||
fs.writeFileSync(envPath, 'DISCORD_BOT_TOKEN=secret\n', 'utf8');
|
||||
clearConfiguredOfficialChannelToken('discord');
|
||||
expect(fs.existsSync(envPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('syncs the canonical env file into an alternate CLAUDE_CONFIG_DIR for account sessions', () => {
|
||||
setConfiguredOfficialChannelToken('discord', 'discord-secret');
|
||||
|
||||
const targetConfigDir = path.join(tempHome, '.ccs', 'instances', 'work');
|
||||
const targetPath = path.join(targetConfigDir, 'channels', 'discord', '.env');
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, '# keep\nOTHER_KEY=value\n', 'utf8');
|
||||
|
||||
const result = syncOfficialChannelEnvToConfigDir('discord', targetConfigDir);
|
||||
|
||||
expect(result.synced).toBe(true);
|
||||
expect(result.targetPath).toBe(targetPath);
|
||||
expect(fs.readFileSync(targetPath, 'utf8')).toBe(
|
||||
'# keep\nOTHER_KEY=value\n\nDISCORD_BOT_TOKEN=discord-secret\n'
|
||||
);
|
||||
expect(fs.statSync(targetPath).mode & 0o777).toBe(0o600);
|
||||
});
|
||||
|
||||
it('clears previously synced copies across managed Claude config dirs', () => {
|
||||
setConfiguredOfficialChannelToken('discord', 'discord-secret');
|
||||
setConfiguredOfficialChannelToken('telegram', 'telegram-secret');
|
||||
|
||||
const instanceConfigDir = path.join(tempHome, '.ccs', 'instances', 'work');
|
||||
const instanceEnvPath = path.join(instanceConfigDir, 'channels', 'discord', '.env');
|
||||
const telegramEnvPath = path.join(instanceConfigDir, 'channels', 'telegram', '.env');
|
||||
|
||||
syncOfficialChannelEnvToConfigDir('discord', instanceConfigDir);
|
||||
syncOfficialChannelEnvToConfigDir('telegram', instanceConfigDir);
|
||||
expect(fs.existsSync(instanceEnvPath)).toBe(true);
|
||||
expect(fs.existsSync(telegramEnvPath)).toBe(true);
|
||||
|
||||
const clearedPaths = clearConfiguredOfficialChannelTokensEverywhere();
|
||||
|
||||
expect(clearedPaths).toContain(getOfficialChannelEnvPath('discord'));
|
||||
expect(clearedPaths).toContain(getOfficialChannelEnvPath('telegram'));
|
||||
expect(clearedPaths).toContain(instanceEnvPath);
|
||||
expect(clearedPaths).toContain(telegramEnvPath);
|
||||
expect(fs.existsSync(getOfficialChannelEnvPath('discord'))).toBe(false);
|
||||
expect(fs.existsSync(getOfficialChannelEnvPath('telegram'))).toBe(false);
|
||||
expect(fs.existsSync(instanceEnvPath)).toBe(false);
|
||||
expect(fs.existsSync(telegramEnvPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2,26 +2,36 @@ import { describe, expect, it } from 'bun:test';
|
||||
import { parseChannelsCommandArgs } from '../../../src/commands/config-channels-command';
|
||||
|
||||
describe('config channels command parser', () => {
|
||||
it('parses toggles and token input', () => {
|
||||
it('parses selection, unattended mode, and token input', () => {
|
||||
const result = parseChannelsCommandArgs([
|
||||
'--enable',
|
||||
'--set',
|
||||
'telegram,discord',
|
||||
'--unattended',
|
||||
'--set-token',
|
||||
'discord-secret',
|
||||
'telegram=telegram-secret',
|
||||
]);
|
||||
|
||||
expect(result.enable).toBe(true);
|
||||
expect(result.setSelection).toBe('telegram,discord');
|
||||
expect(result.unattended).toBe(true);
|
||||
expect(result.setToken).toBe('discord-secret');
|
||||
expect(result.setToken).toEqual({
|
||||
channelId: 'telegram',
|
||||
token: 'telegram-secret',
|
||||
});
|
||||
});
|
||||
|
||||
it('supports inline token assignment and clear flags', () => {
|
||||
const result = parseChannelsCommandArgs(['--disable', '--no-unattended', '--set-token=abc']);
|
||||
const clearResult = parseChannelsCommandArgs(['--clear-token']);
|
||||
it('supports inline token assignment, legacy flags, and clear-token variants', () => {
|
||||
const result = parseChannelsCommandArgs([
|
||||
'--disable',
|
||||
'--no-unattended',
|
||||
'--set-token=abc',
|
||||
]);
|
||||
const clearAll = parseChannelsCommandArgs(['--clear-token']);
|
||||
const clearOne = parseChannelsCommandArgs(['--clear-token', 'discord']);
|
||||
|
||||
expect(result.disable).toBe(true);
|
||||
expect(result.noUnattended).toBe(true);
|
||||
expect(result.setToken).toBe('abc');
|
||||
expect(clearResult.clearToken).toBe(true);
|
||||
expect(result.setToken).toEqual({ channelId: 'discord', token: 'abc' });
|
||||
expect(clearAll.clearTokenAll).toBe(true);
|
||||
expect(clearOne.clearTokenChannel).toBe('discord');
|
||||
});
|
||||
});
|
||||
@@ -106,10 +106,10 @@ describe('unified-config-types', () => {
|
||||
expect(config.preferences.auto_update).toBe(true);
|
||||
});
|
||||
|
||||
it('should default Discord Channels to disabled and attended mode', () => {
|
||||
it('should default Official Channels to disabled and attended mode', () => {
|
||||
const config = createEmptyUnifiedConfig();
|
||||
expect(config.discord_channels?.enabled).toBe(false);
|
||||
expect(config.discord_channels?.unattended).toBe(false);
|
||||
expect(config.channels?.selected).toEqual([]);
|
||||
expect(config.channels?.unattended).toBe(false);
|
||||
});
|
||||
|
||||
it('should have CLIProxy providers list', () => {
|
||||
|
||||
Reference in new issue
Block a user