diff --git a/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts b/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts index c0e14d72b8..3abf10c03f 100644 --- a/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/filesystem.e2e.test.ts @@ -1,4 +1,5 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { execFileSync } from "node:child_process"; import { mkdtempSync, writeFileSync, rmSync, mkdirSync, symlinkSync, readdirSync } from "fs"; import { tmpdir } from "os"; import path from "path"; @@ -22,6 +23,80 @@ afterEach(async () => { await ctx.cleanup(); }, 60000); +describe("checkout discard", () => { + test.each([".", "apps/web"])( + "discards selected Changes from the %s workspace without changing siblings", + async (workspacePath) => { + const cwd = tmpCwd(); + function git(args: string[]): string { + return execFileSync("git", args, { cwd, encoding: "utf8", stdio: "pipe" }); + } + try { + git(["init", "-b", "main"]); + git(["config", "user.name", "Paseo Test"]); + git(["config", "user.email", "test@getpaseo.local"]); + git(["config", "core.autocrlf", "false"]); + const nested = path.join(cwd, "apps", "web"); + mkdirSync(nested, { recursive: true }); + writeFileSync(path.join(nested, "target[ab].txt"), "target base\n"); + writeFileSync(path.join(nested, "targeta.txt"), "sibling base\n"); + writeFileSync(path.join(nested, "deleted.txt"), "deleted base\n"); + git(["add", "."]); + git(["-c", "commit.gpgsign=false", "commit", "-m", "initial"]); + const creation = await ctx.client.createWorkspace({ + source: { kind: "directory", path: path.join(cwd, workspacePath) }, + title: "Discard regression", + }); + const workspaceCwd = creation.workspace?.workspaceDirectory; + if (!workspaceCwd) { + throw new Error(creation.error ?? "Workspace creation failed"); + } + + writeFileSync(path.join(nested, "target[ab].txt"), "target edited\n"); + writeFileSync(path.join(nested, "targeta.txt"), "sibling edited\n"); + writeFileSync(path.join(nested, "staged.txt"), "staged new file\n"); + writeFileSync(path.join(nested, "scratch.txt"), "untracked file\n"); + git(["--literal-pathspecs", "add", "--", "apps/web/target[ab].txt", "apps/web/staged.txt"]); + git(["rm", "apps/web/deleted.txt"]); + + const subscription = ctx.client.observeCheckoutDiff(workspaceCwd, { mode: "uncommitted" }); + try { + const changes = await subscription.ready; + expect(changes.error).toBeNull(); + expect(changes.files.map((file) => file.path).sort()).toEqual([ + "apps/web/deleted.txt", + "apps/web/scratch.txt", + "apps/web/staged.txt", + "apps/web/target[ab].txt", + "apps/web/targeta.txt", + ]); + const selectedPaths = changes.files + .filter((file) => file.path !== "apps/web/targeta.txt") + .map((file) => file.path); + const discarded = await ctx.client.checkoutDiscardChanges(workspaceCwd, { + paths: selectedPaths, + }); + expect(discarded).toMatchObject({ success: true, error: null }); + + const target = await ctx.client.readFile(cwd, "apps/web/target[ab].txt"); + expect(new TextDecoder().decode(target.bytes)).toBe("target base\n"); + const restored = await ctx.client.readFile(cwd, "apps/web/deleted.txt"); + expect(new TextDecoder().decode(restored.bytes)).toBe("deleted base\n"); + const sibling = await ctx.client.readFile(cwd, "apps/web/targeta.txt"); + expect(new TextDecoder().decode(sibling.bytes)).toBe("sibling edited\n"); + await expect(ctx.client.readFile(cwd, "apps/web/staged.txt")).rejects.toThrow(/ENOENT/); + await expect(ctx.client.readFile(cwd, "apps/web/scratch.txt")).rejects.toThrow(/ENOENT/); + expect(git(["status", "--porcelain=v1", "-z"])).toBe(" M apps/web/targeta.txt\0"); + } finally { + await subscription.release(); + } + } finally { + rmSync(cwd, { recursive: true, force: true }); + } + }, + ); +}); + describe("file explorer", () => { test.each(["..", "..."])( "workspace suggestions resolve exact paths starting with %s",