import * as NodeServices from "@effect/platform-node/NodeServices";import * as NodeOS from "node:os";import { describe, expect, it } from "@effect/vitest";import { type OrchestrationProjectShell, ProjectId, ProviderDriverKind, ProviderInstanceId, type ServerSettings as ContractServerSettings,} from "@t3tools/contracts";import { symlinksSupported } from "@t3tools/shared/testing/symlinks";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 Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock"; 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 makeProjectShell = (workspaceRoot: string): OrchestrationProjectShell => ({ 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",}); /** Only `getShellSnapshot` is exercised; the rest must not be called. */const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray<string>) => Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getUserInputActivity: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.succeed({ snapshotSequence: 0, projects: importedWorkspaceRoots.map((workspaceRoot) => makeProjectShell(workspaceRoot)), threads: [], updatedAt: "2026-01-01T00:00:00.000Z", }), getArchivedShellSnapshot: () => Effect.die("unused"), getSnapshotSequence: () => Effect.die("unused"), getCounts: () => Effect.die("unused"), getEventReplayStats: () => Effect.die("unused"), getActiveProjectByWorkspaceRoot: () => Effect.die("unused"), getProjectShellById: () => Effect.die("unused"), getImportedAgentSessionSources: () => Effect.succeed([]), getFirstActiveThreadIdByProjectId: () => Effect.die("unused"), getThreadCheckpointContext: () => Effect.die("unused"), getFullThreadDiffContext: () => Effect.die("unused"), getThreadShellById: () => Effect.die("unused"), getThreadRuntimeContext: () => Effect.die("unused"), getTurnStartMessage: () => 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. */interface ScannerTestInput { readonly claudeHomePath: string; readonly codexHomePath: string; readonly importedWorkspaceRoots?: ReadonlyArray<string>; /** Base dir for the test ServerConfig; worktreesDir derives from it. */ readonly configBaseDir?: string; readonly providerInstances?: ContractServerSettings["providerInstances"];} const makeScannerTestLayer = (input: ScannerTestInput) => AgentSessionScanner.layer.pipe( Layer.provide( Layer.mergeAll( ServerSettings.layerTest({ providers: { claudeAgent: { homePath: input.claudeHomePath }, codex: { homePath: input.codexHomePath }, }, ...(input.providerInstances === undefined ? {} : { providerInstances: input.providerInstances }), }), ServerConfig.layerTest( input.claudeHomePath, input.configBaseDir ?? { prefix: "t3code-scanner-config-" }, ), makeProjectionSnapshotQueryLayer(input.importedWorkspaceRoots ?? []), ), ), ); const runScan = (input: ScannerTestInput) => Effect.gen(function* () { const scanner = yield* AgentSessionScanner.AgentSessionScanner; return yield* scanner.scan; }).pipe(Effect.provide(makeScannerTestLayer(input))); const runRecentThreadOutcomes = (input: ScannerTestInput & { readonly workspaceRoot: string }) => Effect.gen(function* () { const scanner = yield* AgentSessionScanner.AgentSessionScanner; return yield* scanner.recentThreads(input.workspaceRoot).pipe( Stream.runCollect, Effect.map((outcomes) => Array.from(outcomes)), ); }).pipe(Effect.provide(makeScannerTestLayer(input))); const runRecentThreads = (input: ScannerTestInput & { readonly workspaceRoot: string }) => runRecentThreadOutcomes(input).pipe( Effect.map((outcomes) => outcomes.flatMap((outcome) => (outcome._tag === "Importable" ? [outcome.thread] : [])), ), ); 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`; const encodeTranscriptRecord = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); function makeRecordLimitTranscript(cwd: string, overflow: boolean): string { const records = [ encodeTranscriptRecord({ type: "session_meta", payload: { id: "record-limit-session", cwd }, }), encodeTranscriptRecord({ type: "event_msg", payload: { type: "user_message", message: "First prompt" }, }), ].join("\n") + "\n" + "{}\n".repeat(99_998); return overflow ? records + "\n" + encodeTranscriptRecord({ type: "event_msg", payload: { type: "user_message", message: "Overflow prompt" }, }) + "\n" : records;} 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, git: null, }, { path: olderWorkspace, title: path.basename(olderWorkspace), sources: ["claudeAgent"], threadCount: 2, lastActiveAt: "2026-01-02T00:00:00.000Z", alreadyImported: false, git: null, }, ]); }), ); 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, git: null, }, { path: workspace, title: path.basename(workspace), sources: ["codex"], threadCount: 2, lastActiveAt: "2026-02-09T10:00:00.000Z", alreadyImported: false, git: null, }, ]); }), ); it.effect.each(["claudeAgent", "codex"] as const)( "does not open a non-file %s transcript", (source) => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const transcriptPath = source === "claudeAgent" ? path.join(claudeHomePath, "projects", "-slug", "session.jsonl") : path.join(codexHomePath, "sessions", "2026", "08", "24", "rollout-session.jsonl"); yield* fileSystem.makeDirectory(transcriptPath, { recursive: true }); let transcriptOpenCount = 0; const simulatedFileSystem = FileSystem.FileSystem.of({ ...fileSystem, open: (filePath, options) => { if (filePath === transcriptPath) transcriptOpenCount += 1; return fileSystem.open(filePath, options); }, }); const result = yield* runScan({ claudeHomePath, codexHomePath }).pipe( Effect.provideService(FileSystem.FileSystem, simulatedFileSystem), ); expect(result.candidates).toEqual([]); expect(transcriptOpenCount).toBe(0); }), ); it.effect.each(["claudeAgent", "codex"] as const)( "stops %s directory reads at the discovery operation budget", (source) => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const discoveryRoot = source === "claudeAgent" ? path.join(claudeHomePath, "projects") : path.join(codexHomePath, "sessions"); const emptyDirectories = Array.from( { length: 20_001 }, (_, index) => `empty-${index.toString().padStart(5, "0")}`, ); let directoryReadCount = 0; const simulatedFileSystem = FileSystem.FileSystem.of({ ...fileSystem, readDirectory: (directory, options) => { if (directory === discoveryRoot) { directoryReadCount += 1; return Effect.succeed(emptyDirectories); } if (path.dirname(directory) === discoveryRoot) { directoryReadCount += 1; return Effect.succeed([]); } return fileSystem.readDirectory(directory, options); }, }); const result = yield* runScan({ claudeHomePath, codexHomePath }).pipe( Effect.provideService(FileSystem.FileSystem, simulatedFileSystem), ); expect(result.candidates).toEqual([]); expect(directoryReadCount).toBe(20_000); }), ); 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), projectId: ProjectId.make("project-1"), sources: ["claudeAgent", "codex"], threadCount: 2, lastActiveAt: "2026-04-01T09:00:00.000Z", alreadyImported: true, git: null, }, ]); }), ); it.effect("returns the imported project ID through a realpath alias", () => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const linkParent = yield* makeTempDir("t3code-scanner-links-"); const workspaceAlias = path.join(linkParent, "workspace-alias"); yield* fileSystem.symlink(workspace, workspaceAlias); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspaceAlias), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspace], }); expect(result.candidates[0]).toMatchObject({ path: workspace, projectId: ProjectId.make("project-1"), alreadyImported: true, git: null, }); }), ); it.effect("matches a persisted project alias to a transcript realpath", () => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const linkParent = yield* makeTempDir("t3code-scanner-links-"); const workspaceAlias = path.join(linkParent, "workspace-alias"); yield* fileSystem.symlink(workspace, workspaceAlias); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspace), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspaceAlias], }); expect(result.candidates[0]).toMatchObject({ path: workspaceAlias, projectId: ProjectId.make("project-1"), alreadyImported: true, git: null, }); }), ); it.effect("merges case aliases and preserves the persisted project path", () => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const workspaceAlias = path.join( path.dirname(workspace), path.basename(workspace).toUpperCase(), ); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspaceAlias), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); yield* writeTranscript({ filePath: path.join(codexHomePath, "sessions", "2026", "01", "02", "rollout-b.jsonl"), contents: codexRolloutLine(workspace), mtimeMs: Date.parse("2026-01-02T00:00:00.000Z"), }); const simulatedFileSystem = FileSystem.FileSystem.of({ ...fileSystem, stat: (filePath) => fileSystem.stat(filePath === workspaceAlias ? workspace : filePath), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspace], }).pipe(Effect.provideService(FileSystem.FileSystem, simulatedFileSystem)); expect(result.candidates).toEqual([ {Lines 1–500 of 3216. Load more
import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NodeOS from "node:os"; import { describe, expect, it } from "@effect/vitest"; import { type OrchestrationProjectShell, ProjectId, ProviderDriverKind, ProviderInstanceId, type ServerSettings as ContractServerSettings, } from "@t3tools/contracts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; 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 Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; 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 makeProjectShell = (workspaceRoot: string): OrchestrationProjectShell => ({ 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", }); /** Only `getShellSnapshot` is exercised; the rest must not be called. */ const makeProjectionSnapshotQueryLayer = (importedWorkspaceRoots: ReadonlyArray<string>) => Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { getCommandReadModel: () => Effect.die("unused"), getUserInputActivity: () => Effect.die("unused"), getSnapshot: () => Effect.die("unused"), getShellSnapshot: () => Effect.succeed({ snapshotSequence: 0, projects: importedWorkspaceRoots.map((workspaceRoot) => makeProjectShell(workspaceRoot)), threads: [], updatedAt: "2026-01-01T00:00:00.000Z", }), getArchivedShellSnapshot: () => Effect.die("unused"), getSnapshotSequence: () => Effect.die("unused"), getCounts: () => Effect.die("unused"), getEventReplayStats: () => Effect.die("unused"), getActiveProjectByWorkspaceRoot: () => Effect.die("unused"), getProjectShellById: () => Effect.die("unused"), getImportedAgentSessionSources: () => Effect.succeed([]), getFirstActiveThreadIdByProjectId: () => Effect.die("unused"), getThreadCheckpointContext: () => Effect.die("unused"), getFullThreadDiffContext: () => Effect.die("unused"), getThreadShellById: () => Effect.die("unused"), getThreadRuntimeContext: () => Effect.die("unused"), getTurnStartMessage: () => 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. */ interface ScannerTestInput { readonly claudeHomePath: string; readonly codexHomePath: string; readonly importedWorkspaceRoots?: ReadonlyArray<string>; /** Base dir for the test ServerConfig; worktreesDir derives from it. */ readonly configBaseDir?: string; readonly providerInstances?: ContractServerSettings["providerInstances"]; } const makeScannerTestLayer = (input: ScannerTestInput) => AgentSessionScanner.layer.pipe( Layer.provide( Layer.mergeAll( ServerSettings.layerTest({ providers: { claudeAgent: { homePath: input.claudeHomePath }, codex: { homePath: input.codexHomePath }, }, ...(input.providerInstances === undefined ? {} : { providerInstances: input.providerInstances }), }), ServerConfig.layerTest( input.claudeHomePath, input.configBaseDir ?? { prefix: "t3code-scanner-config-" }, ), makeProjectionSnapshotQueryLayer(input.importedWorkspaceRoots ?? []), ), ), ); const runScan = (input: ScannerTestInput) => Effect.gen(function* () { const scanner = yield* AgentSessionScanner.AgentSessionScanner; return yield* scanner.scan; }).pipe(Effect.provide(makeScannerTestLayer(input))); const runRecentThreadOutcomes = (input: ScannerTestInput & { readonly workspaceRoot: string }) => Effect.gen(function* () { const scanner = yield* AgentSessionScanner.AgentSessionScanner; return yield* scanner.recentThreads(input.workspaceRoot).pipe( Stream.runCollect, Effect.map((outcomes) => Array.from(outcomes)), ); }).pipe(Effect.provide(makeScannerTestLayer(input))); const runRecentThreads = (input: ScannerTestInput & { readonly workspaceRoot: string }) => runRecentThreadOutcomes(input).pipe( Effect.map((outcomes) => outcomes.flatMap((outcome) => (outcome._tag === "Importable" ? [outcome.thread] : [])), ), ); 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`; const encodeTranscriptRecord = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); function makeRecordLimitTranscript(cwd: string, overflow: boolean): string { const records = [ encodeTranscriptRecord({ type: "session_meta", payload: { id: "record-limit-session", cwd }, }), encodeTranscriptRecord({ type: "event_msg", payload: { type: "user_message", message: "First prompt" }, }), ].join("\n") + "\n" + "{}\n".repeat(99_998); return overflow ? records + "\n" + encodeTranscriptRecord({ type: "event_msg", payload: { type: "user_message", message: "Overflow prompt" }, }) + "\n" : records; } 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, git: null, }, { path: olderWorkspace, title: path.basename(olderWorkspace), sources: ["claudeAgent"], threadCount: 2, lastActiveAt: "2026-01-02T00:00:00.000Z", alreadyImported: false, git: null, }, ]); }), ); 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, git: null, }, { path: workspace, title: path.basename(workspace), sources: ["codex"], threadCount: 2, lastActiveAt: "2026-02-09T10:00:00.000Z", alreadyImported: false, git: null, }, ]); }), ); it.effect.each(["claudeAgent", "codex"] as const)( "does not open a non-file %s transcript", (source) => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const transcriptPath = source === "claudeAgent" ? path.join(claudeHomePath, "projects", "-slug", "session.jsonl") : path.join(codexHomePath, "sessions", "2026", "08", "24", "rollout-session.jsonl"); yield* fileSystem.makeDirectory(transcriptPath, { recursive: true }); let transcriptOpenCount = 0; const simulatedFileSystem = FileSystem.FileSystem.of({ ...fileSystem, open: (filePath, options) => { if (filePath === transcriptPath) transcriptOpenCount += 1; return fileSystem.open(filePath, options); }, }); const result = yield* runScan({ claudeHomePath, codexHomePath }).pipe( Effect.provideService(FileSystem.FileSystem, simulatedFileSystem), ); expect(result.candidates).toEqual([]); expect(transcriptOpenCount).toBe(0); }), ); it.effect.each(["claudeAgent", "codex"] as const)( "stops %s directory reads at the discovery operation budget", (source) => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const discoveryRoot = source === "claudeAgent" ? path.join(claudeHomePath, "projects") : path.join(codexHomePath, "sessions"); const emptyDirectories = Array.from( { length: 20_001 }, (_, index) => `empty-${index.toString().padStart(5, "0")}`, ); let directoryReadCount = 0; const simulatedFileSystem = FileSystem.FileSystem.of({ ...fileSystem, readDirectory: (directory, options) => { if (directory === discoveryRoot) { directoryReadCount += 1; return Effect.succeed(emptyDirectories); } if (path.dirname(directory) === discoveryRoot) { directoryReadCount += 1; return Effect.succeed([]); } return fileSystem.readDirectory(directory, options); }, }); const result = yield* runScan({ claudeHomePath, codexHomePath }).pipe( Effect.provideService(FileSystem.FileSystem, simulatedFileSystem), ); expect(result.candidates).toEqual([]); expect(directoryReadCount).toBe(20_000); }), ); 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), projectId: ProjectId.make("project-1"), sources: ["claudeAgent", "codex"], threadCount: 2, lastActiveAt: "2026-04-01T09:00:00.000Z", alreadyImported: true, git: null, }, ]); }), ); it.effect("returns the imported project ID through a realpath alias", () => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const linkParent = yield* makeTempDir("t3code-scanner-links-"); const workspaceAlias = path.join(linkParent, "workspace-alias"); yield* fileSystem.symlink(workspace, workspaceAlias); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspaceAlias), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspace], }); expect(result.candidates[0]).toMatchObject({ path: workspace, projectId: ProjectId.make("project-1"), alreadyImported: true, git: null, }); }), ); it.effect("matches a persisted project alias to a transcript realpath", () => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const linkParent = yield* makeTempDir("t3code-scanner-links-"); const workspaceAlias = path.join(linkParent, "workspace-alias"); yield* fileSystem.symlink(workspace, workspaceAlias); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspace), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspaceAlias], }); expect(result.candidates[0]).toMatchObject({ path: workspaceAlias, projectId: ProjectId.make("project-1"), alreadyImported: true, git: null, }); }), ); it.effect("merges case aliases and preserves the persisted project path", () => Effect.gen(function* () { const path = yield* Path.Path; const fileSystem = yield* FileSystem.FileSystem; const claudeHomePath = yield* makeTempDir("t3code-claude-home-"); const codexHomePath = yield* makeTempDir("t3code-codex-home-"); const workspace = yield* makeTempDir("t3code-workspace-"); const workspaceAlias = path.join( path.dirname(workspace), path.basename(workspace).toUpperCase(), ); yield* writeTranscript({ filePath: path.join(claudeHomePath, "projects", "-slug", "a.jsonl"), contents: claudeSessionLine(workspaceAlias), mtimeMs: Date.parse("2026-01-01T00:00:00.000Z"), }); yield* writeTranscript({ filePath: path.join(codexHomePath, "sessions", "2026", "01", "02", "rollout-b.jsonl"), contents: codexRolloutLine(workspace), mtimeMs: Date.parse("2026-01-02T00:00:00.000Z"), }); const simulatedFileSystem = FileSystem.FileSystem.of({ ...fileSystem, stat: (filePath) => fileSystem.stat(filePath === workspaceAlias ? workspace : filePath), }); const result = yield* runScan({ claudeHomePath, codexHomePath, importedWorkspaceRoots: [workspace], }).pipe(Effect.provideService(FileSystem.FileSystem, simulatedFileSystem)); expect(result.candidates).toEqual([ {