142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
import { Effect } from "effect";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
decodeIssueWorkspaceResult,
|
|
makeIssueWorkspacePlan,
|
|
transitionWorkspaceRun,
|
|
WorkspaceExecutionError,
|
|
WorkspaceStateError,
|
|
WorkspaceValidationError,
|
|
} from "./project-workspace";
|
|
|
|
const makeInput = () => ({
|
|
artifacts: [
|
|
{
|
|
content: "# Work\n",
|
|
path: "work.md",
|
|
},
|
|
],
|
|
branchName: undefined,
|
|
checkoutPath: undefined,
|
|
contextFiles: [
|
|
{
|
|
content: "# Project\n",
|
|
kind: "readme" as const,
|
|
path: "README.md",
|
|
},
|
|
],
|
|
defaultBranch: "main",
|
|
issueBody: "Make the project run in an isolated checkout.",
|
|
issueId: "projectIssues|abc123",
|
|
issueNumber: 7,
|
|
issueTitle: "Use a real project checkout",
|
|
sourceUrl: "https://git.example.test/puter/project",
|
|
});
|
|
|
|
const validationFailure = (input: unknown): WorkspaceValidationError => {
|
|
const error = Effect.runSync(Effect.flip(makeIssueWorkspacePlan(input)));
|
|
expect(error).toBeInstanceOf(WorkspaceValidationError);
|
|
return error;
|
|
};
|
|
|
|
describe("project workspace primitives", () => {
|
|
it("plans a deterministic checkout and keeps control files outside the repo", () => {
|
|
const plan = Effect.runSync(makeIssueWorkspacePlan(makeInput()));
|
|
const secondPlan = Effect.runSync(makeIssueWorkspacePlan(makeInput()));
|
|
|
|
expect(plan.branchName).toBe("work/issue-7-abc123");
|
|
expect(plan.checkoutPath).toBe("/workspace/repository");
|
|
expect(plan).toEqual(secondPlan);
|
|
expect(plan.operations).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
_tag: "WriteFile",
|
|
content: expect.any(String),
|
|
path: "/workspace/control/issue.md",
|
|
},
|
|
{
|
|
_tag: "WriteFile",
|
|
content: "# Project\n",
|
|
path: "/workspace/control/context/README.md",
|
|
},
|
|
{
|
|
_tag: "WriteFile",
|
|
content: "# Work\n",
|
|
path: "/workspace/control/artifacts/work.md",
|
|
},
|
|
])
|
|
);
|
|
expect(
|
|
plan.operations.some(
|
|
(operation) =>
|
|
operation._tag === "Exec" && operation.command.includes("git clone")
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects unsafe sources and control-file paths", () => {
|
|
const missingSource = { ...makeInput(), sourceUrl: undefined };
|
|
expect(validationFailure(missingSource).reason).toBe("MissingSource");
|
|
|
|
const invalidSource = makeInput();
|
|
invalidSource.sourceUrl = "ssh://git@example.test/project";
|
|
expect(validationFailure(invalidSource).reason).toBe("InvalidSourceUrl");
|
|
|
|
const invalidPath = makeInput();
|
|
const [artifact] = invalidPath.artifacts;
|
|
if (!artifact) {
|
|
throw new Error("test artifact missing");
|
|
}
|
|
artifact.path = "../work.md";
|
|
expect(validationFailure(invalidPath).reason).toBe("InvalidPath");
|
|
});
|
|
|
|
it("rejects a checkout result that does not prove the planned branch", () => {
|
|
const plan = Effect.runSync(makeIssueWorkspacePlan(makeInput()));
|
|
const error = Effect.runSync(
|
|
Effect.flip(
|
|
decodeIssueWorkspaceResult({
|
|
command: {
|
|
exitCode: 0,
|
|
stderr: "",
|
|
stdout: "work/issue-7-other deadbeef\n",
|
|
},
|
|
plan,
|
|
})
|
|
)
|
|
);
|
|
|
|
expect(error).toBeInstanceOf(WorkspaceExecutionError);
|
|
expect(error.reason).toBe("InvalidResult");
|
|
});
|
|
|
|
it("accepts the planned checkout result and enforces run transitions", () => {
|
|
const plan = Effect.runSync(makeIssueWorkspacePlan(makeInput()));
|
|
const result = Effect.runSync(
|
|
decodeIssueWorkspaceResult({
|
|
command: {
|
|
exitCode: 0,
|
|
stderr: "",
|
|
stdout: `${plan.branchName} deadbeef1234567\n`,
|
|
},
|
|
plan,
|
|
})
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
branchName: plan.branchName,
|
|
checkoutPath: "/workspace/repository",
|
|
headSha: "deadbeef1234567",
|
|
});
|
|
expect(
|
|
Effect.runSync(transitionWorkspaceRun({ from: "queued", to: "working" }))
|
|
).toBe("working");
|
|
|
|
const error = Effect.runSync(
|
|
Effect.flip(transitionWorkspaceRun({ from: "completed", to: "working" }))
|
|
);
|
|
expect(error).toBeInstanceOf(WorkspaceStateError);
|
|
});
|
|
});
|