import * as NodeServices from "@effect/platform-node/NodeServices";import { describe, expect, it } from "@effect/vitest";import { type OrchestrationProject, ProjectId } from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path"; import * as ServerConfig from "../config.ts";import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";import * as ServerSettings from "../serverSettings.ts";import * as AgentSessionScanner from "./AgentSessionScanner.ts"; const makeProject = (workspaceRoot: string): OrchestrationProject => ({ id: ProjectId.make("project-1"), title: "Imported", workspaceRoot, defaultModelSelection: null, scripts: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", deletedAt: null,}); /** Only `getActiveProjectByWorkspaceRoot` is exercised; the rest must not be called. */const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray<string>) => Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.die("unused"), getArchivedShellSnapshot: () => Effect.die("unused"), getSnapshotSequence: () => Effect.die("unused"), getCounts: () => Effect.die("unused"), getActiveProjectByWorkspaceRoot: (workspaceRoot) => Effect.succeed( importedWorkspaceRoots.includes(workspaceRoot) ? Option.some(makeProject(workspaceRoot)) : Option.none(), ), getProjectShellById: () => Effect.die("unused"), getFirstActiveThreadIdByProjectId: () => Effect.die("unused"), getThreadCheckpointContext: () => Effect.die("unused"), getFullThreadDiffContext: () => Effect.die("unused"), getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), searchThreads: () => Effect.die("unused"), }); /** * Run a scan against the given homes. Homes are temp dirs created inside the * test, so the layer is built per run rather than shared. */const runScan = (input: { readonly claudeHomePath: string; readonly codexHomePath: string; readonly importedWorkspaceRoots?: ReadonlyArray<string>; /** Base dir for the test ServerConfig; worktreesDir derives from it. */ readonly configBaseDir?: string;}) => Effect.gen(function* () { const scanner = yield* AgentSessionScanner.AgentSessionScanner; return yield* scanner.scan; }).pipe( Effect.provide( AgentSessionScanner.layer.pipe( Layer.provide( Layer.mergeAll( ServerSettings.layerTest({ providers: { claudeAgent: { homePath: input.claudeHomePath }, codex: { homePath: input.codexHomePath }, }, }), ServerConfig.layerTest( input.claudeHomePath, input.configBaseDir ?? { prefix: "t3code-scanner-config-" }, ), makeProjectionSnapshotQueryLayer(input.importedWorkspaceRoots ?? []), ), ), ), ), ); const makeTempDir = Effect.fn("AgentSessionScanner.test.makeTempDir")(function* (prefix: string) { const fileSystem = yield* FileSystem.FileSystem; return yield* fileSystem.makeTempDirectoryScoped({ prefix });}); const writeTranscript = Effect.fn("AgentSessionScanner.test.writeTranscript")(function* (input: { readonly filePath: string; readonly contents: string; /** Epoch millis, so ordering assertions never depend on write timing. */ readonly mtimeMs: number;}) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; yield* fileSystem.makeDirectory(path.dirname(input.filePath), { recursive: true }); yield* fileSystem.writeFileString(input.filePath, input.contents); // Numeric utimes arguments are seconds, not milliseconds. const seconds = input.mtimeMs / 1000; yield* fileSystem.utimes(input.filePath, seconds, seconds);}); /** Claude session line: the first record carries the real `cwd`. */const claudeSessionLine = (cwd: string) => `${JSON.stringify({ type: "user", cwd, sessionId: "s1" })}\n${JSON.stringify({ type: "assistant" })}\n`; /** Codex rollout line: session metadata is nested under `payload`. */const codexRolloutLine = (cwd: string) => `${JSON.stringify({ timestamp: "2026-01-01T00:00:00.000Z", type: "session_meta", payload: { id: "r1", cwd } })}\n`; it.layer(NodeServices.layer)("AgentSessionScanner", (it) => { describe("scan", () => { it.effect("reads Claude project cwds from transcripts, newest first", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const olderWorkspace = yield* makeTempDir("t3code-workspace-older-"); const newerWorkspace = yield* makeTempDir("t3code-workspace-newer-"); // Slugs are intentionally lossy; the scanner must not decode them. yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug-older", "a.jsonl"), contents: claudeSessionLine(olderWorkspace), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug-older", "b.jsonl"), contents: claudeSessionLine(olderWorkspace), mtimeMs: Date.parse("2026-01-02T00:00:00.000Z"), }); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug-newer", "c.jsonl"), contents: claudeSessionLine(newerWorkspace), mtimeMs: Date.parse("2026-03-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath }); expect(result.candidates).toEqual([ { path: newerWorkspace, title: path.basename(newerWorkspace), sources: ["claudeAgent"], threadCount: 1, lastActiveAt: "2026-03-01T00:00:00.000Z", alreadyImported: false, }, { path: olderWorkspace, title: path.basename(olderWorkspace), sources: ["claudeAgent"], threadCount: 2, lastActiveAt: "2026-01-02T00:00:00.000Z", alreadyImported: false, }, ]); }), ); it.effect("groups Codex rollouts by cwd across date directories", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const otherWorkspace = yield* makeTempDir("t3code-workspace-other-"); const rollout = (year: string, month: string, day: string, name: string) => path.join(codexHomePath, "sessions", year, month, day, name); yield* writeTranscript({ filePath: rollout("2026", "01", "05", "rollout-2026-01-05T10-00-00-aaa.jsonl"), contents: codexRolloutLine(workspace), mtimeMs: Date.parse("2026-01-05T10:00:00.000Z"), }); yield* writeTranscript({ filePath: rollout("2026", "02", "09", "rollout-2026-02-09T10-00-00-bbb.jsonl"), contents: codexRolloutLine(workspace), mtimeMs: Date.parse("2026-02-09T10:00:00.000Z"), }); yield* writeTranscript({ filePath: rollout("2026", "02", "09", "rollout-2026-02-09T11-00-00-ccc.jsonl"), contents: codexRolloutLine(otherWorkspace), mtimeMs: Date.parse("2026-02-09T11:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath }); expect(result.candidates).toEqual([ { path: otherWorkspace, title: path.basename(otherWorkspace), sources: ["codex"], threadCount: 1, lastActiveAt: "2026-02-09T11:00:00.000Z", alreadyImported: false, }, { path: workspace, title: path.basename(workspace), sources: ["codex"], threadCount: 2, lastActiveAt: "2026-02-09T10:00:00.000Z", alreadyImported: false, }, ]); }), ); it.effect("merges the same cwd seen by both agents and flags imported projects", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspace), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); yield* writeTranscript({ filePath: path.join( codexHomePath, "sessions", "2026", "04", "01", "rollout-2026-04-01T09-00-00-aaa.jsonl", ), contents: codexRolloutLine(workspace), mtimeMs: Date.parse("2026-04-01T09:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspace], }); expect(result.candidates).toEqual([ { path: workspace, title: path.basename(workspace), sources: ["claudeAgent", "codex"], threadCount: 2, lastActiveAt: "2026-04-01T09:00:00.000Z", alreadyImported: true, }, ]); }), ); it.effect("drops candidates whose directory no longer exists", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(path.join(claudeHomePath, "does-not-exist")), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath }); expect(result.candidates).toEqual([]); }), ); it.effect("excludes T3-managed worktree sandboxes", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const fileSystem = yield* FileSystem.FileSystem; const worktreeCwd = path.join(claudeHomePath, ".t3", "worktrees", "t3code", "wt-1"); yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true }); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(worktreeCwd), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath }); expect(result.candidates).toEqual([]); }), ); it.effect("excludes sandboxes under the configured worktrees dir without .t3 in the path", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const configBaseDir = yield* makeTempDir("t3code-scanner-base-"); const fileSystem = yield* FileSystem.FileSystem; // worktreesDir derives as `<baseDir>/worktrees`, and the temp base // dir contains no `.t3` segment — only the config-based prefix match // can exclude this one. const worktreeCwd = path.join(configBaseDir, "worktrees", "t3code", "wt-2"); yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true }); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(worktreeCwd), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, configBaseDir }); expect(result.candidates).toEqual([]); }), ); it.effect("excludes sandboxes reached through a symlink into the worktrees dir", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const configBaseDir = yield* makeTempDir("t3code-scanner-base-"); const linkParent = yield* makeTempDir("t3code-scanner-links-"); const fileSystem = yield* FileSystem.FileSystem; // The recorded cwd is a symlink whose own spelling looks harmless; // only its realpath reveals the managed sandbox. const worktreeCwd = path.join(configBaseDir, "worktrees", "t3code", "wt-3"); yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true }); const symlinkCwd = path.join(linkParent, "innocent-project"); yield* fileSystem.symlink(worktreeCwd, symlinkCwd); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(symlinkCwd), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, configBaseDir }); expect(result.candidates).toEqual([]); }), ); it.effect("skips malformed transcripts without failing the scan", () => Effect.gen(function* () { const path = yield* Path.Path; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-broken", "a.jsonl"), contents: "not json at all\n", mtimeMs: Date.parse("2026-05-01T00:00:00.000Z"), }); // Valid JSON, but no cwd anywhere in the record. yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-no-cwd", "a.jsonl"), contents: `{"type":"summary"}\n`, mtimeMs: Date.parse("2026-05-02T00:00:00.000Z"), }); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-good", "a.jsonl"), contents: claudeSessionLine(workspace), mtimeMs: Date.parse("2026-05-03T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath }); expect(result.candidates).toEqual([ { path: workspace, title: path.basename(workspace), sources: ["claudeAgent"], threadCount: 1, lastActiveAt: "2026-05-03T00:00:00.000Z", alreadyImported: false, }, ]); }), ); it.effect("returns an empty result when neither home directory exists", () => Effect.gen(function* () { const path = yield* Path.Path; const root = yield* makeTempDir("t3code-missing-homes-"); const result = yield* runScan({ claudeHomePath: path.join(root, "no-claude"), codexHomePath: path.join(root, "no-codex"), }); expect(result.candidates).toEqual([]); expect(result.scannedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); }), ); });});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import { type OrchestrationProject, ProjectId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as ServerConfig from "../config.ts";
import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as AgentSessionScanner from "./AgentSessionScanner.ts";
const makeProject = (workspaceRoot: string): OrchestrationProject => ({
id: ProjectId.make("project-1"),
title: "Imported",
workspaceRoot,
defaultModelSelection: null,
scripts: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
deletedAt: null,
});
/** Only `getActiveProjectByWorkspaceRoot` is exercised; the rest must not be called. */
const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray<string>) =>
Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
getCommandReadModel: () => Effect.die("unused"),
getSnapshot: () => Effect.die("unused"),
getShellSnapshot: () => Effect.die("unused"),
getArchivedShellSnapshot: () => Effect.die("unused"),
getSnapshotSequence: () => Effect.die("unused"),
getCounts: () => Effect.die("unused"),
getActiveProjectByWorkspaceRoot: (workspaceRoot) =>
Effect.succeed(
importedWorkspaceRoots.includes(workspaceRoot)
? Option.some(makeProject(workspaceRoot))
: Option.none(),
),
getProjectShellById: () => Effect.die("unused"),
getFirstActiveThreadIdByProjectId: () => Effect.die("unused"),
getThreadCheckpointContext: () => Effect.die("unused"),
getFullThreadDiffContext: () => Effect.die("unused"),
getThreadShellById: () => Effect.die("unused"),
getThreadDetailById: () => Effect.die("unused"),
getThreadDetailSnapshot: () => Effect.die("unused"),
searchThreads: () => Effect.die("unused"),
});
/**
* Run a scan against the given homes. Homes are temp dirs created inside the
* test, so the layer is built per run rather than shared.
*/
const runScan = (input: {
readonly claudeHomePath: string;
readonly codexHomePath: string;
readonly importedWorkspaceRoots?: ReadonlyArray<string>;
/** Base dir for the test ServerConfig; worktreesDir derives from it. */
readonly configBaseDir?: string;
}) =>
Effect.gen(function* () {
const scanner = yield* AgentSessionScanner.AgentSessionScanner;
return yield* scanner.scan;
}).pipe(
Effect.provide(
AgentSessionScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(
ServerSettings.layerTest({
providers: {
claudeAgent: { homePath: input.claudeHomePath },
codex: { homePath: input.codexHomePath },
},
}),
ServerConfig.layerTest(
input.claudeHomePath,
input.configBaseDir ?? { prefix: "t3code-scanner-config-" },
),
makeProjectionSnapshotQueryLayer(input.importedWorkspaceRoots ?? []),
),
),
),
),
);
const makeTempDir = Effect.fn("AgentSessionScanner.test.makeTempDir")(function* (prefix: string) {
const fileSystem = yield* FileSystem.FileSystem;
return yield* fileSystem.makeTempDirectoryScoped({ prefix });
});
const writeTranscript = Effect.fn("AgentSessionScanner.test.writeTranscript")(function* (input: {
readonly filePath: string;
readonly contents: string;
/** Epoch millis, so ordering assertions never depend on write timing. */
readonly mtimeMs: number;
}) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* fileSystem.makeDirectory(path.dirname(input.filePath), { recursive: true });
yield* fileSystem.writeFileString(input.filePath, input.contents);
// Numeric utimes arguments are seconds, not milliseconds.
const seconds = input.mtimeMs / 1000;
yield* fileSystem.utimes(input.filePath, seconds, seconds);
});
/** Claude session line: the first record carries the real `cwd`. */
const claudeSessionLine = (cwd: string) =>
`${JSON.stringify({ type: "user", cwd, sessionId: "s1" })}\n${JSON.stringify({ type: "assistant" })}\n`;
/** Codex rollout line: session metadata is nested under `payload`. */
const codexRolloutLine = (cwd: string) =>
`${JSON.stringify({ timestamp: "2026-01-01T00:00:00.000Z", type: "session_meta", payload: { id: "r1", cwd } })}\n`;
it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
describe("scan", () => {
it.effect("reads Claude project cwds from transcripts, newest first", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const olderWorkspace = yield* makeTempDir("t3code-workspace-older-");
const newerWorkspace = yield* makeTempDir("t3code-workspace-newer-");
// Slugs are intentionally lossy; the scanner must not decode them.
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug-older", "a.jsonl"),
contents: claudeSessionLine(olderWorkspace),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug-older", "b.jsonl"),
contents: claudeSessionLine(olderWorkspace),
mtimeMs: Date.parse("2026-01-02T00:00:00.000Z"),
});
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug-newer", "c.jsonl"),
contents: claudeSessionLine(newerWorkspace),
mtimeMs: Date.parse("2026-03-01T00:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath });
expect(result.candidates).toEqual([
{
path: newerWorkspace,
title: path.basename(newerWorkspace),
sources: ["claudeAgent"],
threadCount: 1,
lastActiveAt: "2026-03-01T00:00:00.000Z",
alreadyImported: false,
},
{
path: olderWorkspace,
title: path.basename(olderWorkspace),
sources: ["claudeAgent"],
threadCount: 2,
lastActiveAt: "2026-01-02T00:00:00.000Z",
alreadyImported: false,
},
]);
}),
);
it.effect("groups Codex rollouts by cwd across date directories", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const workspace = yield* makeTempDir("t3code-workspace-");
const otherWorkspace = yield* makeTempDir("t3code-workspace-other-");
const rollout = (year: string, month: string, day: string, name: string) =>
path.join(codexHomePath, "sessions", year, month, day, name);
yield* writeTranscript({
filePath: rollout("2026", "01", "05", "rollout-2026-01-05T10-00-00-aaa.jsonl"),
contents: codexRolloutLine(workspace),
mtimeMs: Date.parse("2026-01-05T10:00:00.000Z"),
});
yield* writeTranscript({
filePath: rollout("2026", "02", "09", "rollout-2026-02-09T10-00-00-bbb.jsonl"),
contents: codexRolloutLine(workspace),
mtimeMs: Date.parse("2026-02-09T10:00:00.000Z"),
});
yield* writeTranscript({
filePath: rollout("2026", "02", "09", "rollout-2026-02-09T11-00-00-ccc.jsonl"),
contents: codexRolloutLine(otherWorkspace),
mtimeMs: Date.parse("2026-02-09T11:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath });
expect(result.candidates).toEqual([
{
path: otherWorkspace,
title: path.basename(otherWorkspace),
sources: ["codex"],
threadCount: 1,
lastActiveAt: "2026-02-09T11:00:00.000Z",
alreadyImported: false,
},
{
path: workspace,
title: path.basename(workspace),
sources: ["codex"],
threadCount: 2,
lastActiveAt: "2026-02-09T10:00:00.000Z",
alreadyImported: false,
},
]);
}),
);
it.effect("merges the same cwd seen by both agents and flags imported projects", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const workspace = yield* makeTempDir("t3code-workspace-");
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"),
contents: claudeSessionLine(workspace),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
yield* writeTranscript({
filePath: path.join(
codexHomePath,
"sessions",
"2026",
"04",
"01",
"rollout-2026-04-01T09-00-00-aaa.jsonl",
),
contents: codexRolloutLine(workspace),
mtimeMs: Date.parse("2026-04-01T09:00:00.000Z"),
});
const result = yield* runScan({
claudeHomePath,
codexHomePath,
importedWorkspaceRoots: [workspace],
});
expect(result.candidates).toEqual([
{
path: workspace,
title: path.basename(workspace),
sources: ["claudeAgent", "codex"],
threadCount: 2,
lastActiveAt: "2026-04-01T09:00:00.000Z",
alreadyImported: true,
},
]);
}),
);
it.effect("drops candidates whose directory no longer exists", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"),
contents: claudeSessionLine(path.join(claudeHomePath, "does-not-exist")),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath });
expect(result.candidates).toEqual([]);
}),
);
it.effect("excludes T3-managed worktree sandboxes", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const fileSystem = yield* FileSystem.FileSystem;
const worktreeCwd = path.join(claudeHomePath, ".t3", "worktrees", "t3code", "wt-1");
yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true });
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"),
contents: claudeSessionLine(worktreeCwd),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath });
expect(result.candidates).toEqual([]);
}),
);
it.effect("excludes sandboxes under the configured worktrees dir without .t3 in the path", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const configBaseDir = yield* makeTempDir("t3code-scanner-base-");
const fileSystem = yield* FileSystem.FileSystem;
// worktreesDir derives as `<baseDir>/worktrees`, and the temp base
// dir contains no `.t3` segment — only the config-based prefix match
// can exclude this one.
const worktreeCwd = path.join(configBaseDir, "worktrees", "t3code", "wt-2");
yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true });
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"),
contents: claudeSessionLine(worktreeCwd),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath, configBaseDir });
expect(result.candidates).toEqual([]);
}),
);
it.effect("excludes sandboxes reached through a symlink into the worktrees dir", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const configBaseDir = yield* makeTempDir("t3code-scanner-base-");
const linkParent = yield* makeTempDir("t3code-scanner-links-");
const fileSystem = yield* FileSystem.FileSystem;
// The recorded cwd is a symlink whose own spelling looks harmless;
// only its realpath reveals the managed sandbox.
const worktreeCwd = path.join(configBaseDir, "worktrees", "t3code", "wt-3");
yield* fileSystem.makeDirectory(worktreeCwd, { recursive: true });
const symlinkCwd = path.join(linkParent, "innocent-project");
yield* fileSystem.symlink(worktreeCwd, symlinkCwd);
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"),
contents: claudeSessionLine(symlinkCwd),
mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath, configBaseDir });
expect(result.candidates).toEqual([]);
}),
);
it.effect("skips malformed transcripts without failing the scan", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-home-");
const codexHomePath = yield* makeTempDir("t3code-codex-home-");
const workspace = yield* makeTempDir("t3code-workspace-");
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-broken", "a.jsonl"),
contents: "not json at all\n",
mtimeMs: Date.parse("2026-05-01T00:00:00.000Z"),
});
// Valid JSON, but no cwd anywhere in the record.
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-no-cwd", "a.jsonl"),
contents: `{"type":"summary"}\n`,
mtimeMs: Date.parse("2026-05-02T00:00:00.000Z"),
});
yield* writeTranscript({
filePath: path.join(claudeHomePath, "projects", "-good", "a.jsonl"),
contents: claudeSessionLine(workspace),
mtimeMs: Date.parse("2026-05-03T00:00:00.000Z"),
});
const result = yield* runScan({ claudeHomePath, codexHomePath });
expect(result.candidates).toEqual([
{
path: workspace,
title: path.basename(workspace),
sources: ["claudeAgent"],
threadCount: 1,
lastActiveAt: "2026-05-03T00:00:00.000Z",
alreadyImported: false,
},
]);
}),
);
it.effect("returns an empty result when neither home directory exists", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const root = yield* makeTempDir("t3code-missing-homes-");
const result = yield* runScan({
claudeHomePath: path.join(root, "no-claude"),
codexHomePath: path.join(root, "no-codex"),
});
expect(result.candidates).toEqual([]);
expect(result.scannedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
}),
);
});
});