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
112 changes: 111 additions & 1 deletion apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from "node:path";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
import { Deferred, Effect, Fiber, FileSystem, Layer, Option, PlatformError, Scope } from "effect";
import { describe, expect } from "vitest";

import { checkpointBaselineRefForThreadTurn, checkpointRefForThreadTurn } from "../Utils.ts";
Expand All @@ -17,6 +17,7 @@ import {
import { GitCommandError } from "@t3tools/contracts";
import { ServerConfig } from "../../config.ts";
import { ThreadId } from "@t3tools/contracts";
import { CheckoutCoordinator } from "../../git/CheckoutCoordinator.ts";

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-checkpoint-store-test-",
Expand Down Expand Up @@ -117,6 +118,115 @@ function replaceLine(contents: string, lineIndex: number, replacement: string):
}

describe("CheckpointStoreLive range resolution", () => {
for (const phase of [
"HEAD",
"workspace tree",
"index tree",
"restore worktree",
"restore index",
] as const) {
it.effect(`excludes manual checkout operations during ${phase}`, () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const real = yield* GitCore;
const checkpointRef = checkpointBaselineRefForThreadTurn(
ThreadId.make("coordinated-store"),
1,
);
yield* writeTextFile(path.join(cwd, "README.md"), "staged\n");
yield* git(cwd, ["add", "README.md"]);
yield* writeTextFile(path.join(cwd, "README.md"), "unstaged\n");
const entered = yield* Deferred.make<void>();
const release = yield* Deferred.make<void>();
let armed = false;
const restoring = phase.startsWith("restore");
const instrumentedGit = Layer.succeed(GitCore, {
...real,
execute: (input) =>
real.execute(input).pipe(
Effect.tap(() => {
const matches =
phase === "HEAD"
? input.operation === "CheckpointStore.resolveHeadCommit"
: phase === "workspace tree"
? input.args[0] === "write-tree" && input.env?.GIT_INDEX_FILE !== undefined
: phase === "index tree"
? input.args[0] === "write-tree" && input.env?.GIT_INDEX_FILE === undefined
: phase === "restore worktree"
? input.args[0] === "read-tree" && input.args[1] === "--reset"
: input.args[0] === "read-tree" && input.args.length === 2;
return armed && matches
? Deferred.succeed(entered, undefined).pipe(
Effect.andThen(Deferred.await(release)),
)
: Effect.void;
}),
),
});
yield* Effect.gen(function* () {
const store = yield* CheckpointStore;
const coordinator = yield* CheckoutCoordinator;
if (restoring) {
yield* store.captureCheckpoint({ cwd, checkpointRef });
yield* writeTextFile(path.join(cwd, "README.md"), "later\n");
yield* git(cwd, ["add", "README.md"]);
yield* writeTextFile(path.join(cwd, "untracked.txt"), "remove me\n");
}
armed = true;
const operation = yield* (
restoring
? store.restoreCheckpoint({ cwd, checkpointRef })
: store.captureCheckpoint({ cwd, checkpointRef })
).pipe(Effect.forkScoped);
yield* Effect.gen(function* () {
yield* Deferred.await(entered);
let mutated = false;
const manual = coordinator.tryWithCheckout(
cwd,
writeTextFile(path.join(cwd, "README.md"), "manual\n").pipe(
Effect.andThen(git(cwd, ["add", "README.md"])),
Effect.tap(() =>
Effect.sync(() => {
mutated = true;
}),
),
),
);
expect(Option.isNone(yield* manual)).toBe(true);
expect(mutated).toBe(false);
yield* Deferred.succeed(release, undefined);
yield* Fiber.join(operation);
if (restoring) {
const fs = yield* FileSystem.FileSystem;
expect(yield* fs.readFileString(path.join(cwd, "README.md"))).toBe("unstaged\n");
expect(yield* git(cwd, ["show", ":README.md"])).toBe("staged");
expect(yield* fs.exists(path.join(cwd, "untracked.txt"))).toBe(false);
} else {
expect(yield* git(cwd, ["show", `${checkpointRef}:README.md`])).toBe("unstaged");
const message = yield* git(cwd, ["show", "-s", "--format=%B", checkpointRef]);
const indexTree = /^t3-index-tree=(.+)$/m.exec(message)?.[1];
expect(indexTree).toBeDefined();
expect(yield* git(cwd, ["show", `${indexTree}:README.md`])).toBe("staged");
expect(yield* git(cwd, ["rev-parse", `${checkpointRef}^`])).toBe(
yield* git(cwd, ["rev-parse", "HEAD"]),
);
}
expect(Option.isSome(yield* manual)).toBe(true);
expect(mutated).toBe(true);
}).pipe(Effect.ensuring(Deferred.succeed(release, undefined)));
}).pipe(
Effect.provide(
CheckpointStoreLive.pipe(
Layer.provide(instrumentedGit),
Layer.provide(NodeServices.layer),
),
),
);
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, GitCoreTestLayer))),
);
}

