mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
766 lines
28 KiB
JavaScript
766 lines
28 KiB
JavaScript
/**
|
|
* Persist Command Tests
|
|
*
|
|
* Tests for the `ccs persist` CLI command including:
|
|
* - Argument parsing
|
|
* - API key masking
|
|
* - Settings merge logic
|
|
* - Backup management
|
|
* - Error handling
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
describe('Persist Command', () => {
|
|
// =========================================================================
|
|
// Argument Parsing Tests
|
|
// =========================================================================
|
|
describe('parseArgs', () => {
|
|
/**
|
|
* Simulates the argument parsing logic from persist-command.ts
|
|
*/
|
|
function parseArgs(args) {
|
|
const validPermissionModes = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
|
|
const knownFlags = [
|
|
'--yes',
|
|
'-y',
|
|
'--list-backups',
|
|
'--restore',
|
|
'--permission-mode',
|
|
'--dangerously-skip-permissions',
|
|
'--auto-approve',
|
|
'--help',
|
|
'-h',
|
|
];
|
|
const result = {
|
|
profile: undefined,
|
|
yes: false,
|
|
listBackups: false,
|
|
restore: undefined,
|
|
permissionMode: undefined,
|
|
dangerouslySkipPermissions: false,
|
|
parseError: undefined,
|
|
};
|
|
const unknownFlags = [];
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--yes' || arg === '-y') {
|
|
result.yes = true;
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
// Will be handled in main function
|
|
} else if (arg === '--list-backups') {
|
|
result.listBackups = true;
|
|
} else if (arg === '--restore') {
|
|
// Check if next arg is a timestamp (not a flag)
|
|
const nextArg = args[i + 1];
|
|
if (nextArg && !nextArg.startsWith('-')) {
|
|
result.restore = nextArg;
|
|
i++; // Skip next arg
|
|
} else {
|
|
result.restore = true; // Use latest
|
|
}
|
|
} else if (arg === '--permission-mode') {
|
|
const nextArg = args[i + 1];
|
|
if (!nextArg || nextArg.startsWith('-')) {
|
|
result.parseError = 'Missing value for --permission-mode';
|
|
} else if (!validPermissionModes.includes(nextArg)) {
|
|
result.parseError = `Invalid --permission-mode "${nextArg}". Valid modes: ${validPermissionModes.join(', ')}`;
|
|
} else {
|
|
result.permissionMode = nextArg;
|
|
i++; // Skip next arg
|
|
}
|
|
} else if (arg.startsWith('--permission-mode=')) {
|
|
const mode = arg.split('=').slice(1).join('=');
|
|
if (!mode.trim()) {
|
|
result.parseError = 'Missing value for --permission-mode';
|
|
} else if (!validPermissionModes.includes(mode)) {
|
|
result.parseError = `Invalid --permission-mode "${mode}". Valid modes: ${validPermissionModes.join(', ')}`;
|
|
} else {
|
|
result.permissionMode = mode;
|
|
}
|
|
} else if (arg === '--dangerously-skip-permissions' || arg === '--auto-approve') {
|
|
result.dangerouslySkipPermissions = true;
|
|
} else if (!arg.startsWith('-') && !result.profile) {
|
|
result.profile = arg;
|
|
} else if (arg.startsWith('-')) {
|
|
const known = knownFlags.some((flag) => arg === flag || arg.startsWith(`${flag}=`));
|
|
if (!known) {
|
|
unknownFlags.push(arg);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!result.parseError && unknownFlags.length > 0) {
|
|
const unknownList = unknownFlags.map((flag) => `"${flag}"`).join(', ');
|
|
result.parseError = `Unknown option(s): ${unknownList}. Run 'ccs persist --help' for usage.`;
|
|
}
|
|
|
|
if (!result.parseError && result.listBackups && result.restore) {
|
|
result.parseError = '--list-backups cannot be used with --restore';
|
|
}
|
|
|
|
if (
|
|
!result.parseError &&
|
|
(result.listBackups || result.restore) &&
|
|
(result.permissionMode || result.dangerouslySkipPermissions)
|
|
) {
|
|
result.parseError =
|
|
'Permission flags are not valid with backup operations. Use them only with ccs persist <profile>.';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function resolvePermissionMode(parsedArgs) {
|
|
if (!parsedArgs.dangerouslySkipPermissions) {
|
|
return parsedArgs.permissionMode;
|
|
}
|
|
|
|
if (parsedArgs.permissionMode && parsedArgs.permissionMode !== 'bypassPermissions') {
|
|
throw new Error(
|
|
'--dangerously-skip-permissions conflicts with --permission-mode. Use bypassPermissions or remove one flag.'
|
|
);
|
|
}
|
|
|
|
return 'bypassPermissions';
|
|
}
|
|
|
|
it('parses profile name as first positional argument', () => {
|
|
const result = parseArgs(['glm']);
|
|
assert.strictEqual(result.profile, 'glm');
|
|
});
|
|
|
|
it('parses --yes flag', () => {
|
|
const result = parseArgs(['glm', '--yes']);
|
|
assert.strictEqual(result.yes, true);
|
|
assert.strictEqual(result.profile, 'glm');
|
|
});
|
|
|
|
it('parses -y short flag', () => {
|
|
const result = parseArgs(['gemini', '-y']);
|
|
assert.strictEqual(result.yes, true);
|
|
assert.strictEqual(result.profile, 'gemini');
|
|
});
|
|
|
|
it('parses flags before profile name', () => {
|
|
const result = parseArgs(['--yes', 'kimi']);
|
|
assert.strictEqual(result.yes, true);
|
|
assert.strictEqual(result.profile, 'kimi');
|
|
});
|
|
|
|
it('handles no arguments', () => {
|
|
const result = parseArgs([]);
|
|
assert.strictEqual(result.profile, undefined);
|
|
assert.strictEqual(result.yes, false);
|
|
});
|
|
|
|
it('rejects unknown flags with a clear parseError', () => {
|
|
const result = parseArgs(['glm', '--unknown', '--yes']);
|
|
assert.match(result.parseError, /Unknown option\(s\)/);
|
|
});
|
|
|
|
it('takes only first positional as profile', () => {
|
|
const result = parseArgs(['first', 'second', 'third']);
|
|
assert.strictEqual(result.profile, 'first');
|
|
});
|
|
|
|
it('parses --list-backups flag', () => {
|
|
const result = parseArgs(['--list-backups']);
|
|
assert.strictEqual(result.listBackups, true);
|
|
assert.strictEqual(result.profile, undefined);
|
|
});
|
|
|
|
it('parses --restore flag without timestamp (use latest)', () => {
|
|
const result = parseArgs(['--restore']);
|
|
assert.strictEqual(result.restore, true);
|
|
});
|
|
|
|
it('parses --restore flag with timestamp', () => {
|
|
const result = parseArgs(['--restore', '20260110_205324']);
|
|
assert.strictEqual(result.restore, '20260110_205324');
|
|
});
|
|
|
|
it('parses --restore with --yes flag', () => {
|
|
const result = parseArgs(['--restore', '--yes']);
|
|
assert.strictEqual(result.restore, true);
|
|
assert.strictEqual(result.yes, true);
|
|
});
|
|
|
|
it('parses --restore with timestamp and --yes flag', () => {
|
|
const result = parseArgs(['--restore', '20260110_205324', '--yes']);
|
|
assert.strictEqual(result.restore, '20260110_205324');
|
|
assert.strictEqual(result.yes, true);
|
|
});
|
|
|
|
it('parses --permission-mode with valid mode', () => {
|
|
const result = parseArgs(['glm', '--permission-mode', 'acceptEdits']);
|
|
assert.strictEqual(result.profile, 'glm');
|
|
assert.strictEqual(result.permissionMode, 'acceptEdits');
|
|
assert.strictEqual(result.parseError, undefined);
|
|
});
|
|
|
|
it('parses --permission-mode=value syntax', () => {
|
|
const result = parseArgs(['glm', '--permission-mode=bypassPermissions']);
|
|
assert.strictEqual(result.permissionMode, 'bypassPermissions');
|
|
assert.strictEqual(result.parseError, undefined);
|
|
});
|
|
|
|
it('sets parseError for invalid --permission-mode', () => {
|
|
const result = parseArgs(['glm', '--permission-mode', 'invalid']);
|
|
assert.match(result.parseError, /Invalid --permission-mode/);
|
|
});
|
|
|
|
it('sets parseError for missing --permission-mode value', () => {
|
|
const result = parseArgs(['glm', '--permission-mode']);
|
|
assert.strictEqual(result.parseError, 'Missing value for --permission-mode');
|
|
});
|
|
|
|
it('sets parseError for empty inline --permission-mode=', () => {
|
|
const result = parseArgs(['glm', '--permission-mode=']);
|
|
assert.strictEqual(result.parseError, 'Missing value for --permission-mode');
|
|
});
|
|
|
|
it('parses --dangerously-skip-permissions flag', () => {
|
|
const result = parseArgs(['glm', '--dangerously-skip-permissions']);
|
|
assert.strictEqual(result.dangerouslySkipPermissions, true);
|
|
});
|
|
|
|
it('parses --auto-approve as alias flag', () => {
|
|
const result = parseArgs(['glm', '--auto-approve']);
|
|
assert.strictEqual(result.dangerouslySkipPermissions, true);
|
|
});
|
|
|
|
it('handles both dangerous alias flags together', () => {
|
|
const result = parseArgs(['glm', '--dangerously-skip-permissions', '--auto-approve']);
|
|
assert.strictEqual(result.dangerouslySkipPermissions, true);
|
|
assert.strictEqual(resolvePermissionMode(result), 'bypassPermissions');
|
|
});
|
|
|
|
['acceptEdits', 'plan', 'default'].forEach((mode) => {
|
|
it(`throws on conflict between dangerous mode and --permission-mode ${mode}`, () => {
|
|
const parsed = parseArgs(['glm', '--permission-mode', mode, '--auto-approve']);
|
|
|
|
assert.throws(
|
|
() => resolvePermissionMode(parsed),
|
|
/--dangerously-skip-permissions conflicts with --permission-mode/
|
|
);
|
|
});
|
|
});
|
|
|
|
it('allows dangerous mode with --permission-mode bypassPermissions', () => {
|
|
const parsed = parseArgs([
|
|
'glm',
|
|
'--permission-mode',
|
|
'bypassPermissions',
|
|
'--dangerously-skip-permissions',
|
|
]);
|
|
assert.strictEqual(resolvePermissionMode(parsed), 'bypassPermissions');
|
|
});
|
|
|
|
it('sets parseError when --list-backups and --restore are both provided', () => {
|
|
const parsed = parseArgs(['--list-backups', '--restore']);
|
|
assert.strictEqual(parsed.parseError, '--list-backups cannot be used with --restore');
|
|
});
|
|
|
|
it('sets parseError when permission flags are used with --restore', () => {
|
|
const parsed = parseArgs(['--restore', '--auto-approve']);
|
|
assert.match(parsed.parseError, /Permission flags are not valid with backup operations/);
|
|
});
|
|
|
|
it('sets parseError when permission flags are used with --list-backups', () => {
|
|
const parsed = parseArgs(['--list-backups', '--permission-mode', 'plan']);
|
|
assert.match(parsed.parseError, /Permission flags are not valid with backup operations/);
|
|
});
|
|
|
|
it('keeps test permission mode list exactly aligned with source constant', () => {
|
|
const sourcePath = path.join(__dirname, '../../../src/commands/persist-command.ts');
|
|
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
const match = sourceContent.match(/const VALID_PERMISSION_MODES = \[(.*?)\] as const/s);
|
|
assert(match, 'Expected VALID_PERMISSION_MODES constant to exist');
|
|
const sourceModes = [...match[1].matchAll(/'([^']+)'/g)].map((entry) => entry[1]).sort();
|
|
const testModes = ['default', 'plan', 'acceptEdits', 'bypassPermissions'].sort();
|
|
assert.deepStrictEqual(sourceModes, testModes);
|
|
});
|
|
|
|
it('accepts common option ordering permutations', () => {
|
|
const cases = [
|
|
['glm', '--yes', '--permission-mode', 'plan'],
|
|
['--yes', 'glm', '--permission-mode=plan'],
|
|
['--permission-mode', 'plan', 'glm', '--yes'],
|
|
['--yes', '--permission-mode', 'plan', 'glm'],
|
|
];
|
|
|
|
for (const input of cases) {
|
|
const parsed = parseArgs(input);
|
|
assert.strictEqual(parsed.parseError, undefined, `Expected no parseError for: ${input.join(' ')}`);
|
|
assert.strictEqual(parsed.profile, 'glm');
|
|
assert.strictEqual(parsed.yes, true);
|
|
assert.strictEqual(parsed.permissionMode, 'plan');
|
|
}
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// API Key Masking Tests
|
|
// =========================================================================
|
|
describe('maskApiKey', () => {
|
|
/**
|
|
* Simulates the maskApiKey function from persist-command.ts
|
|
*/
|
|
function maskApiKey(key) {
|
|
if (key.length <= 12) {
|
|
return '****';
|
|
}
|
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
}
|
|
|
|
it('masks keys showing first 4 and last 4 characters', () => {
|
|
const result = maskApiKey('sk-1234567890abcdef');
|
|
assert.strictEqual(result, 'sk-1...cdef');
|
|
});
|
|
|
|
it('returns **** for keys <= 12 characters', () => {
|
|
assert.strictEqual(maskApiKey('123456789012'), '****');
|
|
assert.strictEqual(maskApiKey('12345678901'), '****');
|
|
assert.strictEqual(maskApiKey('short'), '****');
|
|
assert.strictEqual(maskApiKey(''), '****');
|
|
});
|
|
|
|
it('handles exactly 13 character keys', () => {
|
|
const result = maskApiKey('1234567890abc');
|
|
assert.strictEqual(result, '1234...0abc');
|
|
});
|
|
|
|
it('handles long API keys', () => {
|
|
const longKey = 'api_key_1234567890abcdefghijklmnopqrstuvwxyz';
|
|
const result = maskApiKey(longKey);
|
|
assert.strictEqual(result, 'api_...wxyz');
|
|
});
|
|
|
|
it('preserves special characters', () => {
|
|
const key = '!@#$%^&*()_+-=[]{}';
|
|
const result = maskApiKey(key);
|
|
assert.strictEqual(result, '!@#$...[]{}');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Settings Merge Tests
|
|
// =========================================================================
|
|
describe('Settings Merge Logic', () => {
|
|
/**
|
|
* Simulates the merge logic from persist-command.ts
|
|
*/
|
|
function mergeSettings(existing, newEnv, permissionMode) {
|
|
const existingEnv = existing.env || {};
|
|
const merged = {
|
|
...existing,
|
|
env: {
|
|
...existingEnv,
|
|
...newEnv,
|
|
},
|
|
};
|
|
|
|
if (permissionMode) {
|
|
const existingPermissions =
|
|
existing.permissions && typeof existing.permissions === 'object'
|
|
? existing.permissions
|
|
: {};
|
|
merged.permissions = {
|
|
...existingPermissions,
|
|
defaultMode: permissionMode,
|
|
};
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
it('merges env vars into empty settings', () => {
|
|
const existing = {};
|
|
const newEnv = { ANTHROPIC_BASE_URL: 'http://example.com' };
|
|
const result = mergeSettings(existing, newEnv);
|
|
assert.deepStrictEqual(result.env, newEnv);
|
|
});
|
|
|
|
it('preserves existing hooks', () => {
|
|
const existing = {
|
|
hooks: { PreToolUse: [{ matcher: 'WebSearch' }] },
|
|
};
|
|
const newEnv = { ANTHROPIC_MODEL: 'test' };
|
|
const result = mergeSettings(existing, newEnv);
|
|
assert.deepStrictEqual(result.hooks, existing.hooks);
|
|
});
|
|
|
|
it('preserves existing presets', () => {
|
|
const existing = {
|
|
presets: [{ name: 'test', default: 'model' }],
|
|
};
|
|
const newEnv = { ANTHROPIC_MODEL: 'test' };
|
|
const result = mergeSettings(existing, newEnv);
|
|
assert.deepStrictEqual(result.presets, existing.presets);
|
|
});
|
|
|
|
it('preserves other settings like model and alwaysThinkingEnabled', () => {
|
|
const existing = {
|
|
model: 'opus',
|
|
alwaysThinkingEnabled: true,
|
|
};
|
|
const newEnv = { ANTHROPIC_MODEL: 'test' };
|
|
const result = mergeSettings(existing, newEnv);
|
|
assert.strictEqual(result.model, 'opus');
|
|
assert.strictEqual(result.alwaysThinkingEnabled, true);
|
|
});
|
|
|
|
it('merges new env vars with existing env vars', () => {
|
|
const existing = {
|
|
env: { EXISTING_VAR: 'value' },
|
|
};
|
|
const newEnv = { NEW_VAR: 'new_value' };
|
|
const result = mergeSettings(existing, newEnv);
|
|
assert.strictEqual(result.env.EXISTING_VAR, 'value');
|
|
assert.strictEqual(result.env.NEW_VAR, 'new_value');
|
|
});
|
|
|
|
it('overwrites existing env vars with new values', () => {
|
|
const existing = {
|
|
env: { ANTHROPIC_MODEL: 'old_model' },
|
|
};
|
|
const newEnv = { ANTHROPIC_MODEL: 'new_model' };
|
|
const result = mergeSettings(existing, newEnv);
|
|
assert.strictEqual(result.env.ANTHROPIC_MODEL, 'new_model');
|
|
});
|
|
|
|
it('handles complex settings with multiple fields', () => {
|
|
const existing = {
|
|
hooks: { PreToolUse: [] },
|
|
presets: [],
|
|
model: 'sonnet',
|
|
env: { OLD_VAR: 'old' },
|
|
customField: 'custom',
|
|
};
|
|
const newEnv = {
|
|
ANTHROPIC_BASE_URL: 'http://test.com',
|
|
ANTHROPIC_MODEL: 'test',
|
|
};
|
|
const result = mergeSettings(existing, newEnv);
|
|
|
|
assert.deepStrictEqual(result.hooks, existing.hooks);
|
|
assert.deepStrictEqual(result.presets, existing.presets);
|
|
assert.strictEqual(result.model, 'sonnet');
|
|
assert.strictEqual(result.customField, 'custom');
|
|
assert.strictEqual(result.env.OLD_VAR, 'old');
|
|
assert.strictEqual(result.env.ANTHROPIC_BASE_URL, 'http://test.com');
|
|
assert.strictEqual(result.env.ANTHROPIC_MODEL, 'test');
|
|
});
|
|
|
|
it('sets permissions.defaultMode when permission mode is provided', () => {
|
|
const existing = {
|
|
hooks: { PreToolUse: [] },
|
|
};
|
|
const result = mergeSettings(existing, { ANTHROPIC_MODEL: 'test' }, 'acceptEdits');
|
|
|
|
assert.strictEqual(result.permissions.defaultMode, 'acceptEdits');
|
|
assert.deepStrictEqual(result.hooks, existing.hooks);
|
|
});
|
|
|
|
it('preserves existing permissions allow/deny when setting defaultMode', () => {
|
|
const existing = {
|
|
permissions: {
|
|
allow: ['Bash(ls:*)'],
|
|
deny: ['Bash(rm:*)'],
|
|
},
|
|
};
|
|
const result = mergeSettings(existing, { ANTHROPIC_MODEL: 'test' }, 'bypassPermissions');
|
|
|
|
assert.deepStrictEqual(result.permissions.allow, ['Bash(ls:*)']);
|
|
assert.deepStrictEqual(result.permissions.deny, ['Bash(rm:*)']);
|
|
assert.strictEqual(result.permissions.defaultMode, 'bypassPermissions');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Backup Timestamp Tests
|
|
// =========================================================================
|
|
describe('Backup Timestamp Format', () => {
|
|
/**
|
|
* Simulates the timestamp generation from persist-command.ts
|
|
*/
|
|
function generateTimestamp(date) {
|
|
return (
|
|
date.getFullYear().toString() +
|
|
(date.getMonth() + 1).toString().padStart(2, '0') +
|
|
date.getDate().toString().padStart(2, '0') +
|
|
'_' +
|
|
date.getHours().toString().padStart(2, '0') +
|
|
date.getMinutes().toString().padStart(2, '0') +
|
|
date.getSeconds().toString().padStart(2, '0')
|
|
);
|
|
}
|
|
|
|
it('generates correct format YYYYMMDD_HHMMSS', () => {
|
|
const date = new Date(2025, 0, 15, 10, 30, 45); // Jan 15, 2025 10:30:45
|
|
const result = generateTimestamp(date);
|
|
assert.strictEqual(result, '20250115_103045');
|
|
});
|
|
|
|
it('pads single digit months', () => {
|
|
const date = new Date(2025, 0, 1, 0, 0, 0); // Jan 1
|
|
const result = generateTimestamp(date);
|
|
assert.match(result, /^202501/);
|
|
});
|
|
|
|
it('pads single digit days', () => {
|
|
const date = new Date(2025, 11, 5, 0, 0, 0); // Dec 5
|
|
const result = generateTimestamp(date);
|
|
assert.match(result, /^20251205/);
|
|
});
|
|
|
|
it('pads single digit hours, minutes, seconds', () => {
|
|
const date = new Date(2025, 5, 15, 1, 2, 3);
|
|
const result = generateTimestamp(date);
|
|
assert.strictEqual(result, '20250615_010203');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Profile Type Detection Tests
|
|
// =========================================================================
|
|
describe('Profile Type Messages', () => {
|
|
const profileTypes = {
|
|
settings: 'API',
|
|
cliproxy: 'CLIProxy',
|
|
copilot: 'Copilot',
|
|
account: 'Account (not supported)',
|
|
default: 'Default (not supported)',
|
|
};
|
|
|
|
it('maps settings type to API', () => {
|
|
assert.strictEqual(profileTypes.settings, 'API');
|
|
});
|
|
|
|
it('maps cliproxy type to CLIProxy', () => {
|
|
assert.strictEqual(profileTypes.cliproxy, 'CLIProxy');
|
|
});
|
|
|
|
it('maps copilot type to Copilot', () => {
|
|
assert.strictEqual(profileTypes.copilot, 'Copilot');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Sensitive Key Detection Tests
|
|
// =========================================================================
|
|
describe('Sensitive Key Detection', () => {
|
|
/**
|
|
* Simulates the logic to detect sensitive keys for masking
|
|
*/
|
|
function isSensitiveKey(key) {
|
|
const sensitiveParts = new Set([
|
|
'TOKEN',
|
|
'KEY',
|
|
'SECRET',
|
|
'PASSWORD',
|
|
'PASS',
|
|
'AUTH',
|
|
'CREDENTIAL',
|
|
'PRIVATE',
|
|
'ACCESS',
|
|
'REFRESH',
|
|
'APIKEY',
|
|
]);
|
|
const withCamelCaseBoundaries = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
|
|
const parts = withCamelCaseBoundaries.toUpperCase().split(/[^A-Z0-9]+/).filter(Boolean);
|
|
if (parts.some((part) => sensitiveParts.has(part))) {
|
|
return true;
|
|
}
|
|
const compact = parts.join('');
|
|
return (
|
|
compact.includes('TOKEN') ||
|
|
compact.includes('APIKEY') ||
|
|
compact.includes('ACCESSKEY') ||
|
|
compact.includes('AUTHKEY') ||
|
|
compact.includes('SECRET') ||
|
|
compact.includes('PASSWORD') ||
|
|
compact.includes('CREDENTIAL')
|
|
);
|
|
}
|
|
|
|
it('detects TOKEN in key name', () => {
|
|
assert.strictEqual(isSensitiveKey('ANTHROPIC_AUTH_TOKEN'), true);
|
|
assert.strictEqual(isSensitiveKey('ACCESS_TOKEN'), true);
|
|
});
|
|
|
|
it('detects KEY in key name', () => {
|
|
assert.strictEqual(isSensitiveKey('API_KEY'), true);
|
|
assert.strictEqual(isSensitiveKey('ANTHROPIC_API_KEY'), true);
|
|
});
|
|
|
|
it('detects SECRET in key name', () => {
|
|
assert.strictEqual(isSensitiveKey('CLIENT_SECRET'), true);
|
|
assert.strictEqual(isSensitiveKey('SECRET_KEY'), true);
|
|
});
|
|
|
|
it('does not flag non-sensitive keys', () => {
|
|
assert.strictEqual(isSensitiveKey('ANTHROPIC_BASE_URL'), false);
|
|
assert.strictEqual(isSensitiveKey('ANTHROPIC_MODEL'), false);
|
|
assert.strictEqual(isSensitiveKey('DISABLE_TELEMETRY'), false);
|
|
});
|
|
|
|
it('detects lowercase and mixed-case sensitive keys', () => {
|
|
assert.strictEqual(isSensitiveKey('anthropic_auth_token'), true);
|
|
assert.strictEqual(isSensitiveKey('Api_Key'), true);
|
|
assert.strictEqual(isSensitiveKey('clientSecret'), true);
|
|
});
|
|
|
|
it('detects additional secret families', () => {
|
|
assert.strictEqual(isSensitiveKey('DB_PASSWORD'), true);
|
|
assert.strictEqual(isSensitiveKey('refresh_token_value'), true);
|
|
assert.strictEqual(isSensitiveKey('private_credential_blob'), true);
|
|
});
|
|
|
|
it('detects camelCase sensitive key variants', () => {
|
|
assert.strictEqual(isSensitiveKey('accessKeyId'), true);
|
|
assert.strictEqual(isSensitiveKey('authTokenValue'), true);
|
|
assert.strictEqual(isSensitiveKey('apiKeyValue'), true);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Help Text Coverage Tests
|
|
// =========================================================================
|
|
describe('Help Text Coverage', () => {
|
|
const expectedOptions = ['--yes', '-y', '--help', '-h'];
|
|
const expectedProfileTypes = ['API profiles', 'CLIProxy', 'Copilot', 'Account-based'];
|
|
|
|
it('documents all CLI options', () => {
|
|
expectedOptions.forEach((option) => {
|
|
assert(typeof option === 'string', `Option ${option} should be a string`);
|
|
assert(option.startsWith('-'), `Option ${option} should start with -`);
|
|
});
|
|
});
|
|
|
|
it('documents all profile types', () => {
|
|
expectedProfileTypes.forEach((type) => {
|
|
assert(typeof type === 'string', `Profile type ${type} should be documented`);
|
|
});
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Error Message Tests
|
|
// =========================================================================
|
|
describe('Error Messages', () => {
|
|
it('account profile error message mentions CLAUDE_CONFIG_DIR', () => {
|
|
const errorMessage =
|
|
'Account profiles use CLAUDE_CONFIG_DIR isolation, not env vars.\n' +
|
|
"Use 'ccs profileName' to run with this profile instead.";
|
|
assert(errorMessage.includes('CLAUDE_CONFIG_DIR'));
|
|
assert(errorMessage.includes('ccs profileName'));
|
|
});
|
|
|
|
it('no env vars error message includes profile name placeholder', () => {
|
|
const profileName = 'test';
|
|
const errorMessage = `Profile '${profileName}' has no env vars configured`;
|
|
assert(errorMessage.includes(profileName));
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Backup File Parsing Tests
|
|
// =========================================================================
|
|
describe('Backup File Parsing', () => {
|
|
const backupPattern = /^settings\.json\.backup\.(\d{8}_\d{6})$/;
|
|
|
|
/**
|
|
* Simulates parsing a backup filename into a BackupFile object
|
|
*/
|
|
function parseBackupFile(filename, dir) {
|
|
const match = filename.match(backupPattern);
|
|
if (!match) return null;
|
|
const timestamp = match[1];
|
|
// Parse YYYYMMDD_HHMMSS
|
|
const year = parseInt(timestamp.slice(0, 4));
|
|
const month = parseInt(timestamp.slice(4, 6)) - 1;
|
|
const day = parseInt(timestamp.slice(6, 8));
|
|
const hour = parseInt(timestamp.slice(9, 11));
|
|
const min = parseInt(timestamp.slice(11, 13));
|
|
const sec = parseInt(timestamp.slice(13, 15));
|
|
return {
|
|
path: dir + '/' + filename,
|
|
timestamp,
|
|
date: new Date(year, month, day, hour, min, sec),
|
|
};
|
|
}
|
|
|
|
it('matches valid backup filename pattern', () => {
|
|
assert(backupPattern.test('settings.json.backup.20260110_205324'));
|
|
assert(backupPattern.test('settings.json.backup.20250101_000000'));
|
|
});
|
|
|
|
it('rejects invalid backup filenames', () => {
|
|
assert(!backupPattern.test('settings.json'));
|
|
assert(!backupPattern.test('settings.json.backup'));
|
|
assert(!backupPattern.test('settings.json.backup.invalid'));
|
|
assert(!backupPattern.test('settings.json.backup.20260110'));
|
|
assert(!backupPattern.test('other.json.backup.20260110_205324'));
|
|
});
|
|
|
|
it('parses backup file correctly', () => {
|
|
const result = parseBackupFile('settings.json.backup.20260110_205324', '/home/user/.claude');
|
|
assert.strictEqual(result.timestamp, '20260110_205324');
|
|
assert.strictEqual(result.path, '/home/user/.claude/settings.json.backup.20260110_205324');
|
|
assert.strictEqual(result.date.getFullYear(), 2026);
|
|
assert.strictEqual(result.date.getMonth(), 0); // January
|
|
assert.strictEqual(result.date.getDate(), 10);
|
|
assert.strictEqual(result.date.getHours(), 20);
|
|
assert.strictEqual(result.date.getMinutes(), 53);
|
|
assert.strictEqual(result.date.getSeconds(), 24);
|
|
});
|
|
|
|
it('returns null for non-matching files', () => {
|
|
const result = parseBackupFile('settings.json', '/home/user/.claude');
|
|
assert.strictEqual(result, null);
|
|
});
|
|
|
|
it('sorts backup files by date descending (newest first)', () => {
|
|
const files = [
|
|
{ timestamp: '20260109_100000', date: new Date(2026, 0, 9, 10, 0, 0) },
|
|
{ timestamp: '20260110_205324', date: new Date(2026, 0, 10, 20, 53, 24) },
|
|
{ timestamp: '20260110_100000', date: new Date(2026, 0, 10, 10, 0, 0) },
|
|
];
|
|
const sorted = files.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
assert.strictEqual(sorted[0].timestamp, '20260110_205324');
|
|
assert.strictEqual(sorted[1].timestamp, '20260110_100000');
|
|
assert.strictEqual(sorted[2].timestamp, '20260109_100000');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Backup Restore Logic Tests
|
|
// =========================================================================
|
|
describe('Backup Restore Logic', () => {
|
|
it('selects first backup when restore=true (latest)', () => {
|
|
const backups = [{ timestamp: '20260110_205324' }, { timestamp: '20260110_100000' }];
|
|
const restore = true;
|
|
const selected = restore === true ? backups[0] : backups.find((b) => b.timestamp === restore);
|
|
assert.strictEqual(selected.timestamp, '20260110_205324');
|
|
});
|
|
|
|
it('selects specific backup when restore is a timestamp', () => {
|
|
const backups = [{ timestamp: '20260110_205324' }, { timestamp: '20260110_100000' }];
|
|
const restore = '20260110_100000';
|
|
const selected = restore === true ? backups[0] : backups.find((b) => b.timestamp === restore);
|
|
assert.strictEqual(selected.timestamp, '20260110_100000');
|
|
});
|
|
|
|
it('returns undefined when timestamp not found', () => {
|
|
const backups = [{ timestamp: '20260110_205324' }, { timestamp: '20260110_100000' }];
|
|
const restore = '20260101_000000';
|
|
const selected = restore === true ? backups[0] : backups.find((b) => b.timestamp === restore);
|
|
assert.strictEqual(selected, undefined);
|
|
});
|
|
});
|
|
});
|