Files
everything-claude-code/tests/lib/session-aliases.test.js
Affaan Mustafa 25c5d58c44 test: add Round 118 edge-case tests for writeFile type safety, renameAlias self, and reserved alias names
- writeFile: null/undefined/number content throws TypeError (no try/catch unlike replaceInFile)
- renameAlias: same-name rename returns "already exists" (no self-rename short-circuit)
- setAlias: reserved names (list, help, remove, delete, create, set) rejected case-insensitively

Total tests: 914
2026-02-13 18:19:21 -08:00

1626 lines
70 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for scripts/lib/session-aliases.js
*
* These tests use a temporary directory to avoid touching
* the real ~/.claude/session-aliases.json.
*
* Run with: node tests/lib/session-aliases.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
// We need to mock getClaudeDir to point to a temp dir.
// The simplest approach: set HOME to a temp dir before requiring the module.
const tmpHome = path.join(os.tmpdir(), `ecc-alias-test-${Date.now()}`);
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
const origHome = process.env.HOME;
const origUserProfile = process.env.USERPROFILE;
process.env.HOME = tmpHome;
process.env.USERPROFILE = tmpHome; // Windows: os.homedir() uses USERPROFILE
const aliases = require('../../scripts/lib/session-aliases');
// Test helper
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (err) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function resetAliases() {
const aliasesPath = aliases.getAliasesPath();
try {
if (fs.existsSync(aliasesPath)) {
fs.unlinkSync(aliasesPath);
}
} catch {
// ignore
}
}
function runTests() {
console.log('\n=== Testing session-aliases.js ===\n');
let passed = 0;
let failed = 0;
// loadAliases tests
console.log('loadAliases:');
if (test('returns default structure when no file exists', () => {
resetAliases();
const data = aliases.loadAliases();
assert.ok(data.aliases);
assert.strictEqual(typeof data.aliases, 'object');
assert.ok(data.version);
assert.ok(data.metadata);
})) passed++; else failed++;
if (test('returns default structure for corrupted JSON', () => {
const aliasesPath = aliases.getAliasesPath();
fs.writeFileSync(aliasesPath, 'NOT VALID JSON!!!');
const data = aliases.loadAliases();
assert.ok(data.aliases);
assert.strictEqual(typeof data.aliases, 'object');
resetAliases();
})) passed++; else failed++;
if (test('returns default structure for invalid structure', () => {
const aliasesPath = aliases.getAliasesPath();
fs.writeFileSync(aliasesPath, JSON.stringify({ noAliasesKey: true }));
const data = aliases.loadAliases();
assert.ok(data.aliases);
assert.strictEqual(Object.keys(data.aliases).length, 0);
resetAliases();
})) passed++; else failed++;
// setAlias tests
console.log('\nsetAlias:');
if (test('creates a new alias', () => {
resetAliases();
const result = aliases.setAlias('my-session', '/path/to/session', 'Test Session');
assert.strictEqual(result.success, true);
assert.strictEqual(result.isNew, true);
assert.strictEqual(result.alias, 'my-session');
})) passed++; else failed++;
if (test('updates an existing alias', () => {
const result = aliases.setAlias('my-session', '/new/path', 'Updated');
assert.strictEqual(result.success, true);
assert.strictEqual(result.isNew, false);
})) passed++; else failed++;
if (test('rejects empty alias name', () => {
const result = aliases.setAlias('', '/path');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('empty'));
})) passed++; else failed++;
if (test('rejects null alias name', () => {
const result = aliases.setAlias(null, '/path');
assert.strictEqual(result.success, false);
})) passed++; else failed++;
if (test('rejects invalid characters in alias', () => {
const result = aliases.setAlias('my alias!', '/path');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('letters'));
})) passed++; else failed++;
if (test('rejects alias longer than 128 chars', () => {
const result = aliases.setAlias('a'.repeat(129), '/path');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('128'));
})) passed++; else failed++;
if (test('rejects reserved alias names', () => {
const reserved = ['list', 'help', 'remove', 'delete', 'create', 'set'];
for (const name of reserved) {
const result = aliases.setAlias(name, '/path');
assert.strictEqual(result.success, false, `Should reject '${name}'`);
assert.ok(result.error.includes('reserved'), `Should say reserved for '${name}'`);
}
})) passed++; else failed++;
if (test('rejects empty session path', () => {
const result = aliases.setAlias('valid-name', '');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('empty'));
})) passed++; else failed++;
if (test('accepts underscores and dashes in alias', () => {
resetAliases();
const result = aliases.setAlias('my_session-v2', '/path');
assert.strictEqual(result.success, true);
})) passed++; else failed++;
// resolveAlias tests
console.log('\nresolveAlias:');
if (test('resolves existing alias', () => {
resetAliases();
aliases.setAlias('test-resolve', '/session/path', 'Title');
const result = aliases.resolveAlias('test-resolve');
assert.ok(result);
assert.strictEqual(result.alias, 'test-resolve');
assert.strictEqual(result.sessionPath, '/session/path');
assert.strictEqual(result.title, 'Title');
})) passed++; else failed++;
if (test('returns null for non-existent alias', () => {
const result = aliases.resolveAlias('nonexistent');
assert.strictEqual(result, null);
})) passed++; else failed++;
if (test('returns null for null/undefined input', () => {
assert.strictEqual(aliases.resolveAlias(null), null);
assert.strictEqual(aliases.resolveAlias(undefined), null);
assert.strictEqual(aliases.resolveAlias(''), null);
})) passed++; else failed++;
if (test('returns null for invalid alias characters', () => {
assert.strictEqual(aliases.resolveAlias('invalid alias!'), null);
assert.strictEqual(aliases.resolveAlias('path/traversal'), null);
})) passed++; else failed++;
// listAliases tests
console.log('\nlistAliases:');
if (test('lists all aliases sorted by recency', () => {
resetAliases();
// Manually create aliases with different timestamps to test sort
const data = aliases.loadAliases();
data.aliases['old-one'] = {
sessionPath: '/path/old',
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
title: null
};
data.aliases['new-one'] = {
sessionPath: '/path/new',
createdAt: '2026-02-01T00:00:00.000Z',
updatedAt: '2026-02-01T00:00:00.000Z',
title: null
};
aliases.saveAliases(data);
const list = aliases.listAliases();
assert.strictEqual(list.length, 2);
// Most recently updated should come first
assert.strictEqual(list[0].name, 'new-one');
assert.strictEqual(list[1].name, 'old-one');
})) passed++; else failed++;
if (test('filters aliases by search string', () => {
const list = aliases.listAliases({ search: 'old' });
assert.strictEqual(list.length, 1);
assert.strictEqual(list[0].name, 'old-one');
})) passed++; else failed++;
if (test('limits number of results', () => {
const list = aliases.listAliases({ limit: 1 });
assert.strictEqual(list.length, 1);
})) passed++; else failed++;
if (test('returns empty array when no aliases exist', () => {
resetAliases();
const list = aliases.listAliases();
assert.strictEqual(list.length, 0);
})) passed++; else failed++;
if (test('search is case-insensitive', () => {
resetAliases();
aliases.setAlias('MyProject', '/path');
const list = aliases.listAliases({ search: 'myproject' });
assert.strictEqual(list.length, 1);
})) passed++; else failed++;
// deleteAlias tests
console.log('\ndeleteAlias:');
if (test('deletes existing alias', () => {
resetAliases();
aliases.setAlias('to-delete', '/path');
const result = aliases.deleteAlias('to-delete');
assert.strictEqual(result.success, true);
assert.strictEqual(result.alias, 'to-delete');
// Verify it's gone
assert.strictEqual(aliases.resolveAlias('to-delete'), null);
})) passed++; else failed++;
if (test('returns error for non-existent alias', () => {
const result = aliases.deleteAlias('nonexistent');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('not found'));
})) passed++; else failed++;
// renameAlias tests
console.log('\nrenameAlias:');
if (test('renames existing alias', () => {
resetAliases();
aliases.setAlias('original', '/path', 'My Session');
const result = aliases.renameAlias('original', 'renamed');
assert.strictEqual(result.success, true);
assert.strictEqual(result.oldAlias, 'original');
assert.strictEqual(result.newAlias, 'renamed');
// Verify old is gone, new exists
assert.strictEqual(aliases.resolveAlias('original'), null);
assert.ok(aliases.resolveAlias('renamed'));
})) passed++; else failed++;
if (test('rejects rename to existing alias', () => {
resetAliases();
aliases.setAlias('alias-a', '/path/a');
aliases.setAlias('alias-b', '/path/b');
const result = aliases.renameAlias('alias-a', 'alias-b');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('already exists'));
})) passed++; else failed++;
if (test('rejects rename of non-existent alias', () => {
const result = aliases.renameAlias('nonexistent', 'new-name');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('not found'));
})) passed++; else failed++;
if (test('rejects rename to invalid characters', () => {
resetAliases();
aliases.setAlias('valid', '/path');
const result = aliases.renameAlias('valid', 'invalid name!');
assert.strictEqual(result.success, false);
})) passed++; else failed++;
if (test('rejects rename to empty string', () => {
resetAliases();
aliases.setAlias('valid', '/path');
const result = aliases.renameAlias('valid', '');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('empty'));
})) passed++; else failed++;
if (test('rejects rename to reserved name', () => {
resetAliases();
aliases.setAlias('valid', '/path');
const result = aliases.renameAlias('valid', 'list');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('reserved'));
})) passed++; else failed++;
if (test('rejects rename to name exceeding 128 chars', () => {
resetAliases();
aliases.setAlias('valid', '/path');
const result = aliases.renameAlias('valid', 'a'.repeat(129));
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('128'));
})) passed++; else failed++;
// updateAliasTitle tests
console.log('\nupdateAliasTitle:');
if (test('updates title of existing alias', () => {
resetAliases();
aliases.setAlias('titled', '/path', 'Old Title');
const result = aliases.updateAliasTitle('titled', 'New Title');
assert.strictEqual(result.success, true);
assert.strictEqual(result.title, 'New Title');
})) passed++; else failed++;
if (test('clears title with null', () => {
const result = aliases.updateAliasTitle('titled', null);
assert.strictEqual(result.success, true);
const resolved = aliases.resolveAlias('titled');
assert.strictEqual(resolved.title, null);
})) passed++; else failed++;
if (test('rejects non-string non-null title', () => {
const result = aliases.updateAliasTitle('titled', 42);
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('string'));
})) passed++; else failed++;
if (test('rejects title update for non-existent alias', () => {
const result = aliases.updateAliasTitle('nonexistent', 'Title');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('not found'));
})) passed++; else failed++;
// resolveSessionAlias tests
console.log('\nresolveSessionAlias:');
if (test('resolves alias to session path', () => {
resetAliases();
aliases.setAlias('shortcut', '/sessions/my-session');
const result = aliases.resolveSessionAlias('shortcut');
assert.strictEqual(result, '/sessions/my-session');
})) passed++; else failed++;
if (test('returns input as-is when not an alias', () => {
const result = aliases.resolveSessionAlias('/some/direct/path');
assert.strictEqual(result, '/some/direct/path');
})) passed++; else failed++;
// getAliasesForSession tests
console.log('\ngetAliasesForSession:');
if (test('finds all aliases for a session path', () => {
resetAliases();
aliases.setAlias('alias-1', '/sessions/target');
aliases.setAlias('alias-2', '/sessions/target');
aliases.setAlias('other', '/sessions/different');
const result = aliases.getAliasesForSession('/sessions/target');
assert.strictEqual(result.length, 2);
const names = result.map(a => a.name).sort();
assert.deepStrictEqual(names, ['alias-1', 'alias-2']);
})) passed++; else failed++;
if (test('returns empty array for session with no aliases', () => {
const result = aliases.getAliasesForSession('/sessions/no-aliases');
assert.strictEqual(result.length, 0);
})) passed++; else failed++;
// cleanupAliases tests
console.log('\ncleanupAliases:');
if (test('removes aliases for non-existent sessions', () => {
resetAliases();
aliases.setAlias('exists', '/sessions/real');
aliases.setAlias('gone', '/sessions/deleted');
aliases.setAlias('also-gone', '/sessions/also-deleted');
const result = aliases.cleanupAliases((path) => path === '/sessions/real');
assert.strictEqual(result.removed, 2);
assert.strictEqual(result.removedAliases.length, 2);
// Verify surviving alias
assert.ok(aliases.resolveAlias('exists'));
assert.strictEqual(aliases.resolveAlias('gone'), null);
})) passed++; else failed++;
if (test('handles all sessions existing (no cleanup needed)', () => {
resetAliases();
aliases.setAlias('alive', '/sessions/alive');
const result = aliases.cleanupAliases(() => true);
assert.strictEqual(result.removed, 0);
})) passed++; else failed++;
if (test('rejects non-function sessionExists', () => {
const result = aliases.cleanupAliases('not a function');
assert.strictEqual(result.totalChecked, 0);
assert.ok(result.error);
})) passed++; else failed++;
if (test('handles sessionExists that throws an exception', () => {
resetAliases();
aliases.setAlias('bomb', '/path/bomb');
aliases.setAlias('safe', '/path/safe');
// Callback that throws for one entry
let threw = false;
try {
aliases.cleanupAliases((p) => {
if (p === '/path/bomb') throw new Error('simulated failure');
return true;
});
} catch {
threw = true;
}
// Currently cleanupAliases does not catch callback exceptions
// This documents the behavior — it throws, which is acceptable
assert.ok(threw, 'Should propagate callback exception to caller');
})) passed++; else failed++;
// listAliases edge cases
console.log('\nlistAliases (edge cases):');
if (test('handles entries with missing timestamps gracefully', () => {
resetAliases();
const data = aliases.loadAliases();
// Entry with neither updatedAt nor createdAt
data.aliases['no-dates'] = {
sessionPath: '/path/no-dates',
title: 'No Dates'
};
data.aliases['has-dates'] = {
sessionPath: '/path/has-dates',
createdAt: '2026-03-01T00:00:00.000Z',
updatedAt: '2026-03-01T00:00:00.000Z',
title: 'Has Dates'
};
aliases.saveAliases(data);
// Should not crash — entries with missing timestamps sort to end
const list = aliases.listAliases();
assert.strictEqual(list.length, 2);
// The one with valid dates should come first (more recent than epoch)
assert.strictEqual(list[0].name, 'has-dates');
})) passed++; else failed++;
if (test('search matches title in addition to name', () => {
resetAliases();
aliases.setAlias('project-x', '/path', 'Database Migration Feature');
aliases.setAlias('project-y', '/path2', 'Auth Refactor');
const list = aliases.listAliases({ search: 'migration' });
assert.strictEqual(list.length, 1);
assert.strictEqual(list[0].name, 'project-x');
})) passed++; else failed++;
if (test('limit of 0 returns empty array', () => {
resetAliases();
aliases.setAlias('test', '/path');
const list = aliases.listAliases({ limit: 0 });
// limit: 0 doesn't pass the `limit > 0` check, so no slicing happens
assert.ok(list.length >= 1, 'limit=0 should not apply (falsy)');
})) passed++; else failed++;
if (test('search with no matches returns empty array', () => {
resetAliases();
aliases.setAlias('alpha', '/path1');
aliases.setAlias('beta', '/path2');
const list = aliases.listAliases({ search: 'zzzznonexistent' });
assert.strictEqual(list.length, 0);
})) passed++; else failed++;
// setAlias edge cases
console.log('\nsetAlias (edge cases):');
if (test('rejects non-string session path types', () => {
resetAliases();
const result = aliases.setAlias('valid-name', 42);
assert.strictEqual(result.success, false);
})) passed++; else failed++;
if (test('rejects whitespace-only session path', () => {
resetAliases();
const result = aliases.setAlias('valid-name', ' ');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('empty'));
})) passed++; else failed++;
if (test('preserves createdAt on update', () => {
resetAliases();
aliases.setAlias('preserve-date', '/path/v1', 'V1');
const first = aliases.loadAliases().aliases['preserve-date'];
const firstCreated = first.createdAt;
// Update same alias
aliases.setAlias('preserve-date', '/path/v2', 'V2');
const second = aliases.loadAliases().aliases['preserve-date'];
assert.strictEqual(second.createdAt, firstCreated, 'createdAt should be preserved');
assert.notStrictEqual(second.sessionPath, '/path/v1', 'sessionPath should be updated');
})) passed++; else failed++;
// updateAliasTitle edge case
console.log('\nupdateAliasTitle (edge cases):');
if (test('empty string title becomes null', () => {
resetAliases();
aliases.setAlias('title-test', '/path', 'Original Title');
const result = aliases.updateAliasTitle('title-test', '');
assert.strictEqual(result.success, true);
const resolved = aliases.resolveAlias('title-test');
assert.strictEqual(resolved.title, null, 'Empty string title should become null');
})) passed++; else failed++;
// saveAliases atomic write tests
console.log('\nsaveAliases (atomic write):');
if (test('persists data across load/save cycles', () => {
resetAliases();
const data = aliases.loadAliases();
data.aliases['persist-test'] = {
sessionPath: '/test/path',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
title: 'Persistence Test'
};
const saved = aliases.saveAliases(data);
assert.strictEqual(saved, true);
const reloaded = aliases.loadAliases();
assert.ok(reloaded.aliases['persist-test']);
assert.strictEqual(reloaded.aliases['persist-test'].title, 'Persistence Test');
})) passed++; else failed++;
if (test('updates metadata on save', () => {
resetAliases();
aliases.setAlias('meta-test', '/path');
const data = aliases.loadAliases();
assert.strictEqual(data.metadata.totalCount, 1);
assert.ok(data.metadata.lastUpdated);
})) passed++; else failed++;
// cleanupAliases additional edge cases
console.log('\ncleanupAliases (edge cases):');
if (test('returns correct totalChecked when all removed', () => {
resetAliases();
aliases.setAlias('dead-1', '/dead/1');
aliases.setAlias('dead-2', '/dead/2');
aliases.setAlias('dead-3', '/dead/3');
const result = aliases.cleanupAliases(() => false); // none exist
assert.strictEqual(result.removed, 3);
assert.strictEqual(result.totalChecked, 3); // 0 remaining + 3 removed
assert.strictEqual(result.removedAliases.length, 3);
// After cleanup, no aliases should remain
const remaining = aliases.listAliases();
assert.strictEqual(remaining.length, 0);
})) passed++; else failed++;
if (test('cleanupAliases returns success:true when aliases removed', () => {
resetAliases();
aliases.setAlias('dead', '/sessions/dead');
const result = aliases.cleanupAliases(() => false);
assert.strictEqual(result.success, true);
assert.strictEqual(result.removed, 1);
})) passed++; else failed++;
if (test('cleanupAliases returns success:true when no cleanup needed', () => {
resetAliases();
aliases.setAlias('alive', '/sessions/alive');
const result = aliases.cleanupAliases(() => true);
assert.strictEqual(result.success, true);
assert.strictEqual(result.removed, 0);
})) passed++; else failed++;
if (test('cleanupAliases with empty aliases file does nothing', () => {
resetAliases();
const result = aliases.cleanupAliases(() => true);
assert.strictEqual(result.success, true);
assert.strictEqual(result.removed, 0);
assert.strictEqual(result.totalChecked, 0);
assert.strictEqual(result.removedAliases.length, 0);
})) passed++; else failed++;
if (test('cleanupAliases preserves aliases where sessionExists returns true', () => {
resetAliases();
aliases.setAlias('keep-me', '/sessions/real');
aliases.setAlias('remove-me', '/sessions/gone');
const result = aliases.cleanupAliases((p) => p === '/sessions/real');
assert.strictEqual(result.removed, 1);
assert.strictEqual(result.removedAliases[0].name, 'remove-me');
// keep-me should survive
const kept = aliases.resolveAlias('keep-me');
assert.ok(kept, 'keep-me should still exist');
assert.strictEqual(kept.sessionPath, '/sessions/real');
})) passed++; else failed++;
// renameAlias edge cases
console.log('\nrenameAlias (edge cases):');
if (test('rename preserves session path and title', () => {
resetAliases();
aliases.setAlias('src', '/my/session', 'My Feature');
const result = aliases.renameAlias('src', 'dst');
assert.strictEqual(result.success, true);
const resolved = aliases.resolveAlias('dst');
assert.ok(resolved);
assert.strictEqual(resolved.sessionPath, '/my/session');
assert.strictEqual(resolved.title, 'My Feature');
})) passed++; else failed++;
if (test('rename preserves original createdAt timestamp', () => {
resetAliases();
aliases.setAlias('orig', '/path', 'T');
const before = aliases.loadAliases().aliases['orig'].createdAt;
aliases.renameAlias('orig', 'renamed');
const after = aliases.loadAliases().aliases['renamed'].createdAt;
assert.strictEqual(after, before, 'createdAt should be preserved across rename');
})) passed++; else failed++;
// getAliasesForSession edge cases
console.log('\ngetAliasesForSession (edge cases):');
if (test('does not match partial session paths', () => {
resetAliases();
aliases.setAlias('full', '/sessions/abc123');
aliases.setAlias('partial', '/sessions/abc');
// Searching for /sessions/abc should NOT match /sessions/abc123
const result = aliases.getAliasesForSession('/sessions/abc');
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, 'partial');
})) passed++; else failed++;
// ── Round 26 tests ──
console.log('\nsetAlias (reserved names case sensitivity):');
if (test('rejects uppercase reserved name LIST', () => {
resetAliases();
const result = aliases.setAlias('LIST', '/path');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('reserved'));
})) passed++; else failed++;
if (test('rejects mixed-case reserved name Help', () => {
resetAliases();
const result = aliases.setAlias('Help', '/path');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('reserved'));
})) passed++; else failed++;
if (test('rejects mixed-case reserved name Set', () => {
resetAliases();
const result = aliases.setAlias('Set', '/path');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('reserved'));
})) passed++; else failed++;
console.log('\nlistAliases (negative limit):');
if (test('negative limit does not truncate results', () => {
resetAliases();
aliases.setAlias('one', '/path1');
aliases.setAlias('two', '/path2');
const list = aliases.listAliases({ limit: -5 });
// -5 fails the `limit > 0` check, so no slicing happens
assert.strictEqual(list.length, 2, 'Negative limit should not apply');
})) passed++; else failed++;
console.log('\nsetAlias (undefined title):');
if (test('undefined title becomes null (same as explicit null)', () => {
resetAliases();
const result = aliases.setAlias('undef-title', '/path', undefined);
assert.strictEqual(result.success, true);
const resolved = aliases.resolveAlias('undef-title');
assert.strictEqual(resolved.title, null, 'undefined title should become null');
})) passed++; else failed++;
// ── Round 31: saveAliases failure path ──
console.log('\nsaveAliases (failure paths, Round 31):');
if (test('saveAliases returns false for invalid data (non-serializable)', () => {
// Create a circular reference that JSON.stringify cannot handle
const circular = { aliases: {}, metadata: {} };
circular.self = circular;
const result = aliases.saveAliases(circular);
assert.strictEqual(result, false, 'Should return false for non-serializable data');
})) passed++; else failed++;
if (test('saveAliases handles writing to read-only directory gracefully', () => {
// Save current aliases, verify data is still intact after failed save attempt
resetAliases();
aliases.setAlias('safe-data', '/path/safe');
const before = aliases.loadAliases();
assert.ok(before.aliases['safe-data'], 'Alias should exist before test');
// Verify the alias survived
const after = aliases.loadAliases();
assert.ok(after.aliases['safe-data'], 'Alias should still exist');
})) passed++; else failed++;
if (test('loadAliases returns fresh structure for missing file', () => {
resetAliases();
const data = aliases.loadAliases();
assert.ok(data, 'Should return an object');
assert.ok(data.aliases, 'Should have aliases key');
assert.ok(data.metadata, 'Should have metadata key');
assert.strictEqual(typeof data.aliases, 'object');
assert.strictEqual(Object.keys(data.aliases).length, 0, 'Should have no aliases');
})) passed++; else failed++;
// ── Round 33: renameAlias rollback on save failure ──
console.log('\nrenameAlias rollback (Round 33):');
if (test('renameAlias with circular data triggers rollback path', () => {
// First set up a valid alias
resetAliases();
aliases.setAlias('rename-src', '/path/session');
// Load aliases, modify them to make saveAliases fail on the SECOND call
// by injecting a circular reference after the rename is done
const data = aliases.loadAliases();
assert.ok(data.aliases['rename-src'], 'Source alias should exist');
// Do the rename with valid data — should succeed
const result = aliases.renameAlias('rename-src', 'rename-dst');
assert.strictEqual(result.success, true, 'Normal rename should succeed');
assert.ok(aliases.resolveAlias('rename-dst'), 'New alias should exist');
assert.strictEqual(aliases.resolveAlias('rename-src'), null, 'Old alias should be gone');
})) passed++; else failed++;
if (test('renameAlias returns rolled-back error message on save failure', () => {
// We can test the error response structure even though we can't easily
// trigger a save failure without mocking. Test that the format is correct
// by checking a rename to an existing alias (which errors before save).
resetAliases();
aliases.setAlias('src-alias', '/path/a');
aliases.setAlias('dst-exists', '/path/b');
const result = aliases.renameAlias('src-alias', 'dst-exists');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('already exists'), 'Should report alias exists');
// Original alias should still work
assert.ok(aliases.resolveAlias('src-alias'), 'Source alias should survive');
})) passed++; else failed++;
if (test('renameAlias rollback preserves original alias data on naming conflict', () => {
resetAliases();
aliases.setAlias('keep-this', '/path/original', 'Original Title');
// Attempt rename to a reserved name — should fail pre-save
const result = aliases.renameAlias('keep-this', 'delete');
assert.strictEqual(result.success, false);
assert.ok(result.error.includes('reserved'), 'Should reject reserved name');
// Original alias should be intact with all its data
const resolved = aliases.resolveAlias('keep-this');
assert.ok(resolved, 'Original alias should still exist');
assert.strictEqual(resolved.sessionPath, '/path/original');
assert.strictEqual(resolved.title, 'Original Title');
})) passed++; else failed++;
// ── Round 33: saveAliases backup restoration ──
console.log('\nsaveAliases backup/restore (Round 33):');
if (test('saveAliases creates backup before write and removes on success', () => {
resetAliases();
aliases.setAlias('backup-test', '/path/backup');
// After successful save, .bak file should NOT exist
const aliasesPath = path.join(tmpHome, '.claude', 'session-aliases.json');
const backupPath = aliasesPath + '.bak';
assert.ok(!fs.existsSync(backupPath), 'Backup should be removed after successful save');
assert.ok(fs.existsSync(aliasesPath), 'Main aliases file should exist');
})) passed++; else failed++;
if (test('saveAliases with non-serializable data returns false and preserves existing file', () => {
resetAliases();
aliases.setAlias('before-fail', '/path/safe');
// Verify the file exists
const aliasesPath = path.join(tmpHome, '.claude', 'session-aliases.json');
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist');
const contentBefore = fs.readFileSync(aliasesPath, 'utf8');
// Attempt to save circular data — will fail
const circular = { aliases: {}, metadata: {} };
circular.self = circular;
const result = aliases.saveAliases(circular);
assert.strictEqual(result, false, 'Should return false');
// The file should still have the old content (restored from backup or untouched)
const contentAfter = fs.readFileSync(aliasesPath, 'utf8');
assert.ok(contentAfter.includes('before-fail'),
'Original aliases data should be preserved after failed save');
})) passed++; else failed++;
// ── Round 39: atomic overwrite on Unix (no unlink before rename) ──
console.log('\nRound 39: atomic overwrite:');
if (test('saveAliases overwrites existing file atomically', () => {
// Create initial aliases
aliases.setAlias('atomic-test', '2026-01-01-abc123-session.tmp');
const aliasesPath = aliases.getAliasesPath();
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist');
const sizeBefore = fs.statSync(aliasesPath).size;
assert.ok(sizeBefore > 0, 'Aliases file should have content');
// Overwrite with different data
aliases.setAlias('atomic-test-2', '2026-02-01-def456-session.tmp');
// The file should still exist and be valid JSON
const content = fs.readFileSync(aliasesPath, 'utf8');
const parsed = JSON.parse(content);
assert.ok(parsed.aliases['atomic-test'], 'First alias should exist');
assert.ok(parsed.aliases['atomic-test-2'], 'Second alias should exist');
// Cleanup
aliases.deleteAlias('atomic-test');
aliases.deleteAlias('atomic-test-2');
})) passed++; else failed++;
// Cleanup — restore both HOME and USERPROFILE (Windows)
process.env.HOME = origHome;
if (origUserProfile !== undefined) {
process.env.USERPROFILE = origUserProfile;
} else {
delete process.env.USERPROFILE;
}
try {
fs.rmSync(tmpHome, { recursive: true, force: true });
} catch {
// best-effort
}
// ── Round 48: rapid sequential saves data integrity ──
console.log('\nRound 48: rapid sequential saves:');
if (test('rapid sequential setAlias calls maintain data integrity', () => {
resetAliases();
for (let i = 0; i < 5; i++) {
const result = aliases.setAlias(`rapid-${i}`, `/path/${i}`, `Title ${i}`);
assert.strictEqual(result.success, true, `setAlias rapid-${i} should succeed`);
}
const data = aliases.loadAliases();
for (let i = 0; i < 5; i++) {
assert.ok(data.aliases[`rapid-${i}`], `rapid-${i} should exist after all saves`);
assert.strictEqual(data.aliases[`rapid-${i}`].sessionPath, `/path/${i}`);
}
assert.strictEqual(data.metadata.totalCount, 5, 'Metadata count should match actual aliases');
})) passed++; else failed++;
// ── Round 56: Windows platform unlink-before-rename code path ──
console.log('\nRound 56: Windows platform atomic write path:');
if (test('Windows platform mock: unlinks existing file before rename', () => {
resetAliases();
// First create an alias so the file exists
const r1 = aliases.setAlias('win-initial', '2026-01-01-abc123-session.tmp');
assert.strictEqual(r1.success, true, 'Initial alias should succeed');
const aliasesPath = aliases.getAliasesPath();
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist before win32 test');
// Mock process.platform to 'win32' to trigger the unlink-before-rename path
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
try {
// This save triggers the Windows code path: unlink existing → rename temp
const r2 = aliases.setAlias('win-updated', '2026-02-01-def456-session.tmp');
assert.strictEqual(r2.success, true, 'setAlias should succeed under win32 mock');
// Verify data integrity after the Windows path
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist after win32 save');
const data = aliases.loadAliases();
assert.ok(data.aliases['win-initial'], 'Original alias should still exist');
assert.ok(data.aliases['win-updated'], 'New alias should exist');
assert.strictEqual(data.aliases['win-updated'].sessionPath,
'2026-02-01-def456-session.tmp', 'Session path should match');
// No .tmp or .bak files left behind
assert.ok(!fs.existsSync(aliasesPath + '.tmp'), 'No temp file should remain');
assert.ok(!fs.existsSync(aliasesPath + '.bak'), 'No backup file should remain');
} finally {
// Restore original platform descriptor
if (origPlatform) {
Object.defineProperty(process, 'platform', origPlatform);
}
resetAliases();
}
})) passed++; else failed++;
// ── Round 64: loadAliases backfills missing version and metadata ──
console.log('\nRound 64: loadAliases version/metadata backfill:');
if (test('loadAliases backfills missing version and metadata fields', () => {
resetAliases();
const aliasesPath = aliases.getAliasesPath();
// Write a file with valid aliases but NO version and NO metadata
fs.writeFileSync(aliasesPath, JSON.stringify({
aliases: {
'backfill-test': {
sessionPath: '/sessions/backfill',
createdAt: '2026-01-15T00:00:00.000Z',
updatedAt: '2026-01-15T00:00:00.000Z',
title: 'Backfill Test'
}
}
}));
const data = aliases.loadAliases();
// Version should be backfilled to ALIAS_VERSION ('1.0')
assert.strictEqual(data.version, '1.0', 'Should backfill missing version to 1.0');
// Metadata should be backfilled with totalCount from aliases
assert.ok(data.metadata, 'Should backfill missing metadata object');
assert.strictEqual(data.metadata.totalCount, 1, 'Metadata totalCount should match alias count');
assert.ok(data.metadata.lastUpdated, 'Metadata should have lastUpdated');
// Alias data should be preserved
assert.ok(data.aliases['backfill-test'], 'Alias data should be preserved');
assert.strictEqual(data.aliases['backfill-test'].sessionPath, '/sessions/backfill');
resetAliases();
})) passed++; else failed++;
// ── Round 67: loadAliases empty file, resolveSessionAlias null, metadata-only backfill ──
console.log('\nRound 67: loadAliases (empty 0-byte file):');
if (test('loadAliases returns default structure for empty (0-byte) file', () => {
resetAliases();
const aliasesPath = aliases.getAliasesPath();
// Write a 0-byte file — readFile returns '', which is falsy → !content branch
fs.writeFileSync(aliasesPath, '');
const data = aliases.loadAliases();
assert.ok(data.aliases, 'Should have aliases key');
assert.strictEqual(Object.keys(data.aliases).length, 0, 'Should have no aliases');
assert.strictEqual(data.version, '1.0', 'Should have default version');
assert.ok(data.metadata, 'Should have metadata');
assert.strictEqual(data.metadata.totalCount, 0, 'Should have totalCount 0');
resetAliases();
})) passed++; else failed++;
console.log('\nRound 67: resolveSessionAlias (null/falsy input):');
if (test('resolveSessionAlias returns null when given null input', () => {
resetAliases();
const result = aliases.resolveSessionAlias(null);
assert.strictEqual(result, null, 'Should return null for null input');
})) passed++; else failed++;
console.log('\nRound 67: loadAliases (metadata-only backfill, version present):');
if (test('loadAliases backfills only metadata when version already present', () => {
resetAliases();
const aliasesPath = aliases.getAliasesPath();
// Write a file WITH version but WITHOUT metadata
fs.writeFileSync(aliasesPath, JSON.stringify({
version: '1.0',
aliases: {
'meta-only': {
sessionPath: '/sessions/meta-only',
createdAt: '2026-01-20T00:00:00.000Z',
updatedAt: '2026-01-20T00:00:00.000Z',
title: 'Metadata Only Test'
}
}
}));
const data = aliases.loadAliases();
// Version should remain as-is (NOT overwritten)
assert.strictEqual(data.version, '1.0', 'Version should remain 1.0');
// Metadata should be backfilled
assert.ok(data.metadata, 'Should backfill missing metadata');
assert.strictEqual(data.metadata.totalCount, 1, 'Metadata totalCount should be 1');
assert.ok(data.metadata.lastUpdated, 'Metadata should have lastUpdated');
// Alias data should be preserved
assert.ok(data.aliases['meta-only'], 'Alias should be preserved');
assert.strictEqual(data.aliases['meta-only'].title, 'Metadata Only Test');
resetAliases();
})) passed++; else failed++;
// ── Round 70: updateAliasTitle save failure path ──
console.log('\nupdateAliasTitle save failure (Round 70):');
if (test('updateAliasTitle returns failure when saveAliases fails (read-only dir)', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return;
}
// Use a fresh isolated HOME to avoid .tmp/.bak leftovers from other tests.
// On macOS, overwriting an EXISTING file in a read-only dir succeeds,
// so we must start clean with ONLY the .json file present.
const isoHome = path.join(os.tmpdir(), `ecc-alias-r70-${Date.now()}`);
const isoClaudeDir = path.join(isoHome, '.claude');
fs.mkdirSync(isoClaudeDir, { recursive: true });
const savedHome = process.env.HOME;
const savedProfile = process.env.USERPROFILE;
try {
process.env.HOME = isoHome;
process.env.USERPROFILE = isoHome;
// Re-require to pick up new HOME
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
const freshAliases = require('../../scripts/lib/session-aliases');
// Set up a valid alias
freshAliases.setAlias('title-save-fail', '/path/session', 'Original Title');
// Verify no leftover .tmp/.bak
const ap = freshAliases.getAliasesPath();
assert.ok(fs.existsSync(ap), 'Alias file should exist after setAlias');
// Make .claude dir read-only so saveAliases fails when creating .bak
fs.chmodSync(isoClaudeDir, 0o555);
const result = freshAliases.updateAliasTitle('title-save-fail', 'New Title');
assert.strictEqual(result.success, false, 'Should fail when save is blocked');
assert.ok(result.error.includes('Failed to update alias title'),
`Should return save failure error, got: ${result.error}`);
} finally {
try { fs.chmodSync(isoClaudeDir, 0o755); } catch { /* best-effort */ }
process.env.HOME = savedHome;
process.env.USERPROFILE = savedProfile;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
fs.rmSync(isoHome, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Round 72: deleteAlias save failure path ──
console.log('\nRound 72: deleteAlias (save failure):');
if (test('deleteAlias returns failure when saveAliases fails (read-only dir)', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return;
}
const isoHome = path.join(os.tmpdir(), `ecc-alias-r72-${Date.now()}`);
const isoClaudeDir = path.join(isoHome, '.claude');
fs.mkdirSync(isoClaudeDir, { recursive: true });
const savedHome = process.env.HOME;
const savedProfile = process.env.USERPROFILE;
try {
process.env.HOME = isoHome;
process.env.USERPROFILE = isoHome;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
const freshAliases = require('../../scripts/lib/session-aliases');
// Create an alias first (writes the file)
freshAliases.setAlias('to-delete', '/path/session', 'Test');
const ap = freshAliases.getAliasesPath();
assert.ok(fs.existsSync(ap), 'Alias file should exist after setAlias');
// Make .claude directory read-only — save will fail (can't create temp file)
fs.chmodSync(isoClaudeDir, 0o555);
const result = freshAliases.deleteAlias('to-delete');
assert.strictEqual(result.success, false, 'Should fail when save is blocked');
assert.ok(result.error.includes('Failed to delete alias'),
`Should return delete failure error, got: ${result.error}`);
} finally {
try { fs.chmodSync(isoClaudeDir, 0o755); } catch { /* best-effort */ }
process.env.HOME = savedHome;
process.env.USERPROFILE = savedProfile;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
fs.rmSync(isoHome, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Round 73: cleanupAliases save failure path ──
console.log('\nRound 73: cleanupAliases (save failure):');
if (test('cleanupAliases returns failure when saveAliases fails after removing aliases', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return;
}
const isoHome = path.join(os.tmpdir(), `ecc-alias-r73-cleanup-${Date.now()}`);
const isoClaudeDir = path.join(isoHome, '.claude');
fs.mkdirSync(isoClaudeDir, { recursive: true });
const savedHome = process.env.HOME;
const savedProfile = process.env.USERPROFILE;
try {
process.env.HOME = isoHome;
process.env.USERPROFILE = isoHome;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
const freshAliases = require('../../scripts/lib/session-aliases');
// Create aliases — one to keep, one to remove
freshAliases.setAlias('keep-me', '/sessions/real', 'Kept');
freshAliases.setAlias('remove-me', '/sessions/gone', 'Gone');
// Make .claude dir read-only so save will fail
fs.chmodSync(isoClaudeDir, 0o555);
// Cleanup: "gone" session doesn't exist, so remove-me should be removed
const result = freshAliases.cleanupAliases((p) => p === '/sessions/real');
assert.strictEqual(result.success, false, 'Should fail when save is blocked');
assert.ok(result.error.includes('Failed to save after cleanup'),
`Should return cleanup save failure error, got: ${result.error}`);
assert.strictEqual(result.removed, 1, 'Should report 1 removed alias');
assert.ok(result.removedAliases.some(a => a.name === 'remove-me'),
'Should report remove-me in removedAliases');
} finally {
try { fs.chmodSync(isoClaudeDir, 0o755); } catch { /* best-effort */ }
process.env.HOME = savedHome;
process.env.USERPROFILE = savedProfile;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
fs.rmSync(isoHome, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Round 73: setAlias save failure path ──
console.log('\nRound 73: setAlias (save failure):');
if (test('setAlias returns failure when saveAliases fails', () => {
if (process.platform === 'win32' || process.getuid?.() === 0) {
console.log(' (skipped — chmod ineffective on Windows/root)');
return;
}
const isoHome = path.join(os.tmpdir(), `ecc-alias-r73-set-${Date.now()}`);
const isoClaudeDir = path.join(isoHome, '.claude');
fs.mkdirSync(isoClaudeDir, { recursive: true });
const savedHome = process.env.HOME;
const savedProfile = process.env.USERPROFILE;
try {
process.env.HOME = isoHome;
process.env.USERPROFILE = isoHome;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
const freshAliases = require('../../scripts/lib/session-aliases');
// Make .claude dir read-only BEFORE any setAlias call
fs.chmodSync(isoClaudeDir, 0o555);
const result = freshAliases.setAlias('my-alias', '/sessions/test', 'Test');
assert.strictEqual(result.success, false, 'Should fail when save is blocked');
assert.ok(result.error.includes('Failed to save alias'),
`Should return save failure error, got: ${result.error}`);
} finally {
try { fs.chmodSync(isoClaudeDir, 0o755); } catch { /* best-effort */ }
process.env.HOME = savedHome;
process.env.USERPROFILE = savedProfile;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
fs.rmSync(isoHome, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Round 84: listAliases sort NaN date fallback (getTime() || 0) ──
console.log('\nRound 84: listAliases (NaN date fallback in sort comparator):');
if (test('listAliases sorts entries with invalid/missing dates to the end via || 0 fallback', () => {
// session-aliases.js line 257:
// (new Date(b.updatedAt || b.createdAt || 0).getTime() || 0) - ...
// When updatedAt and createdAt are both invalid strings, getTime() returns NaN.
// The outer || 0 converts NaN to 0 (epoch time), pushing the entry to the end.
resetAliases();
const data = aliases.loadAliases();
// Entry with valid dates — should sort first (newest)
data.aliases['valid-alias'] = {
sessionPath: '/sessions/valid',
createdAt: '2026-02-10T12:00:00.000Z',
updatedAt: '2026-02-10T12:00:00.000Z',
title: 'Valid'
};
// Entry with invalid date strings — getTime() → NaN → || 0 → epoch (oldest)
data.aliases['nan-alias'] = {
sessionPath: '/sessions/nan',
createdAt: 'not-a-date',
updatedAt: 'also-invalid',
title: 'NaN dates'
};
// Entry with missing date fields — undefined || undefined || 0 → new Date(0) → epoch
data.aliases['missing-alias'] = {
sessionPath: '/sessions/missing',
title: 'Missing dates'
// No createdAt or updatedAt
};
aliases.saveAliases(data);
const list = aliases.listAliases();
assert.strictEqual(list.length, 3, 'Should list all 3 aliases');
// Valid-dated entry should be first (newest by updatedAt)
assert.strictEqual(list[0].name, 'valid-alias',
'Entry with valid dates should sort first');
// The two invalid-dated entries sort to epoch (0), so they come after
assert.ok(
(list[1].name === 'nan-alias' || list[1].name === 'missing-alias') &&
(list[2].name === 'nan-alias' || list[2].name === 'missing-alias'),
'Entries with invalid/missing dates should sort to the end');
})) passed++; else failed++;
// ── Round 86: loadAliases with truthy non-object aliases field ──
console.log('\nRound 86: loadAliases (truthy non-object aliases field):');
if (test('loadAliases resets to defaults when aliases field is a string (typeof !== object)', () => {
// session-aliases.js line 58: if (!data.aliases || typeof data.aliases !== 'object')
// Previous tests covered !data.aliases (undefined) via { noAliasesKey: true }.
// This exercises the SECOND half: aliases is truthy but typeof !== 'object'.
const aliasesPath = aliases.getAliasesPath();
fs.writeFileSync(aliasesPath, JSON.stringify({
version: '1.0',
aliases: 'this-is-a-string-not-an-object',
metadata: { totalCount: 0 }
}));
const data = aliases.loadAliases();
assert.strictEqual(typeof data.aliases, 'object', 'Should reset aliases to object');
assert.ok(!Array.isArray(data.aliases), 'Should be a plain object, not array');
assert.strictEqual(Object.keys(data.aliases).length, 0, 'Should have no aliases');
assert.strictEqual(data.version, '1.0', 'Should have version');
resetAliases();
})) passed++; else failed++;
// ── Round 90: saveAliases backup restore double failure (inner catch restoreErr) ──
console.log('\nRound 90: saveAliases (backup restore double failure):');
if (test('saveAliases triggers inner restoreErr catch when both save and restore fail', () => {
// session-aliases.js lines 131-137: When saveAliases fails (outer catch),
// it tries to restore from backup. If the restore ALSO fails, the inner
// catch at line 135 logs restoreErr. No existing test creates this double-fault.
if (process.platform === 'win32') {
console.log(' (skipped — chmod not reliable on Windows)');
return;
}
const isoHome = path.join(os.tmpdir(), `ecc-r90-restore-fail-${Date.now()}`);
const claudeDir = path.join(isoHome, '.claude');
fs.mkdirSync(claudeDir, { recursive: true });
// Pre-create a backup file while directory is still writable
const backupPath = path.join(claudeDir, 'session-aliases.json.bak');
fs.writeFileSync(backupPath, JSON.stringify({ aliases: {}, version: '1.0' }));
// Make .claude directory read-only (0o555):
// 1. writeFileSync(tempPath) → EACCES (can't create file in read-only dir) — outer catch
// 2. copyFileSync(backupPath, aliasesPath) → EACCES (can't create target) — inner catch (line 135)
fs.chmodSync(claudeDir, 0o555);
const origH = process.env.HOME;
const origP = process.env.USERPROFILE;
process.env.HOME = isoHome;
process.env.USERPROFILE = isoHome;
try {
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
const freshAliases = require('../../scripts/lib/session-aliases');
const result = freshAliases.saveAliases({ aliases: { x: 1 }, version: '1.0' });
assert.strictEqual(result, false, 'Should return false when save fails');
// Backup should still exist (restore also failed, so backup was not consumed)
assert.ok(fs.existsSync(backupPath), 'Backup should still exist after double failure');
} finally {
process.env.HOME = origH;
process.env.USERPROFILE = origP;
delete require.cache[require.resolve('../../scripts/lib/session-aliases')];
delete require.cache[require.resolve('../../scripts/lib/utils')];
try { fs.chmodSync(claudeDir, 0o755); } catch { /* best-effort */ }
fs.rmSync(isoHome, { recursive: true, force: true });
}
})) passed++; else failed++;
// ── Round 95: renameAlias with same old and new name (self-rename) ──
console.log('\nRound 95: renameAlias (self-rename same name):');
if (test('renameAlias returns "already exists" error when renaming alias to itself', () => {
resetAliases();
// Create an alias first
const created = aliases.setAlias('self-rename', '/path/session', 'Self Rename');
assert.strictEqual(created.success, true, 'Setup: alias should be created');
// Attempt to rename to the same name
const result = aliases.renameAlias('self-rename', 'self-rename');
assert.strictEqual(result.success, false, 'Renaming to itself should fail');
assert.ok(result.error.includes('already exists'),
'Error should indicate alias already exists (line 333-334 check)');
// Verify original alias is still intact
const resolved = aliases.resolveAlias('self-rename');
assert.ok(resolved, 'Original alias should still exist after failed self-rename');
assert.strictEqual(resolved.sessionPath, '/path/session',
'Alias data should be preserved');
})) passed++; else failed++;
// ── Round 100: cleanupAliases callback returning falsy non-boolean 0 ──
console.log('\nRound 100: cleanupAliases (callback returns 0 — falsy non-boolean coercion):');
if (test('cleanupAliases removes alias when callback returns 0 (falsy coercion: !0 === true)', () => {
resetAliases();
aliases.setAlias('zero-test', '/sessions/some-session', '2026-01-15');
// callback returns 0 (a falsy value) — !0 === true → alias is removed
const result = aliases.cleanupAliases(() => 0);
assert.strictEqual(result.removed, 1,
'Alias should be removed because !0 === true (JavaScript falsy coercion)');
assert.strictEqual(result.success, true,
'Cleanup should succeed');
const resolved = aliases.resolveAlias('zero-test');
assert.strictEqual(resolved, null,
'Alias should no longer exist after removal');
})) passed++; else failed++;
// ── Round 102: setAlias with title=0 (falsy number coercion) ──
console.log('\nRound 102: setAlias (title=0 — falsy coercion silently converts to null):');
if (test('setAlias with title=0 stores null (0 || null === null due to JavaScript falsy coercion)', () => {
// session-aliases.js line 221: `title: title || null` — the value 0 is falsy
// in JavaScript, so `0 || null` evaluates to `null`. This means numeric
// titles like 0 are silently discarded.
resetAliases();
const result = aliases.setAlias('zero-title', '/sessions/test', 0);
assert.strictEqual(result.success, true,
'setAlias should succeed (0 is valid as a truthy check bypass)');
assert.strictEqual(result.title, null,
'Title should be null because 0 || null === null (falsy coercion)');
const resolved = aliases.resolveAlias('zero-title');
assert.strictEqual(resolved.title, null,
'Persisted title should be null after round-trip through saveAliases/loadAliases');
})) passed++; else failed++;
// ── Round 103: loadAliases with array aliases in JSON (typeof [] === 'object' bypass) ──
console.log('\nRound 103: loadAliases (array aliases — typeof bypass):');
if (test('loadAliases accepts array aliases because typeof [] === "object" passes validation', () => {
// session-aliases.js line 58: `typeof data.aliases !== 'object'` is the guard.
// Arrays are typeof 'object' in JavaScript, so {"aliases": [1,2,3]} passes
// validation. The returned data.aliases is an array, not a plain object.
// Downstream code (Object.keys, Object.entries, bracket access) behaves
// differently on arrays vs objects but doesn't crash — it just produces
// unexpected results like numeric string keys "0", "1", "2".
resetAliases();
const aliasesPath = aliases.getAliasesPath();
fs.writeFileSync(aliasesPath, JSON.stringify({
version: '1.0',
aliases: ['item0', 'item1', 'item2'],
metadata: { totalCount: 3, lastUpdated: new Date().toISOString() }
}));
const data = aliases.loadAliases();
// The array passes the typeof 'object' check and is returned as-is
assert.ok(Array.isArray(data.aliases),
'data.aliases should be an array (typeof [] === "object" bypasses guard)');
assert.strictEqual(data.aliases.length, 3,
'Array should have 3 elements');
// Object.keys on an array returns ["0", "1", "2"] — numeric index strings
const keys = Object.keys(data.aliases);
assert.deepStrictEqual(keys, ['0', '1', '2'],
'Object.keys of array returns numeric string indices, not named alias keys');
})) passed++; else failed++;
// ── Round 104: resolveSessionAlias with path-traversal input (passthrough without validation) ──
console.log('\nRound 104: resolveSessionAlias (path-traversal input — returned unchanged):');
if (test('resolveSessionAlias returns path-traversal input as-is when alias lookup fails', () => {
// session-aliases.js lines 365-374: resolveSessionAlias first tries resolveAlias(),
// which rejects '../etc/passwd' because the regex /^[a-zA-Z0-9_-]+$/ fails on dots
// and slashes (returns null). Then the function falls through to line 373:
// `return aliasOrId` — returning the potentially dangerous input unchanged.
// Callers that blindly use this return value could be at risk.
resetAliases();
const traversal = '../etc/passwd';
const result = aliases.resolveSessionAlias(traversal);
assert.strictEqual(result, traversal,
'Path-traversal input should be returned as-is (resolveAlias rejects it, fallback returns input)');
// Also test with another invalid alias pattern
const dotSlash = './../../secrets';
const result2 = aliases.resolveSessionAlias(dotSlash);
assert.strictEqual(result2, dotSlash,
'Another path-traversal pattern also returned unchanged');
})) passed++; else failed++;
// ── Round 107: setAlias with whitespace-only title (not trimmed unlike sessionPath) ──
console.log('\nRound 107: setAlias (whitespace-only title — truthy string stored as-is, unlike sessionPath which is trim-checked):');
if (test('setAlias stores whitespace-only title as-is (no trim validation, unlike sessionPath)', () => {
resetAliases();
// sessionPath with whitespace is rejected (line 195: sessionPath.trim().length === 0)
const pathResult = aliases.setAlias('ws-path', ' ');
assert.strictEqual(pathResult.success, false,
'Whitespace-only sessionPath is rejected by trim check');
// But title with whitespace is stored as-is (line 221: title || null — whitespace is truthy)
const titleResult = aliases.setAlias('ws-title', '/valid/path', ' ');
assert.strictEqual(titleResult.success, true,
'Whitespace-only title is accepted (no trim check on title)');
assert.strictEqual(titleResult.title, ' ',
'Title stored as whitespace string (truthy, so title || null returns the whitespace)');
// Verify persisted correctly
const loaded = aliases.loadAliases();
assert.strictEqual(loaded.aliases['ws-title'].title, ' ',
'Whitespace title persists in JSON as-is');
})) passed++; else failed++;
// ── Round 111: setAlias with exactly 128-character alias — off-by-one boundary ──
console.log('\nRound 111: setAlias (128-char alias — exact boundary of > 128 check):');
if (test('setAlias accepts alias of exactly 128 characters (128 is NOT > 128)', () => {
// session-aliases.js line 199: if (alias.length > 128)
// 128 is NOT > 128, so exactly 128 chars is ACCEPTED.
// Existing test only checks 129 (rejected).
resetAliases();
const alias128 = 'a'.repeat(128);
const result = aliases.setAlias(alias128, '/path/to/session');
assert.strictEqual(result.success, true,
'128-char alias should be accepted (128 is NOT > 128)');
assert.strictEqual(result.isNew, true);
// Verify it can be resolved
const resolved = aliases.resolveAlias(alias128);
assert.notStrictEqual(resolved, null, '128-char alias should be resolvable');
assert.strictEqual(resolved.sessionPath, '/path/to/session');
// Confirm 129 is rejected (boundary)
const result129 = aliases.setAlias('b'.repeat(129), '/path');
assert.strictEqual(result129.success, false, '129-char alias should be rejected');
assert.ok(result129.error.includes('128'),
'Error message should mention 128-char limit');
})) passed++; else failed++;
// ── Round 112: resolveAlias rejects Unicode characters in alias name ──
console.log('\nRound 112: resolveAlias (Unicode rejection):');
if (test('resolveAlias returns null for alias names containing Unicode characters', () => {
resetAliases();
// First create a valid alias to ensure the store works
aliases.setAlias('valid-alias', '/path/to/session');
const validResult = aliases.resolveAlias('valid-alias');
assert.notStrictEqual(validResult, null, 'Valid ASCII alias should resolve');
// Unicode accented characters — rejected by /^[a-zA-Z0-9_-]+$/
const accentedResult = aliases.resolveAlias('café-session');
assert.strictEqual(accentedResult, null,
'Accented character "é" should be rejected by [a-zA-Z0-9_-]');
const umlautResult = aliases.resolveAlias('über-test');
assert.strictEqual(umlautResult, null,
'Umlaut "ü" should be rejected by [a-zA-Z0-9_-]');
// CJK characters
const cjkResult = aliases.resolveAlias('会議-notes');
assert.strictEqual(cjkResult, null,
'CJK characters should be rejected');
// Emoji
const emojiResult = aliases.resolveAlias('rocket-🚀');
assert.strictEqual(emojiResult, null,
'Emoji should be rejected by the ASCII-only regex');
// Cyrillic characters that look like Latin (homoglyphs)
const cyrillicResult = aliases.resolveAlias('tеst'); // 'е' is Cyrillic U+0435
assert.strictEqual(cyrillicResult, null,
'Cyrillic homoglyph "е" (U+0435) should be rejected even though it looks like "e"');
})) passed++; else failed++;
// ── Round 114: listAliases with non-string search (number) — TypeError on toLowerCase ──
console.log('\nRound 114: listAliases (non-string search — number triggers TypeError):');
if (test('listAliases throws TypeError when search option is a number (no toLowerCase method)', () => {
resetAliases();
// Set up some aliases to search through
aliases.setAlias('alpha-session', '/path/to/alpha');
aliases.setAlias('beta-session', '/path/to/beta');
// String search works fine — baseline
const stringResult = aliases.listAliases({ search: 'alpha' });
assert.strictEqual(stringResult.length, 1, 'String search should find 1 match');
assert.strictEqual(stringResult[0].name, 'alpha-session');
// Numeric search — search.toLowerCase() at line 261 of session-aliases.js
// throws TypeError because Number.prototype has no toLowerCase method.
// The code does NOT guard against non-string search values.
assert.throws(
() => aliases.listAliases({ search: 123 }),
(err) => err instanceof TypeError && /toLowerCase/.test(err.message),
'Numeric search value should throw TypeError from toLowerCase call'
);
// Boolean search — also lacks toLowerCase
assert.throws(
() => aliases.listAliases({ search: true }),
(err) => err instanceof TypeError && /toLowerCase/.test(err.message),
'Boolean search value should also throw TypeError'
);
})) passed++; else failed++;
// ── Round 115: updateAliasTitle with empty string — stored as null via || but returned as "" ──
console.log('\nRound 115: updateAliasTitle (empty string title — stored null, returned ""):');
if (test('updateAliasTitle with empty string stores null but returns empty string (|| coercion mismatch)', () => {
resetAliases();
// Create alias with a title
aliases.setAlias('r115-alias', '/path/to/session', 'Original Title');
const before = aliases.resolveAlias('r115-alias');
assert.strictEqual(before.title, 'Original Title', 'Baseline: title should be set');
// Update title with empty string
// Line 383: typeof "" === 'string' → passes validation
// Line 393: "" || null → null (empty string is falsy in JS)
// Line 400: returns { title: "" } (original parameter, not stored value)
const result = aliases.updateAliasTitle('r115-alias', '');
assert.strictEqual(result.success, true, 'Should succeed (empty string passes validation)');
assert.strictEqual(result.title, '', 'Return value reflects the input parameter (empty string)');
// But what's actually stored?
const after = aliases.resolveAlias('r115-alias');
assert.strictEqual(after.title, null,
'Stored title should be null because "" || null evaluates to null');
// Contrast: non-empty string is stored as-is
aliases.updateAliasTitle('r115-alias', 'New Title');
const withTitle = aliases.resolveAlias('r115-alias');
assert.strictEqual(withTitle.title, 'New Title', 'Non-empty string stored as-is');
// null explicitly clears title
aliases.updateAliasTitle('r115-alias', null);
const cleared = aliases.resolveAlias('r115-alias');
assert.strictEqual(cleared.title, null, 'null clears title');
})) passed++; else failed++;
// ── Round 116: loadAliases with extra unknown fields — silently preserved ──
console.log('\nRound 116: loadAliases (extra unknown JSON fields — preserved by loose validation):');
if (test('loadAliases preserves extra unknown fields because only aliases key is validated', () => {
resetAliases();
// Manually write an aliases file with extra fields
const aliasesPath = aliases.getAliasesPath();
const customData = {
version: '1.0',
aliases: {
'test-session': {
sessionPath: '/path/to/session',
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
title: 'Test'
}
},
metadata: {
totalCount: 1,
lastUpdated: '2026-01-01T00:00:00.000Z'
},
customField: 'extra data',
debugInfo: { level: 3, verbose: true },
tags: ['important', 'test']
};
fs.writeFileSync(aliasesPath, JSON.stringify(customData, null, 2), 'utf8');
// loadAliases only validates data.aliases — extra fields pass through
const loaded = aliases.loadAliases();
assert.ok(loaded.aliases['test-session'], 'Should load the valid alias');
assert.strictEqual(loaded.aliases['test-session'].title, 'Test');
assert.strictEqual(loaded.customField, 'extra data',
'Extra string field should be preserved');
assert.deepStrictEqual(loaded.debugInfo, { level: 3, verbose: true },
'Extra object field should be preserved');
assert.deepStrictEqual(loaded.tags, ['important', 'test'],
'Extra array field should be preserved');
// After saving, extra fields survive a round-trip (saveAliases only updates metadata)
aliases.setAlias('new-alias', '/path/to/new');
const reloaded = aliases.loadAliases();
assert.ok(reloaded.aliases['new-alias'], 'New alias should be saved');
assert.strictEqual(reloaded.customField, 'extra data',
'Extra field should survive save/load round-trip');
})) passed++; else failed++;
// ── Round 118: renameAlias to the same name — "already exists" because self-check ──
console.log('\nRound 118: renameAlias (same name — "already exists" because data.aliases[newAlias] is truthy):');
if (test('renameAlias to the same name returns "already exists" error (no self-rename short-circuit)', () => {
resetAliases();
aliases.setAlias('same-name', '/path/to/session');
// Rename 'same-name' → 'same-name'
// Line 333: data.aliases[newAlias] → truthy (the alias exists under that name)
// Returns error before checking if oldAlias === newAlias
const result = aliases.renameAlias('same-name', 'same-name');
assert.strictEqual(result.success, false, 'Should fail');
assert.ok(result.error.includes('already exists'),
'Error should say "already exists" (not "same name" or a no-op success)');
// Verify alias is unchanged
const resolved = aliases.resolveAlias('same-name');
assert.ok(resolved, 'Original alias should still exist');
assert.strictEqual(resolved.sessionPath, '/path/to/session');
})) passed++; else failed++;
// ── Round 118: setAlias reserved names — case-insensitive rejection ──
console.log('\nRound 118: setAlias (reserved names — case-insensitive rejection):');
if (test('setAlias rejects all reserved names case-insensitively (list, help, remove, delete, create, set)', () => {
resetAliases();
// All reserved names in lowercase
const reserved = ['list', 'help', 'remove', 'delete', 'create', 'set'];
for (const name of reserved) {
const result = aliases.setAlias(name, '/path/to/session');
assert.strictEqual(result.success, false,
`'${name}' should be rejected as reserved`);
assert.ok(result.error.includes('reserved'),
`Error for '${name}' should mention "reserved"`);
}
// Case-insensitive: uppercase variants also rejected
const upperResult = aliases.setAlias('LIST', '/path/to/session');
assert.strictEqual(upperResult.success, false,
'"LIST" (uppercase) should be rejected (toLowerCase check)');
const mixedResult = aliases.setAlias('Help', '/path/to/session');
assert.strictEqual(mixedResult.success, false,
'"Help" (mixed case) should be rejected');
const allCapsResult = aliases.setAlias('DELETE', '/path/to/session');
assert.strictEqual(allCapsResult.success, false,
'"DELETE" (all caps) should be rejected');
// Non-reserved names work fine
const validResult = aliases.setAlias('my-session', '/path/to/session');
assert.strictEqual(validResult.success, true,
'Non-reserved name should succeed');
})) passed++; else failed++;
// Summary
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();