|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | + |
| 3 | +import { hasOversizedPathSegment, MAX_PATH_SEGMENT_BYTES, writeFile } from "./files.js"; |
| 4 | +import type { StorageAdapter } from "./storage.js"; |
| 5 | + |
| 6 | +describe("hasOversizedPathSegment", () => { |
| 7 | + it("accepts a path whose every segment is at the byte limit", () => { |
| 8 | + const segment = "a".repeat(MAX_PATH_SEGMENT_BYTES - ".json".length); |
| 9 | + expect(hasOversizedPathSegment(`/reddit/subreddits/localllama/posts/${segment}.json`)).toBe(false); |
| 10 | + }); |
| 11 | + |
| 12 | + it("rejects a path with one segment over the byte limit", () => { |
| 13 | + const segment = "a".repeat(MAX_PATH_SEGMENT_BYTES + 1); |
| 14 | + expect(hasOversizedPathSegment(`/reddit/subreddits/localllama/posts/${segment}.json`)).toBe(true); |
| 15 | + }); |
| 16 | + |
| 17 | + it("counts UTF-8 encoded bytes, not JS string length", () => { |
| 18 | + // Each "é" is 1 UTF-16 code unit but 2 UTF-8 bytes. |
| 19 | + const segment = "é".repeat(MAX_PATH_SEGMENT_BYTES); // 200 chars, 400 bytes |
| 20 | + expect(hasOversizedPathSegment(`/x/${segment}`)).toBe(true); |
| 21 | + }); |
| 22 | + |
| 23 | + it("ignores empty segments from a normalized path", () => { |
| 24 | + expect(hasOversizedPathSegment("/reddit/subreddits/localllama.json")).toBe(false); |
| 25 | + }); |
| 26 | + |
| 27 | + it("reproduces the real oversized r/LocalLLaMA post slug that broke the mount", () => { |
| 28 | + const badPath = |
| 29 | + "/reddit/subreddits/localllama/posts/i-just-don-t-get-it-these-big-tech-companies-can-illegally-" + |
| 30 | + "scrape-the-entire-internet-and-gatekeep-their-better-models-behind-higher-prices-so-it-s-natural-" + |
| 31 | + "that-people-look-for-affordable-options-and-there-will-be-providers-who-apparently-distill-models-" + |
| 32 | + "from-them__1uvwn9q.json"; |
| 33 | + expect(hasOversizedPathSegment(badPath)).toBe(true); |
| 34 | + }); |
| 35 | +}); |
| 36 | + |
| 37 | +describe("writeFile", () => { |
| 38 | + function storageStub(overrides: Partial<StorageAdapter> = {}): StorageAdapter { |
| 39 | + return { |
| 40 | + getFile: () => null, |
| 41 | + listFiles: () => [], |
| 42 | + putFile: () => {}, |
| 43 | + deleteFile: () => {}, |
| 44 | + appendEvent: () => {}, |
| 45 | + listEvents: () => ({ items: [], nextCursor: null }), |
| 46 | + getRecentEvents: () => [], |
| 47 | + getOperation: () => null, |
| 48 | + putOperation: () => {}, |
| 49 | + listOperations: () => ({ items: [], nextCursor: null }), |
| 50 | + nextRevision: () => "rev_1", |
| 51 | + nextOperationId: () => "op_1", |
| 52 | + nextEventId: () => "evt_1", |
| 53 | + enqueueWriteback: () => {}, |
| 54 | + getPendingWritebacks: () => [], |
| 55 | + getWorkspaceId: () => "ws_test", |
| 56 | + ...overrides, |
| 57 | + } as StorageAdapter; |
| 58 | + } |
| 59 | + |
| 60 | + it("rejects a write whose path has an oversized segment before touching storage", () => { |
| 61 | + const segment = "a".repeat(MAX_PATH_SEGMENT_BYTES + 1); |
| 62 | + let storageTouched = false; |
| 63 | + const storage = storageStub({ |
| 64 | + getFile: () => { |
| 65 | + storageTouched = true; |
| 66 | + return null; |
| 67 | + }, |
| 68 | + }); |
| 69 | + |
| 70 | + const result = writeFile(storage, { |
| 71 | + path: `/reddit/subreddits/localllama/posts/${segment}.json`, |
| 72 | + ifMatch: "*", |
| 73 | + content: "{}", |
| 74 | + }); |
| 75 | + |
| 76 | + expect(result).toEqual({ ok: false, error: "invalid_input" }); |
| 77 | + expect(storageTouched).toBe(false); |
| 78 | + }); |
| 79 | + |
| 80 | + it("allows a write with normal-length path segments to proceed past the guard", () => { |
| 81 | + const storage = storageStub(); |
| 82 | + const result = writeFile(storage, { |
| 83 | + path: "/reddit/subreddits/localllama/posts/a-normal-title__abc123.json", |
| 84 | + ifMatch: "0", |
| 85 | + content: "{}", |
| 86 | + }); |
| 87 | + |
| 88 | + expect(result.ok).toBe(true); |
| 89 | + }); |
| 90 | +}); |
0 commit comments