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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Tests for Claude session discovery via on-disk jsonl introspection.
*
* Issue #829 — conversation resume.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, utimesSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
encodeClaudeProjectDir,
findLatestSessionId,
} from '../utils/claude-session-discovery.js';

describe('encodeClaudeProjectDir', () => {
it('replaces / with -', () => {
expect(encodeClaudeProjectDir('/Users/x/repo')).toBe('-Users-x-repo');
});

it('replaces . with -', () => {
expect(encodeClaudeProjectDir('/Users/x/repo/.builders/pir-1')).toBe(
'-Users-x-repo--builders-pir-1',
);
});

it('leaves dashes in the source path untouched', () => {
expect(encodeClaudeProjectDir('/Users/x/repo/pir-1298')).toBe(
'-Users-x-repo-pir-1298',
);
});

it('handles paths with multiple dots and slashes', () => {
expect(encodeClaudeProjectDir('/a/b.c/.d.e/f')).toBe('-a-b-c--d-e-f');
});
});

describe('findLatestSessionId', () => {
let fakeHome: string;
let projectsRoot: string;

beforeEach(() => {
fakeHome = mkdtempSync(join(tmpdir(), 'csd-test-'));
projectsRoot = join(fakeHome, '.claude', 'projects');
mkdirSync(projectsRoot, { recursive: true });
});

afterEach(() => {
rmSync(fakeHome, { recursive: true, force: true });
});

function writeSession(absPath: string, uuid: string, mtime: number): void {
const dir = join(projectsRoot, encodeClaudeProjectDir(absPath));
mkdirSync(dir, { recursive: true });
const file = join(dir, `${uuid}.jsonl`);
writeFileSync(file, `{"sessionId":"${uuid}"}\n`, 'utf-8');
const t = mtime / 1000;
utimesSync(file, t, t);
}

it('returns the newest session UUID by mtime', () => {
const worktree = '/Users/x/repo/.builders/pir-1';
writeSession(worktree, 'old-uuid', 1_000_000_000_000);
writeSession(worktree, 'newest-uuid', 1_700_000_000_000);
writeSession(worktree, 'middle-uuid', 1_400_000_000_000);
expect(findLatestSessionId(worktree, { homeDir: fakeHome })).toBe('newest-uuid');
});

it('returns null when the project dir does not exist', () => {
expect(findLatestSessionId('/nonexistent/path', { homeDir: fakeHome })).toBeNull();
});

it('returns null when the project dir exists but contains no jsonl files', () => {
const worktree = '/Users/x/repo/.builders/pir-2';
const dir = join(projectsRoot, encodeClaudeProjectDir(worktree));
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'memory'), 'not a jsonl', 'utf-8');
expect(findLatestSessionId(worktree, { homeDir: fakeHome })).toBeNull();
});

it('ignores non-jsonl files and subdirectories', () => {
const worktree = '/Users/x/repo/.builders/pir-3';
const dir = join(projectsRoot, encodeClaudeProjectDir(worktree));
mkdirSync(dir, { recursive: true });
mkdirSync(join(dir, 'some-uuid'), { recursive: true });
writeFileSync(join(dir, 'history.txt'), 'text', 'utf-8');
writeSession(worktree, 'the-uuid', 1_500_000_000_000);
expect(findLatestSessionId(worktree, { homeDir: fakeHome })).toBe('the-uuid');
});

it('returns the single jsonl when only one exists', () => {
const worktree = '/Users/x/repo/.builders/pir-4';
writeSession(worktree, 'only-uuid', 1_500_000_000_000);
expect(findLatestSessionId(worktree, { homeDir: fakeHome })).toBe('only-uuid');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Tests for the discoverResumeSession helper — the spawn-CLI wrapper that
* gates findLatestSessionId on the --resume flag and surfaces a user-facing
* log line. (Issues #829 / #831.)
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, utimesSync } from 'node:fs';
import { tmpdir, homedir } from 'node:os';
import { join } from 'node:path';

import { discoverResumeSession } from '../commands/spawn.js';
import { encodeClaudeProjectDir } from '../utils/claude-session-discovery.js';

// discoverResumeSession reads from $HOME via os.homedir() through
// findLatestSessionId. Override the env var for the duration of the test so
// the helper looks at our fake home instead of the user's real one.
function pinHome<T>(fakeHome: string, fn: () => T): T {
const original = process.env.HOME;
process.env.HOME = fakeHome;
try {
return fn();
} finally {
if (original === undefined) delete process.env.HOME;
else process.env.HOME = original;
}
}

function writeSession(projectsRoot: string, absPath: string, uuid: string, mtimeMs: number): void {
const dir = join(projectsRoot, encodeClaudeProjectDir(absPath));
mkdirSync(dir, { recursive: true });
const file = join(dir, `${uuid}.jsonl`);
writeFileSync(file, `{"sessionId":"${uuid}"}\n`, 'utf-8');
const t = mtimeMs / 1000;
utimesSync(file, t, t);
}

describe('discoverResumeSession', () => {
let fakeHome: string;
let projectsRoot: string;

beforeEach(() => {
fakeHome = mkdtempSync(join(tmpdir(), 'drs-test-'));
projectsRoot = join(fakeHome, '.claude', 'projects');
mkdirSync(projectsRoot, { recursive: true });
});

afterEach(() => {
rmSync(fakeHome, { recursive: true, force: true });
});

it('returns undefined when isResume is false (no filesystem touch)', () => {
// Even if a jsonl exists, a non-resume spawn must not pick it up.
const worktree = '/Users/x/repo/.builders/spir-1';
writeSession(projectsRoot, worktree, 'should-not-pick', 1_700_000_000_000);
pinHome(fakeHome, () => {
expect(discoverResumeSession(worktree, false)).toBeUndefined();
});
});

it('returns undefined when isResume is undefined', () => {
const worktree = '/Users/x/repo/.builders/spir-2';
writeSession(projectsRoot, worktree, 'should-not-pick', 1_700_000_000_000);
pinHome(fakeHome, () => {
expect(discoverResumeSession(worktree, undefined)).toBeUndefined();
});
});

it('returns undefined when isResume is true but no jsonl exists', () => {
const worktree = '/Users/x/repo/.builders/spir-3-no-jsonl';
pinHome(fakeHome, () => {
expect(discoverResumeSession(worktree, true)).toBeUndefined();
});
});

it('returns the newest jsonl UUID when isResume is true and jsonls exist', () => {
const worktree = '/Users/x/repo/.builders/pir-1661';
writeSession(projectsRoot, worktree, 'older-uuid', 1_500_000_000_000);
writeSession(projectsRoot, worktree, 'newest-uuid', 1_700_000_000_000);
pinHome(fakeHome, () => {
expect(discoverResumeSession(worktree, true)).toBe('newest-uuid');
});
});

it('does not consult the filesystem when isResume is false (perf safety)', () => {
// Negative case: isResume=false short-circuits before any filesystem
// access happens. Tests pass even if HOME points at /nonexistent.
pinHome('/nonexistent-home-path', () => {
expect(discoverResumeSession('/some/worktree', false)).toBeUndefined();
});
});
});
Loading
Loading