Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
200 lines
9.0 KiB
TypeScript
200 lines
9.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { BACKGROUND_CONTEXT, type Context } from "../../src/harness/context.ts";
|
|
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
|
|
import { JSONL_STORAGE_VERSION, JsonlSessionRepo } from "../../src/harness/session/jsonl/index.ts";
|
|
import { sessionName, setValue } from "../../src/harness/session/values.ts";
|
|
import { getOrThrow } from "../../src/harness/types.ts";
|
|
import { createTempDir } from "./session-test-utils.ts";
|
|
|
|
const NOW = 1_700_000_000_000;
|
|
|
|
class AtomicPublicationNodeExecutionEnv extends NodeExecutionEnv {
|
|
publication:
|
|
| { sourcePath: string; destinationPath: string; destinationExisted: boolean; stagedContent: string }
|
|
| undefined;
|
|
|
|
override async renameFile(sourcePath: string, destinationPath: string, context: Context) {
|
|
const destinationExists = await super.exists(destinationPath, context);
|
|
const stagedContent = await super.readTextFile(sourcePath, context);
|
|
if (destinationExists.ok && stagedContent.ok) {
|
|
this.publication = {
|
|
sourcePath,
|
|
destinationPath,
|
|
destinationExisted: destinationExists.value,
|
|
stagedContent: stagedContent.value,
|
|
};
|
|
}
|
|
return super.renameFile(sourcePath, destinationPath, context);
|
|
}
|
|
}
|
|
|
|
describe("JsonlSessionRepo cwd-scoped lifecycle", () => {
|
|
it("persists metadata and filters discovery by cwd", async () => {
|
|
const fileSystem = new NodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
const session = await repo.create(
|
|
{ id: "child", cwd: "/workspace", parentSessionId: "parent" },
|
|
BACKGROUND_CONTEXT,
|
|
);
|
|
const metadata = session.metadata;
|
|
|
|
expect(metadata).toMatchObject({
|
|
id: "child",
|
|
createdAt: NOW,
|
|
storageVersion: JSONL_STORAGE_VERSION,
|
|
cwd: "/workspace",
|
|
parentSessionId: "parent",
|
|
});
|
|
expect(metadata.path).toContain("/sessions/--workspace--/");
|
|
expect(metadata.path.endsWith("_child.jsonl")).toBe(true);
|
|
expect(Number.isFinite(metadata.modifiedAt)).toBe(true);
|
|
await session.close(BACKGROUND_CONTEXT);
|
|
|
|
expect(await repo.list({ cwd: "/other" }, BACKGROUND_CONTEXT)).toEqual([]);
|
|
expect(await repo.list({ cwd: "/workspace" }, BACKGROUND_CONTEXT)).toEqual([metadata]);
|
|
const firstLine = getOrThrow(
|
|
await fileSystem.readTextLines(metadata.path, { maxLines: 1 }, BACKGROUND_CONTEXT),
|
|
)[0];
|
|
expect(JSON.parse(firstLine!)).toEqual({
|
|
v: 4,
|
|
kind: "header",
|
|
id: "child",
|
|
storageVersion: JSONL_STORAGE_VERSION,
|
|
createdAt: NOW,
|
|
cwd: "/workspace",
|
|
parentSessionId: "parent",
|
|
});
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
it("atomically publishes a branchless session header", async () => {
|
|
const fileSystem = new AtomicPublicationNodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
const session = await repo.create({ id: "session", cwd: "/workspace" }, BACKGROUND_CONTEXT);
|
|
|
|
const publication = fileSystem.publication;
|
|
if (publication === undefined) throw new Error("Expected atomic session publication");
|
|
expect(publication.destinationPath).toBe(session.metadata.path);
|
|
expect(publication.destinationExisted).toBe(false);
|
|
const lines = publication.stagedContent.trimEnd().split("\n");
|
|
expect(lines).toHaveLength(1);
|
|
expect(JSON.parse(lines[0]!)).toMatchObject({ kind: "header", id: "session" });
|
|
expect(getOrThrow(await fileSystem.readTextFile(session.metadata.path, BACKGROUND_CONTEXT))).toBe(
|
|
publication.stagedContent,
|
|
);
|
|
expect(getOrThrow(await fileSystem.exists(publication.sourcePath, BACKGROUND_CONTEXT))).toBe(false);
|
|
|
|
await session.close(BACKGROUND_CONTEXT);
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
it("keeps an explicit Session mutation through commit until end", async () => {
|
|
const fileSystem = new NodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
const session = await repo.create({ id: "session", cwd: "/workspace" }, BACKGROUND_CONTEXT);
|
|
const mutation = await session.beginMutation(BACKGROUND_CONTEXT);
|
|
let queuedStarted = false;
|
|
const queued = session.mutate(() => {
|
|
queuedStarted = true;
|
|
}, BACKGROUND_CONTEXT);
|
|
|
|
const result = await mutation.commit([setValue(sessionName, "explicit")], BACKGROUND_CONTEXT);
|
|
expect(result.seqs).toHaveLength(1);
|
|
expect(queuedStarted).toBe(false);
|
|
expect(await mutation.getValue(sessionName, BACKGROUND_CONTEXT)).toMatchObject({ value: "explicit" });
|
|
await mutation.end(BACKGROUND_CONTEXT);
|
|
await queued;
|
|
expect(queuedStarted).toBe(true);
|
|
|
|
await session.close(BACKGROUND_CONTEXT);
|
|
const reopened = await repo.open(session.metadata, BACKGROUND_CONTEXT);
|
|
expect(await reopened.getName(BACKGROUND_CONTEXT)).toBe("explicit");
|
|
await reopened.close(BACKGROUND_CONTEXT);
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
it("rejects unsupported storage versions without repairing a torn tail", async () => {
|
|
const fileSystem = new NodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
const session = await repo.create({ id: "future", cwd: "/workspace" }, BACKGROUND_CONTEXT);
|
|
const metadata = session.metadata;
|
|
await session.close(BACKGROUND_CONTEXT);
|
|
|
|
const lines = getOrThrow(await fileSystem.readTextFile(metadata.path, BACKGROUND_CONTEXT))
|
|
.trimEnd()
|
|
.split("\n");
|
|
const unsupportedVersion = JSONL_STORAGE_VERSION + 1;
|
|
lines[0] = JSON.stringify({ ...JSON.parse(lines[0]!), storageVersion: unsupportedVersion });
|
|
const unsupportedContent = `${lines.join("\n")}\n{"kind":"entry"`;
|
|
getOrThrow(await fileSystem.writeFile(metadata.path, unsupportedContent, BACKGROUND_CONTEXT));
|
|
|
|
await expect(repo.open(metadata, BACKGROUND_CONTEXT)).rejects.toThrow(
|
|
`unsupported storage version ${unsupportedVersion}`,
|
|
);
|
|
expect(getOrThrow(await fileSystem.readTextFile(metadata.path, BACKGROUND_CONTEXT))).toBe(unsupportedContent);
|
|
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
it("keeps fork destinations claimed until close and rejects deleting open sessions", async () => {
|
|
const fileSystem = new NodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
const source = await repo.create({ id: "source", cwd: "/workspace" }, BACKGROUND_CONTEXT);
|
|
const fork = await repo.fork(source.metadata, { id: "fork", scope: "tree" }, BACKGROUND_CONTEXT);
|
|
|
|
await expect(repo.open(fork.metadata, BACKGROUND_CONTEXT)).rejects.toThrow("already open");
|
|
await expect(repo.delete(fork.metadata, BACKGROUND_CONTEXT)).rejects.toThrow("open");
|
|
await fork.close(BACKGROUND_CONTEXT);
|
|
|
|
const reopened = await repo.open(fork.metadata, BACKGROUND_CONTEXT);
|
|
await reopened.close(BACKGROUND_CONTEXT);
|
|
await repo.delete(fork.metadata, BACKGROUND_CONTEXT);
|
|
await expect(repo.open(fork.metadata, BACKGROUND_CONTEXT)).rejects.toThrow("does not exist");
|
|
|
|
await source.close(BACKGROUND_CONTEXT);
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
it("rejects concurrent creates for the same working-directory id", async () => {
|
|
const fileSystem = new NodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
|
|
const results = await Promise.allSettled([
|
|
repo.create({ id: "session", cwd: "/workspace" }, BACKGROUND_CONTEXT),
|
|
repo.create({ id: "session", cwd: "/workspace" }, BACKGROUND_CONTEXT),
|
|
]);
|
|
|
|
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
|
|
expect(results.filter((result) => result.status === "rejected")).toHaveLength(1);
|
|
expect(await repo.list({ cwd: "/workspace" }, BACKGROUND_CONTEXT)).toHaveLength(1);
|
|
for (const result of results) {
|
|
if (result.status === "fulfilled") await result.value.close(BACKGROUND_CONTEXT);
|
|
}
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
|
|
it("allows the same id to be active in different working directories", async () => {
|
|
const fileSystem = new NodeExecutionEnv({ cwd: createTempDir() });
|
|
const repo = new JsonlSessionRepo({ fileSystem, sessionsRoot: "sessions", now: () => NOW });
|
|
const first = await repo.create({ id: "shared", cwd: "/workspace-a" }, BACKGROUND_CONTEXT);
|
|
const second = await repo.create({ id: "shared", cwd: "/workspace-b" }, BACKGROUND_CONTEXT);
|
|
|
|
expect(first.metadata.path).not.toBe(second.metadata.path);
|
|
await expect(repo.create({ id: "shared", cwd: "/workspace-a" }, BACKGROUND_CONTEXT)).rejects.toThrow(
|
|
"already exists",
|
|
);
|
|
expect((await repo.list(undefined, BACKGROUND_CONTEXT)).map(({ cwd, id }) => ({ cwd, id }))).toEqual([
|
|
{ cwd: "/workspace-a", id: "shared" },
|
|
{ cwd: "/workspace-b", id: "shared" },
|
|
]);
|
|
|
|
await Promise.all([first.close(BACKGROUND_CONTEXT), second.close(BACKGROUND_CONTEXT)]);
|
|
const reopened = await Promise.all([
|
|
repo.open(first.metadata, BACKGROUND_CONTEXT),
|
|
repo.open(second.metadata, BACKGROUND_CONTEXT),
|
|
]);
|
|
await Promise.all(reopened.map((session) => session.close(BACKGROUND_CONTEXT)));
|
|
await repo.close(BACKGROUND_CONTEXT);
|
|
});
|
|
});
|