feat(channels): auto-enable official Claude channels

This commit is contained in:
Tam Nhu Tran committed 2026-03-25 16:31:55 -04:00
1 parent 0e2f47802b
commit a97fc42b10
18 files changed
+2653 -337

No files matched your search

@@ -1,15 +1,63 @@
import { describe, expect, it } from 'bun:test';
import {
MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION,
OFFICIAL_CHANNELS,
buildOfficialChannelsLaunchPreview,
buildOfficialChannelsReadinessSummary,
buildOfficialChannelSetupSummary,
buildOfficialChannelsArgs,
expandOfficialChannelSelection,
hasExplicitChannelsFlag,
hasExplicitPermissionOverride,
isDiscordChannelsSessionSupported,
resolveOfficialChannelsAuthSummary,
resolveOfficialChannelsLaunchPlan,
resolveOfficialChannelsSyncConfigDir,
resolveOfficialChannelsVersionSummary,
type OfficialChannelsAuthSummary,
type OfficialChannelsEnvironmentStatus,
type OfficialChannelsVersionSummary,
} from '../../../src/channels/official-channels-runtime';
function buildSupportedVersionSummary(
overrides: Partial<OfficialChannelsVersionSummary> = {}
): OfficialChannelsVersionSummary {
return {
current: '2.1.81',
minimum: MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION,
state: 'supported',
message: 'Claude Code v2.1.81',
...overrides,
};
}
function buildEligibleAuthSummary(
overrides: Partial<OfficialChannelsAuthSummary> = {}
): OfficialChannelsAuthSummary {
return {
checked: true,
loggedIn: true,
authMethod: 'claude.ai',
subscriptionType: 'pro',
state: 'eligible',
eligible: true,
message: 'Authenticated with claude.ai.',
...overrides,
};
}
function buildEnvironment(
overrides: Partial<OfficialChannelsEnvironmentStatus> = {}
): OfficialChannelsEnvironmentStatus {
return {
bunInstalled: true,
supportedProfiles: ['default', 'account'],
stateScopeMessage: 'state scope',
claudeVersion: buildSupportedVersionSummary(overrides.claudeVersion),
auth: buildEligibleAuthSummary(overrides.auth),
...overrides,
};
}
describe('official channels runtime planning', () => {
it('supports only native Claude default/account sessions', () => {
expect(isDiscordChannelsSessionSupported('claude', 'default')).toBe(true);
@@ -26,6 +74,7 @@ describe('official channels runtime planning', () => {
expect(hasExplicitChannelsFlag(['--permission-mode', 'acceptEdits'])).toBe(false);
expect(hasExplicitPermissionOverride(['--dangerously-skip-permissions'])).toBe(true);
expect(hasExplicitPermissionOverride(['--allow-dangerously-skip-permissions'])).toBe(true);
expect(hasExplicitPermissionOverride(['--permission-mode', 'acceptEdits'])).toBe(true);
expect(hasExplicitPermissionOverride(['--permission-mode=acceptEdits'])).toBe(true);
});
@@ -48,7 +97,7 @@ describe('official channels runtime planning', () => {
config: { selected: ['telegram', 'discord'], unattended: true },
target: 'claude',
profileType: 'default',
bunAvailable: true,
environment: buildEnvironment(),
channelReadiness: {
telegram: true,
discord: true,
@@ -63,11 +112,11 @@ describe('official channels runtime planning', () => {
it('keeps explicit permission choice and still returns ready channels', () => {
const plan = resolveOfficialChannelsLaunchPlan({
args: ['--permission-mode', 'acceptEdits'],
args: ['--allow-dangerously-skip-permissions'],
config: { selected: ['discord'], unattended: true },
target: 'claude',
profileType: 'account',
bunAvailable: true,
environment: buildEnvironment(),
channelReadiness: {
telegram: false,
discord: true,
@@ -86,7 +135,7 @@ describe('official channels runtime planning', () => {
config: { selected: ['discord'], unattended: false },
target: 'claude',
profileType: 'settings',
bunAvailable: true,
environment: buildEnvironment(),
channelReadiness: {
telegram: true,
discord: true,
@@ -98,7 +147,7 @@ describe('official channels runtime planning', () => {
config: { selected: ['discord'], unattended: false },
target: 'claude',
profileType: 'default',
bunAvailable: false,
environment: buildEnvironment({ bunInstalled: false }),
channelReadiness: {
telegram: true,
discord: true,
@@ -110,7 +159,7 @@ describe('official channels runtime planning', () => {
config: { selected: ['telegram', 'discord'], unattended: false },
target: 'claude',
profileType: 'default',
bunAvailable: true,
environment: buildEnvironment(),
channelReadiness: {
telegram: false,
discord: true,
@@ -127,13 +176,115 @@ describe('official channels runtime planning', () => {
expect(missingToken.skippedMessages.join(' ')).toContain('TELEGRAM_BOT_TOKEN is not configured');
});
it('skips launch when Claude Code version is unsupported or auth is ineligible', () => {
const unsupportedVersion = resolveOfficialChannelsLaunchPlan({
args: [],
config: { selected: ['discord'], unattended: true },
target: 'claude',
profileType: 'default',
environment: buildEnvironment({
claudeVersion: buildSupportedVersionSummary({
current: '2.1.79',
state: 'unsupported',
message:
'Official Channels require Claude Code v2.1.80+ (found v2.1.79).',
}),
}),
channelReadiness: {
telegram: true,
discord: true,
imessage: true,
},
});
const ineligibleAuth = resolveOfficialChannelsLaunchPlan({
args: [],
config: { selected: ['discord'], unattended: true },
target: 'claude',
profileType: 'default',
environment: buildEnvironment({
auth: buildEligibleAuthSummary({
authMethod: 'console-key',
state: 'ineligible',
eligible: false,
message: 'Official Channels require claude.ai login. Current auth method: console-key.',
}),
}),
channelReadiness: {
telegram: true,
discord: true,
imessage: true,
},
});
const unknownVersion = resolveOfficialChannelsLaunchPlan({
args: [],
config: { selected: ['discord'], unattended: true },
target: 'claude',
profileType: 'default',
environment: buildEnvironment({
claudeVersion: buildSupportedVersionSummary({
current: null,
state: 'unknown',
message: 'Unable to detect Claude Code version. Official Channels require v2.1.80+.',
}),
}),
channelReadiness: {
telegram: true,
discord: true,
imessage: true,
},
});
const unknownAuth = resolveOfficialChannelsLaunchPlan({
args: [],
config: { selected: ['discord'], unattended: true },
target: 'claude',
profileType: 'default',
environment: buildEnvironment({
auth: buildEligibleAuthSummary({
checked: false,
loggedIn: false,
authMethod: null,
subscriptionType: null,
state: 'unknown',
eligible: false,
message:
'Unable to verify Claude auth status. Official Channels require claude.ai login.',
}),
}),
channelReadiness: {
telegram: true,
discord: true,
imessage: true,
},
});
expect(unsupportedVersion.applied).toBe(false);
expect(unsupportedVersion.skippedMessages).toEqual([
'Official Channels require Claude Code v2.1.80+ (found v2.1.79).',
]);
expect(ineligibleAuth.applied).toBe(false);
expect(ineligibleAuth.skippedMessages).toEqual([
'Official Channels require claude.ai login. Current auth method: console-key.',
]);
expect(unknownVersion.applied).toBe(false);
expect(unknownVersion.skippedMessages).toEqual([
'Unable to detect Claude Code version. Official Channels require v2.1.80+.',
]);
expect(unknownAuth.applied).toBe(false);
expect(unknownAuth.skippedMessages).toEqual([
'Unable to verify Claude auth status. Official Channels require claude.ai login.',
]);
});
it('leaves explicit channel arguments untouched', () => {
const plan = resolveOfficialChannelsLaunchPlan({
args: ['--channels', 'plugin:custom'],
config: { selected: ['discord'], unattended: true },
target: 'claude',
profileType: 'default',
bunAvailable: true,
environment: buildEnvironment(),
channelReadiness: {
telegram: true,
discord: true,
@@ -146,19 +297,252 @@ describe('official channels runtime planning', () => {
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';
it('summarizes version compatibility states', () => {
const unknown = resolveOfficialChannelsVersionSummary(null);
const supported = resolveOfficialChannelsVersionSummary(MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION);
const unsupported = resolveOfficialChannelsVersionSummary('2.1.79');
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;
}
}
expect(unknown.state).toBe('unknown');
expect(unknown.message).toContain(`v${MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION}+`);
expect(supported.state).toBe('supported');
expect(supported.message).toBe(`Claude Code v${MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION}`);
expect(unsupported.state).toBe('unsupported');
expect(unsupported.message).toContain(`found v2.1.79`);
});
it('summarizes auth eligibility states and org requirements', () => {
const unknown = resolveOfficialChannelsAuthSummary(null);
const loggedOut = resolveOfficialChannelsAuthSummary({
loggedIn: false,
authMethod: null,
subscriptionType: null,
});
const wrongAuth = resolveOfficialChannelsAuthSummary({
loggedIn: true,
authMethod: 'api-key',
subscriptionType: 'pro',
});
const team = resolveOfficialChannelsAuthSummary({
loggedIn: true,
authMethod: 'claude.ai',
subscriptionType: 'team',
});
expect(unknown.state).toBe('unknown');
expect(loggedOut.state).toBe('ineligible');
expect(loggedOut.message).toContain('claude auth login');
expect(wrongAuth.state).toBe('ineligible');
expect(wrongAuth.message).toContain('Current auth method: api-key');
expect(team.state).toBe('eligible');
expect(team.orgRequirementMessage).toContain('enabled by an admin');
});
it('builds source-aware setup summaries for token and iMessage channels', () => {
expect(
buildOfficialChannelSetupSummary({
id: 'discord',
displayName: 'Discord',
selected: true,
requiresToken: true,
tokenAvailable: true,
tokenSource: 'process_env',
savedInClaudeState: false,
processEnvAvailable: true,
})
).toMatchObject({
state: 'ready',
label: 'Ready from current CCS process env',
});
expect(
buildOfficialChannelSetupSummary({
id: 'telegram',
displayName: 'Telegram',
selected: true,
requiresToken: true,
tokenAvailable: false,
tokenSource: 'missing',
savedInClaudeState: false,
processEnvAvailable: false,
})
).toMatchObject({
state: 'needs_token',
label: 'Needs token',
});
expect(
buildOfficialChannelSetupSummary({
id: 'imessage',
displayName: 'iMessage',
selected: true,
requiresToken: false,
tokenAvailable: true,
})
).toMatchObject({
state: 'needs_claude_setup',
label: 'Claude-side setup remaining',
});
});
it('builds an overall readiness summary that stays explicit about blockers and partial readiness', () => {
const needsSetup = buildOfficialChannelsReadinessSummary({
config: { selected: ['discord'], unattended: false },
environment: buildEnvironment({ bunInstalled: false }),
channels: [
{
id: 'discord',
displayName: 'Discord',
selected: true,
requiresToken: true,
tokenAvailable: false,
tokenSource: 'missing',
savedInClaudeState: false,
processEnvAvailable: false,
},
],
});
const limited = buildOfficialChannelsReadinessSummary({
config: { selected: ['imessage'], unattended: false },
environment: buildEnvironment(),
channels: [
{
id: 'imessage',
displayName: 'iMessage',
selected: true,
requiresToken: false,
tokenAvailable: true,
savedInClaudeState: false,
processEnvAvailable: false,
},
],
});
const ready = buildOfficialChannelsReadinessSummary({
config: { selected: ['telegram', 'discord'], unattended: false },
environment: buildEnvironment(),
channels: [
{
id: 'telegram',
displayName: 'Telegram',
selected: true,
requiresToken: true,
tokenAvailable: true,
tokenSource: 'saved_env',
savedInClaudeState: true,
processEnvAvailable: false,
},
{
id: 'discord',
displayName: 'Discord',
selected: true,
requiresToken: true,
tokenAvailable: true,
tokenSource: 'process_env',
savedInClaudeState: false,
processEnvAvailable: true,
},
],
});
expect(needsSetup).toMatchObject({
state: 'needs_setup',
title: 'Needs setup before CCS can auto-add these channels',
});
expect(needsSetup.blockers.join(' ')).toContain('Install Bun');
expect(needsSetup.blockers.join(' ')).toContain('Missing bot token');
expect(limited).toMatchObject({
state: 'limited',
title: 'Selected, but some channels still need manual setup',
});
expect(limited.blockers.join(' ')).toContain('iMessage still needs Claude-side install');
expect(ready).toMatchObject({
state: 'ready',
title: 'Ready for the next native Claude run',
});
expect(ready.message).toContain('Discord currently depends on this same CCS process env');
});
it('builds a launch preview for the default `ccs` path', () => {
const preview = buildOfficialChannelsLaunchPreview({
config: { selected: ['telegram', 'discord'], unattended: true },
environment: buildEnvironment(),
channels: [
{
id: 'telegram',
displayName: 'Telegram',
selected: true,
requiresToken: true,
tokenAvailable: true,
tokenSource: 'saved_env',
savedInClaudeState: true,
processEnvAvailable: false,
},
{
id: 'discord',
displayName: 'Discord',
selected: true,
requiresToken: true,
tokenAvailable: true,
tokenSource: 'process_env',
savedInClaudeState: false,
processEnvAvailable: true,
},
],
});
expect(preview).toMatchObject({
state: 'ready',
title: 'CCS will auto-add Telegram, Discord',
command: 'ccs',
permissionBypassIncluded: true,
appendedArgs: [
'--channels',
OFFICIAL_CHANNELS.telegram.pluginSpec,
OFFICIAL_CHANNELS.discord.pluginSpec,
'--dangerously-skip-permissions',
],
});
});
it('keeps launch preview explicit when only part of the selection can be applied', () => {
const preview = buildOfficialChannelsLaunchPreview({
config: { selected: ['telegram', 'discord'], unattended: false },
environment: buildEnvironment(),
channels: [
{
id: 'telegram',
displayName: 'Telegram',
selected: true,
requiresToken: true,
tokenAvailable: false,
tokenSource: 'missing',
savedInClaudeState: false,
processEnvAvailable: false,
},
{
id: 'discord',
displayName: 'Discord',
selected: true,
requiresToken: true,
tokenAvailable: true,
tokenSource: 'saved_env',
savedInClaudeState: true,
processEnvAvailable: false,
},
],
});
expect(preview).toMatchObject({
state: 'partial',
title: 'CCS will auto-add Discord',
command: 'ccs',
appendedArgs: ['--channels', OFFICIAL_CHANNELS.discord.pluginSpec],
skippedMessages: ['Telegram auto-enable skipped because TELEGRAM_BOT_TOKEN is not configured.'],
});
});
});
@@ -5,12 +5,13 @@ import * as path from 'path';
import {
clearConfiguredOfficialChannelToken,
clearConfiguredOfficialChannelTokensEverywhere,
getOfficialChannelTokenStatus,
getOfficialChannelEnvPath,
hasConfiguredOfficialChannelToken,
readConfiguredOfficialChannelToken,
readOfficialChannelTokenFromProcessEnv,
readOfficialChannelTokenFromEnvContent,
setConfiguredOfficialChannelToken,
syncOfficialChannelEnvToConfigDir,
} from '../../../src/channels/official-channels-store';
describe('official channels token store', () => {
@@ -55,6 +56,71 @@ describe('official channels token store', () => {
expect(readConfiguredOfficialChannelToken('telegram')).toBe('telegram-secret');
});
it('uses the official state-dir override when one is configured', () => {
const originalDiscordStateDir = process.env.DISCORD_STATE_DIR;
process.env.DISCORD_STATE_DIR = path.join(tempHome, 'discord-state');
try {
const envPath = setConfiguredOfficialChannelToken('discord', 'discord-secret');
expect(envPath).toBe(path.join(tempHome, 'discord-state', '.env'));
expect(getOfficialChannelEnvPath('discord')).toBe(path.join(tempHome, 'discord-state', '.env'));
expect(readConfiguredOfficialChannelToken('discord')).toBe('discord-secret');
} finally {
if (originalDiscordStateDir !== undefined) {
process.env.DISCORD_STATE_DIR = originalDiscordStateDir;
} else {
delete process.env.DISCORD_STATE_DIR;
}
}
});
it('treats a current-process env token as available readiness without marking it as saved', () => {
const originalDiscordToken = process.env.DISCORD_BOT_TOKEN;
process.env.DISCORD_BOT_TOKEN = 'discord-from-env';
try {
expect(readOfficialChannelTokenFromProcessEnv('discord')).toBe('discord-from-env');
expect(hasConfiguredOfficialChannelToken('discord')).toBe(false);
expect(getOfficialChannelTokenStatus('discord')).toEqual({
available: true,
source: 'process_env',
envKey: 'DISCORD_BOT_TOKEN',
savedInClaudeState: false,
processEnvAvailable: true,
});
} finally {
if (originalDiscordToken !== undefined) {
process.env.DISCORD_BOT_TOKEN = originalDiscordToken;
} else {
delete process.env.DISCORD_BOT_TOKEN;
}
}
});
it('prefers current-process env tokens over saved Claude state for readiness source', () => {
const originalTelegramToken = process.env.TELEGRAM_BOT_TOKEN;
setConfiguredOfficialChannelToken('telegram', 'telegram-saved');
process.env.TELEGRAM_BOT_TOKEN = 'telegram-from-env';
try {
expect(getOfficialChannelTokenStatus('telegram')).toEqual({
available: true,
source: 'saved_env',
envKey: 'TELEGRAM_BOT_TOKEN',
tokenPath: path.join(tempHome, '.claude', 'channels', 'telegram', '.env'),
savedInClaudeState: true,
processEnvAvailable: true,
});
} finally {
if (originalTelegramToken !== undefined) {
process.env.TELEGRAM_BOT_TOKEN = originalTelegramToken;
} else {
delete process.env.TELEGRAM_BOT_TOKEN;
}
}
});
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 });
@@ -69,46 +135,51 @@ describe('official channels token store', () => {
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', () => {
const originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
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');
const processConfigDir = path.join(tempHome, '.claude-account-session');
const staleDiscordInstancePath = getOfficialChannelEnvPath('discord', instanceConfigDir);
const staleTelegramInstancePath = getOfficialChannelEnvPath('telegram', instanceConfigDir);
const staleDiscordProcessPath = getOfficialChannelEnvPath('discord', processConfigDir);
syncOfficialChannelEnvToConfigDir('discord', instanceConfigDir);
syncOfficialChannelEnvToConfigDir('telegram', instanceConfigDir);
expect(fs.existsSync(instanceEnvPath)).toBe(true);
expect(fs.existsSync(telegramEnvPath)).toBe(true);
process.env.CLAUDE_CONFIG_DIR = processConfigDir;
const clearedPaths = clearConfiguredOfficialChannelTokensEverywhere();
try {
fs.mkdirSync(path.dirname(staleDiscordInstancePath), { recursive: true });
fs.writeFileSync(staleDiscordInstancePath, 'DISCORD_BOT_TOKEN=discord-secret\n', 'utf8');
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);
fs.mkdirSync(path.dirname(staleTelegramInstancePath), { recursive: true });
fs.writeFileSync(staleTelegramInstancePath, 'TELEGRAM_BOT_TOKEN=telegram-secret\n', 'utf8');
fs.mkdirSync(path.dirname(staleDiscordProcessPath), { recursive: true });
fs.writeFileSync(staleDiscordProcessPath, 'DISCORD_BOT_TOKEN=discord-secret\n', 'utf8');
expect(fs.existsSync(staleDiscordInstancePath)).toBe(true);
expect(fs.existsSync(staleTelegramInstancePath)).toBe(true);
expect(fs.existsSync(staleDiscordProcessPath)).toBe(true);
const clearedPaths = clearConfiguredOfficialChannelTokensEverywhere();
expect(clearedPaths).toContain(getOfficialChannelEnvPath('discord'));
expect(clearedPaths).toContain(getOfficialChannelEnvPath('telegram'));
expect(clearedPaths).toContain(staleDiscordInstancePath);
expect(clearedPaths).toContain(staleTelegramInstancePath);
expect(clearedPaths).toContain(staleDiscordProcessPath);
expect(fs.existsSync(getOfficialChannelEnvPath('discord'))).toBe(false);
expect(fs.existsSync(getOfficialChannelEnvPath('telegram'))).toBe(false);
expect(fs.existsSync(staleDiscordInstancePath)).toBe(false);
expect(fs.existsSync(staleTelegramInstancePath)).toBe(false);
expect(fs.existsSync(staleDiscordProcessPath)).toBe(false);
} finally {
if (originalClaudeConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
} else {
delete process.env.CLAUDE_CONFIG_DIR;
}
}
});
});