Blame · Line-by-line history
editor-nested-paths.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 4f4cb3a | 1 | /** |
| 2 | * The web editor must not destroy directory structure. | |
| 3 | * | |
| 4 | * Both editor write paths built their commit by piping `git ls-tree -r` into | |
| 5 | * `git mktree`: | |
| 6 | * | |
| 7 | * ls-tree -r emits a FLAT, recursive listing whose names are full paths | |
| 8 | * ("100644 blob <sha>\tsrc/lib/foo.ts") | |
| 9 | * mktree builds ONE tree object and expects bare filenames for a | |
| 10 | * single directory level | |
| 11 | * | |
| 12 | * So any repository with a subdirectory produced a rejected or malformed | |
| 13 | * tree. Editing or creating a file in a nested repo silently lost the | |
| 14 | * structure — data loss on a core feature, and invisible because the handler | |
| 15 | * never checked mktree's exit status before running commit-tree. | |
| 16 | * | |
| 17 | * Both handlers now delegate to createOrUpdateFileOnBranch(), which uses | |
| 18 | * read-tree + update-index --cacheinfo + write-tree against an isolated | |
| 19 | * temporary index. That handles arbitrary nesting and cannot be stomped by a | |
| 20 | * concurrent write. | |
| 21 | * | |
| 22 | * These are structural: a functional test needs a real bare repo with nested | |
| 23 | * directories on disk, and git/repository.ts already owns that behaviour. | |
| 24 | * What must not regress is that the editor no longer hand-rolls it. | |
| 25 | */ | |
| 26 | ||
| 27 | import { describe, expect, it } from "bun:test"; | |
| 28 | import { readFileSync } from "fs"; | |
| 29 | ||
| 30 | const SRC = readFileSync("src/routes/editor.tsx", "utf8"); | |
| 31 | // Comments explain the old bug and necessarily contain the strings being | |
| 32 | // asserted absent — assert on code, not prose. | |
| 33 | const CODE = SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); | |
| 34 | ||
| 35 | describe("the editor no longer hand-rolls tree construction", () => { | |
| 36 | it("calls git mktree nowhere", () => { | |
| 37 | expect(CODE).not.toContain('["git", "mktree"]'); | |
| 38 | expect(CODE).not.toMatch(/"mktree"/); | |
| 39 | }); | |
| 40 | ||
| 41 | it("does not feed a recursive ls-tree into a tree builder", () => { | |
| 42 | // The specific combination that caused the loss. | |
| 43 | expect(CODE).not.toMatch(/"ls-tree",\s*"-r"/); | |
| 44 | }); | |
| 45 | ||
| 46 | it("does not hand-roll commit-tree either", () => { | |
| 47 | // commit-tree on a malformed tree is how this stayed silent: the handler | |
| 48 | // never checked mktree's exit code before committing. | |
| 49 | expect(CODE).not.toContain("commit-tree"); | |
| 50 | }); | |
| 51 | }); | |
| 52 | ||
| 53 | describe("both write paths use the shared helper", () => { | |
| 54 | it("create-file and edit-file both call createOrUpdateFileOnBranch", () => { | |
| 55 | const calls = CODE.match(/createOrUpdateFileOnBranch\(\{/g) ?? []; | |
| 56 | expect(calls.length).toBe(2); | |
| 57 | }); | |
| 58 | ||
| 59 | it("both surface a write failure instead of redirecting to a broken blob", () => { | |
| 60 | // Previously a failed write still redirected to the file view, which then | |
| 61 | // 404'd or showed stale content — the user had no idea it had not saved. | |
| 62 | const guards = CODE.match(/if \("error" in written\)/g) ?? []; | |
| 63 | expect(guards.length).toBe(2); | |
| 64 | }); | |
| 65 | ||
| 66 | it("distinguishes a concurrent edit from a write failure", () => { | |
| 67 | expect(CODE).toContain('written.error === "sha-mismatch"'); | |
| 68 | }); | |
| 69 | }); | |
| 70 | ||
| 71 | describe("the helper it delegates to handles nesting", () => { | |
| 72 | const REPO = readFileSync("src/git/repository.ts", "utf8"); | |
| 73 | const start = REPO.indexOf("export async function createOrUpdateFileOnBranch"); | |
| 74 | // To the next top-level export, not a fixed char count — 3000 chars stopped | |
| 75 | // short of write-tree and the assertion failed on a correct implementation. | |
| 76 | const nextExport = REPO.indexOf("\nexport ", start + 10); | |
| 77 | const fn = REPO.slice(start, nextExport > -1 ? nextExport : REPO.length); | |
| 78 | ||
| 79 | it("seeds an index from the parent tree rather than rebuilding one flat tree", () => { | |
| 80 | expect(fn).toContain('"git", "read-tree"'); | |
| 81 | expect(fn).toContain("update-index"); | |
| 82 | expect(fn).toContain("write-tree"); | |
| 83 | }); | |
| 84 | ||
| 85 | it("uses an isolated index so concurrent writes cannot stomp each other", () => { | |
| 86 | expect(fn).toContain("GIT_INDEX_FILE"); | |
| 87 | }); | |
| 88 | }); |