Skip to content

Commit ed97690

Browse files
committed
ENG-2219-end2end tests task 6.1
1 parent 08de3af commit ed97690

1 file changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { CrossAppNode } from "@repo/database/crossAppContracts";
3+
import type { DGSupabaseClient } from "@repo/database/lib/client";
4+
import { contentTypes } from "@repo/content-model";
5+
import type { SharedNode } from "@repo/database/lib/sharedNodes";
6+
import { MAX_ASSET_BYTES } from "@repo/database/lib/assetLimits";
7+
import { publishNodeAssets, summarizeAssetResults } from "../publishNodeAssets";
8+
import { importNodeAssets } from "../importNodeAssets";
9+
import { mirrorAssetToRoamStorage } from "../mirrorAssetToRoamStorage";
10+
11+
/**
12+
* The degradation path, followed across both transfers rather than within one.
13+
*
14+
* The published markdown of the first half is the input to the second, so what a
15+
* destination actually receives for an asset that never made it into shared storage is
16+
* asserted rather than assumed: publication reports the failure and leaves the token,
17+
* and import leaves that same token alone because no row matches it. The two halves are
18+
* covered separately in `publishNodeAssets.test.ts` and `importNodeAssets.test.ts`; what
19+
* is only visible here is that they agree on what passes between them.
20+
*/
21+
22+
vi.mock("../mirrorAssetToRoamStorage", () => ({
23+
mirrorAssetToRoamStorage: vi.fn(),
24+
}));
25+
const mirror = vi.mocked(mirrorAssetToRoamStorage);
26+
27+
const roamAsset = (name: string) =>
28+
`https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FMAPLab%2F${name}.png?alt=media&token=9f1c07a4`;
29+
30+
const STORED = roamAsset("stored");
31+
const UNREADABLE = roamAsset("unreadable");
32+
const OVERSIZED = roamAsset("oversized");
33+
const EXTERNAL = "https://example.org/not-an-asset.png";
34+
35+
const MARKDOWN = [
36+
"# Sleep improves memory consolidation",
37+
"",
38+
`![](${STORED})`,
39+
`![](${UNREADABLE})`,
40+
`![](${OVERSIZED})`,
41+
`[a paper](${EXTERNAL})`,
42+
"",
43+
"- Supported by [[EVD]] - Rasch & Born 2013",
44+
].join("\n");
45+
46+
const SOURCE_LOCAL_ID = "tgWb6JozF";
47+
48+
const node: CrossAppNode = {
49+
localId: SOURCE_LOCAL_ID,
50+
nodeType: "rCLM0schema",
51+
coreTitle: "Sleep improves memory consolidation",
52+
content: {
53+
direct: { value: "Sleep improves memory consolidation" },
54+
full: { contentType: contentTypes.markdown, value: MARKDOWN },
55+
},
56+
createdAt: new Date("2026-06-12T14:00:00.000Z"),
57+
modifiedAt: new Date("2026-06-12T15:00:00.000Z"),
58+
authorId: "maparent",
59+
};
60+
61+
const sharedNode = {
62+
rid: "orn:roam.node:MAPLab/tgWb6JozF",
63+
sourceLocalId: SOURCE_LOCAL_ID,
64+
spaceId: 20,
65+
spaceName: "MAPLab",
66+
spaceUri: "roam:MAPLab",
67+
platform: "Roam",
68+
title: "Sleep improves memory consolidation",
69+
created: null,
70+
lastModified: "2026-06-12T15:00:00.000Z",
71+
directMetadata: null,
72+
} as unknown as SharedNode;
73+
74+
type Row = {
75+
filepath: string;
76+
filehash: string;
77+
source_path: string | null;
78+
};
79+
80+
/**
81+
* One store standing in for Supabase across both halves: publication inserts into it and
82+
* import reads back out of it, so the rows the destination sees are the rows publication
83+
* actually wrote.
84+
*/
85+
const makeSharedStorage = () => {
86+
const rows: Row[] = [];
87+
const thenable = (result: unknown) => ({
88+
then: (resolve: (value: unknown) => unknown) =>
89+
Promise.resolve(result).then(resolve),
90+
});
91+
const selectChain = () => {
92+
const chain = {
93+
eq: () => chain,
94+
in: () => chain,
95+
order: () => chain,
96+
then: (resolve: (value: unknown) => unknown) =>
97+
Promise.resolve({ data: rows, error: null }).then(resolve),
98+
};
99+
return chain;
100+
};
101+
const client = {
102+
rpc: vi.fn((_fn: string, { hashvalue }: { hashvalue: string }) =>
103+
Promise.resolve({
104+
data: rows.some((row) => row.filehash === hashvalue),
105+
error: null,
106+
}),
107+
),
108+
storage: {
109+
from: vi.fn(() => ({
110+
upload: vi.fn().mockResolvedValue({ error: null }),
111+
})),
112+
},
113+
from: vi.fn(() => ({
114+
select: vi.fn(() => selectChain()),
115+
delete: vi.fn(() => {
116+
const chain = {
117+
eq: () => chain,
118+
notIn: () => chain,
119+
then: (resolve: (value: unknown) => unknown) =>
120+
Promise.resolve({ error: null }).then(resolve),
121+
};
122+
return chain;
123+
}),
124+
insert: vi.fn((inserted: Row) => {
125+
rows.push({
126+
filepath: inserted.filepath,
127+
filehash: inserted.filehash,
128+
source_path: inserted.source_path ?? null,
129+
});
130+
return thenable({ error: null });
131+
}),
132+
})),
133+
} as unknown as DGSupabaseClient;
134+
return { client, rows };
135+
};
136+
137+
/**
138+
* Roam's storage: one asset readable, one unreadable, one past the publish cap.
139+
*
140+
* Both reads an asset takes are stubbed — the descriptor over `fetch`, the bytes through
141+
* `file.get` — and the unreadable one fails on either, so the test does not depend on
142+
* which of the two happens to reach it first.
143+
*/
144+
const stubRoamStorage = () => {
145+
const objectPath = (url: string) => url.split("?")[0] ?? url;
146+
const isUnreadable = (url: string) =>
147+
objectPath(url) === objectPath(UNREADABLE);
148+
149+
vi.stubGlobal(
150+
"fetch",
151+
vi.fn((input: string) => {
152+
if (isUnreadable(input))
153+
return Promise.resolve({
154+
ok: false,
155+
status: 500,
156+
} as unknown as Response);
157+
const size =
158+
objectPath(input) === objectPath(OVERSIZED) ? MAX_ASSET_BYTES + 1 : 7;
159+
return Promise.resolve({
160+
ok: true,
161+
status: 200,
162+
json: () =>
163+
Promise.resolve({
164+
name: "imgs/app/MAPLab/stored.png",
165+
contentType: "image/png",
166+
size: String(size),
167+
metadata: { "file-name": "diagram.png" },
168+
}),
169+
} as unknown as Response);
170+
}),
171+
);
172+
173+
const get = vi.fn(({ url }: { url: string }) =>
174+
isUnreadable(url)
175+
? Promise.reject(new Error("Roam could not read the file"))
176+
: Promise.resolve(
177+
new File(["PNGDATA"], "diagram.png", { type: "image/png" }),
178+
),
179+
);
180+
vi.stubGlobal("window", {
181+
roamAlphaAPI: { file: { get }, graph: { isEncrypted: false } },
182+
});
183+
return { get };
184+
};
185+
186+
const THIS_GRAPHS_COPY =
187+
"https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FOtherGraph%2FaB3dEf.png?alt=media&token=1122";
188+
189+
describe("asset degradation across both transfers", () => {
190+
let storage: ReturnType<typeof makeSharedStorage>;
191+
192+
beforeEach(() => {
193+
vi.clearAllMocks();
194+
storage = makeSharedStorage();
195+
stubRoamStorage();
196+
});
197+
198+
afterEach(() => {
199+
vi.unstubAllGlobals();
200+
});
201+
202+
const publish = () =>
203+
publishNodeAssets({
204+
client: storage.client,
205+
spaceId: 20,
206+
nodes: [node],
207+
});
208+
209+
it("leaves the token of every asset it could not store in the published markdown, and reports each one", async () => {
210+
const summary = summarizeAssetResults(await publish());
211+
212+
expect(node.content.full?.value).toBe(MARKDOWN);
213+
expect(summary.failed.map((f) => f.sourceRef)).toEqual([UNREADABLE]);
214+
expect(summary.tooLarge.map((s) => s.sourceRef)).toEqual([OVERSIZED]);
215+
expect(summary.copied).toBe(1);
216+
expect(storage.rows.map((r) => r.filepath)).toEqual([STORED]);
217+
});
218+
219+
it("imports the published markdown with its body intact, rewriting only what was stored", async () => {
220+
await publish();
221+
mirror.mockResolvedValue({
222+
status: "mirrored",
223+
contentHash: storage.rows[0].filehash,
224+
url: THIS_GRAPHS_COPY,
225+
});
226+
227+
const { markdown, report } = await importNodeAssets({
228+
client: storage.client,
229+
sharedNode,
230+
markdown: MARKDOWN,
231+
});
232+
233+
// Only the asset that reached shared storage was mirrored, so only its token moved.
234+
expect(mirror).toHaveBeenCalledTimes(1);
235+
expect(markdown).toContain(`![](${THIS_GRAPHS_COPY})`);
236+
// The rest of the node arrives exactly as published: the two tokens that never
237+
// became rows still point at Roam's world-readable originals, which is what makes
238+
// them render, and the external link was never ours to touch.
239+
expect(markdown).toContain(`![](${UNREADABLE})`);
240+
expect(markdown).toContain(`![](${OVERSIZED})`);
241+
expect(markdown).toContain(`[a paper](${EXTERNAL})`);
242+
expect(markdown).toContain("# Sleep improves memory consolidation");
243+
expect(markdown).toContain("- Supported by [[EVD]] - Rasch & Born 2013");
244+
expect(report).toMatchObject({ mirrored: 1, reused: 0, skipped: [] });
245+
});
246+
247+
it("reports an asset that fails on the way in, leaving its token and the node body untouched", async () => {
248+
await publish();
249+
mirror.mockRejectedValue(new Error("upload refused"));
250+
251+
const { markdown, report } = await importNodeAssets({
252+
client: storage.client,
253+
sharedNode,
254+
markdown: MARKDOWN,
255+
});
256+
257+
expect(markdown).toBe(MARKDOWN);
258+
expect(report.failed).toEqual([
259+
{ sourceRef: STORED, message: "upload refused" },
260+
]);
261+
});
262+
});

0 commit comments

Comments
 (0)