404 lines · 15.9 KB · TypeScript
RawDownload
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/);      }),    );  });});