it.effect("resolves preferred, fallback, and target checkpoint refs only once", () =>
Effect.gen(function* () {
const threadId = ThreadId.make("thread-checkpoint-store-range-resolution");
Expand Down
141 changes: 75 additions & 66 deletions apps/server/src/checkpointing/Layers/CheckpointStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Cache, Data, Duration, Effect, Exit, Layer, FileSystem, Path } from "ef
import { CheckpointInvariantError, CheckpointRefUnavailableError } from "../Errors.ts";
import { GitCommandError } from "@t3tools/contracts";
import { GitCore } from "../../git/Services/GitCore.ts";
import { CheckoutCoordinator, CheckoutCoordinatorLive } from "../../git/CheckoutCoordinator.ts";
import { CheckpointStore, type CheckpointStoreShape } from "../Services/CheckpointStore.ts";
import { CheckpointRef } from "@t3tools/contracts";
import { normalizeChangedFilePath } from "@t3tools/shared/toolChangedFiles";
Expand Down Expand Up @@ -56,6 +57,7 @@ const makeCheckpointStore = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const git = yield* GitCore;
const coordinator = yield* CheckoutCoordinator;

const resolveHeadCommit = (cwd: string): Effect.Effect<string | null, GitCommandError> =>
git
Expand Down Expand Up @@ -132,72 +134,77 @@ const makeCheckpointStore = Effect.gen(function* () {
readonly cwd: string;
readonly operation: string;
}) {
return yield* Effect.acquireUseRelease(
fs.makeTempDirectory({ prefix: "t3-fs-checkpoint-" }),
Effect.fn("snapshotWorkspace.withTempDirectory")(function* (tempDir) {
const tempIndexPath = path.join(tempDir, `index-${randomUUID()}`);
const env: NodeJS.ProcessEnv = {
...process.env,
GIT_INDEX_FILE: tempIndexPath,
};
const headCommit = yield* resolveHeadCommit(input.cwd);
if (headCommit !== null) {
yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["read-tree", "HEAD"],
env,
});
}
yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["add", "-A", "--", "."],
env,
});
const writeTreeResult = yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["write-tree"],
env,
});
const treeOid = writeTreeResult.stdout.trim();
if (treeOid.length === 0) {
return yield* new GitCommandError({
operation: input.operation,
command: "git write-tree",
cwd: input.cwd,
detail: "git write-tree returned an empty tree oid.",
});
}
const indexTreeResult = yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["write-tree"],
});
const indexTreeOid = indexTreeResult.stdout.trim();
if (indexTreeOid.length === 0) {
return yield* new GitCommandError({
operation: input.operation,
command: "git write-tree",
cwd: input.cwd,
detail: "git write-tree returned an empty index tree oid.",
});
}
return { headCommit, treeOid, indexTreeOid };
}),
(tempDir) => fs.remove(tempDir, { recursive: true }),
).pipe(
Effect.catchTag("PlatformError", (error) =>
Effect.fail(
new CheckpointInvariantError({
operation: input.operation,
detail: "Failed to snapshot workspace.",
cause: error,
return yield* coordinator
.withCheckout(
input.cwd,
Effect.acquireUseRelease(
fs.makeTempDirectory({ prefix: "t3-fs-checkpoint-" }),
Effect.fn("snapshotWorkspace.withTempDirectory")(function* (tempDir) {
const tempIndexPath = path.join(tempDir, `index-${randomUUID()}`);
const env: NodeJS.ProcessEnv = {
...process.env,
GIT_INDEX_FILE: tempIndexPath,
};
const headCommit = yield* resolveHeadCommit(input.cwd);
if (headCommit !== null) {
yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["read-tree", "HEAD"],
env,
});
}
yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["add", "-A", "--", "."],
env,
});
const writeTreeResult = yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["write-tree"],
env,
});
const treeOid = writeTreeResult.stdout.trim();
if (treeOid.length === 0) {
return yield* new GitCommandError({
operation: input.operation,
command: "git write-tree",
cwd: input.cwd,
detail: "git write-tree returned an empty tree oid.",
});
}
const indexTreeResult = yield* git.execute({
operation: input.operation,
cwd: input.cwd,
args: ["write-tree"],
});
const indexTreeOid = indexTreeResult.stdout.trim();
if (indexTreeOid.length === 0) {
return yield* new GitCommandError({
operation: input.operation,
command: "git write-tree",
cwd: input.cwd,
detail: "git write-tree returned an empty index tree oid.",
});
}
return { headCommit, treeOid, indexTreeOid };
}),
(tempDir) => fs.remove(tempDir, { recursive: true }),
),
),
);
)
.pipe(
Effect.catchTag("PlatformError", (error) =>
Effect.fail(
new CheckpointInvariantError({
operation: input.operation,
detail: "Failed to snapshot workspace.",
cause: error,
}),
),
),
);
});

const captureCheckpoint: CheckpointStoreShape["captureCheckpoint"] = Effect.fn(
Expand Down Expand Up @@ -862,11 +869,13 @@ const makeCheckpointStore = Effect.gen(function* () {
captureCheckpoint,
hasCheckpointRef,
checkpointRefMatchesWorkspace,
restoreCheckpoint,
restoreCheckpoint: (input) => coordinator.withCheckout(input.cwd, restoreCheckpoint(input)),
diffCheckpoints,
diffCheckpointFiles,
deleteCheckpointRefs,
} satisfies CheckpointStoreShape;
});

export const CheckpointStoreLive = Layer.effect(CheckpointStore, makeCheckpointStore);
export const CheckpointStoreLive = Layer.effect(CheckpointStore, makeCheckpointStore).pipe(
Layer.provideMerge(CheckoutCoordinatorLive),
);
85 changes: 85 additions & 0 deletions apps/server/src/git/CheckoutCoordinator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { assert, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { Deferred, Effect, Fiber, FileSystem, Layer, Option } from "effect";
import { CheckoutCoordinator, CheckoutCoordinatorLive } from "./CheckoutCoordinator.ts";
import { runProcess } from "../processRunner.ts";

const TestLayer = CheckoutCoordinatorLive.pipe(Layer.provideMerge(NodeServices.layer));

it.effect("shares aliases, skips busy automatic work, and leaves other worktrees independent", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "checkout-coordinator-" });
yield* fs.makeDirectory(`${root}/checkout`);
yield* Effect.promise(() => runProcess("git", ["init", `${root}/checkout`]));
yield* fs.makeDirectory(`${root}/checkout/nested`);
yield* fs.symlink(`${root}/checkout`, `${root}/alias`);
const coordinator = yield* CheckoutCoordinator;
const entered = yield* Deferred.make<void>();
const release = yield* Deferred.make<void>();
const holder = yield* coordinator
.withCheckout(
`${root}/checkout`,
Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(release))),
)
.pipe(Effect.forkScoped);
yield* Deferred.await(entered);
assert.isTrue(Option.isNone(yield* coordinator.tryWithCheckout(`${root}/alias`, Effect.void)));
assert.isTrue(
Option.isNone(yield* coordinator.tryWithCheckout(`${root}/checkout/nested`, Effect.void)),
);
assert.isTrue(Option.isSome(yield* coordinator.tryWithCheckout(`${root}/other`, Effect.void)));
yield* Deferred.succeed(release, undefined);
yield* Fiber.join(holder);
assert.isTrue(Option.isSome(yield* coordinator.tryWithCheckout(`${root}/alias`, Effect.void)));
}).pipe(Effect.provide(TestLayer)),
);

it.effect("releases interrupted holders and cancelled waiters without losing the reservation", () =>
Effect.gen(function* () {
const coordinator = yield* CheckoutCoordinator;
const entered = yield* Deferred.make<void>();
const holder = yield* coordinator
.withCheckout(
process.cwd(),
Deferred.succeed(entered, undefined).pipe(Effect.andThen(Effect.never)),
)
.pipe(Effect.forkScoped);
yield* Deferred.await(entered);
const waiter = yield* coordinator
.withCheckout(process.cwd(), Effect.never)
.pipe(Effect.forkScoped);
yield* Fiber.interrupt(waiter);
assert.isTrue(Option.isNone(yield* coordinator.tryWithCheckout(process.cwd(), Effect.void)));
yield* Fiber.interrupt(holder);
assert.isTrue(Option.isSome(yield* coordinator.tryWithCheckout(process.cwd(), Effect.void)));
}).pipe(Effect.provide(TestLayer)),
);

it.effect("keeps overlapping finalizations excluded without holding the operation mutex", () =>
Effect.gen(function* () {
const coordinator = yield* CheckoutCoordinator;
yield* coordinator.beginFinalization("first", process.cwd());
yield* coordinator.beginFinalization("second", process.cwd());
assert.isTrue(yield* coordinator.isFinalizing(process.cwd()));
assert.isTrue(Option.isSome(yield* coordinator.tryWithCheckout(process.cwd(), Effect.void)));
yield* coordinator.endFinalization("first");
assert.isTrue(yield* coordinator.isFinalizing(process.cwd()));
yield* coordinator.endFinalization("second");
assert.isFalse(yield* coordinator.isFinalizing(process.cwd()));
}).pipe(Effect.provide(TestLayer)),
);

it.effect("does not carry process-local exclusions into a rebuilt runtime", () =>
Effect.gen(function* () {
yield* Effect.gen(function* () {
const coordinator = yield* CheckoutCoordinator;
yield* coordinator.beginFinalization("orphaned-completion", process.cwd());
}).pipe(Effect.provide(CheckoutCoordinatorLive));
yield* Effect.gen(function* () {
const coordinator = yield* CheckoutCoordinator;
assert.isFalse(yield* coordinator.isFinalizing(process.cwd()));
assert.isTrue(Option.isSome(yield* coordinator.tryWithCheckout(process.cwd(), Effect.void)));
}).pipe(Effect.provide(CheckoutCoordinatorLive));
}),
);
Loading
Loading