@symbiosis-lab/moss-plugin-matters 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +18 -0
- package/assets/icon.svg +1 -0
- package/assets/manifest.json +36 -0
- package/codegen.ts +26 -0
- package/e2e/moss-cli.test.ts +338 -0
- package/features/api/fetch-articles.feature +39 -0
- package/features/auth/wallet-auth.feature +27 -0
- package/features/download/retry-logic.feature +36 -0
- package/features/download/self-correcting.feature +83 -0
- package/features/download/worker-pool.feature +29 -0
- package/features/social/fetch-social-data.feature +40 -0
- package/features/steps/api.steps.ts +180 -0
- package/features/steps/download.steps.ts +423 -0
- package/features/steps/incremental-sync.steps.ts +105 -0
- package/features/steps/self-correcting.steps.ts +575 -0
- package/features/steps/social.steps.ts +257 -0
- package/features/steps/syndication.steps.ts +264 -0
- package/features/steps/wallet-auth.steps.ts +185 -0
- package/features/sync/article-sync.feature +49 -0
- package/features/sync/homepage-grid.feature +43 -0
- package/features/sync/incremental-sync.feature +28 -0
- package/features/syndication/create-draft.feature +35 -0
- package/package.json +58 -0
- package/src/__generated__/schema.graphql +4289 -0
- package/src/__generated__/types.ts +5355 -0
- package/src/__tests__/api.test.ts +678 -0
- package/src/__tests__/auth-route.test.ts +38 -0
- package/src/__tests__/auth-routing.test.ts +462 -0
- package/src/__tests__/auto-detect.test.ts +412 -0
- package/src/__tests__/binding-guard.test.ts +256 -0
- package/src/__tests__/config.test.ts +212 -0
- package/src/__tests__/converter.test.ts +289 -0
- package/src/__tests__/credential.test.ts +332 -0
- package/src/__tests__/domain.test.ts +341 -0
- package/src/__tests__/downloader.test.ts +679 -0
- package/src/__tests__/folder-detection.test.ts +289 -0
- package/src/__tests__/force-fresh-login.test.ts +236 -0
- package/src/__tests__/main.test.ts +2437 -0
- package/src/__tests__/progress.test.ts +93 -0
- package/src/__tests__/session.test.ts +375 -0
- package/src/__tests__/social-integration.test.ts +386 -0
- package/src/__tests__/social-sync-logic.test.ts +107 -0
- package/src/__tests__/social.test.ts +788 -0
- package/src/__tests__/sync.test.ts +1273 -0
- package/src/__tests__/syndication-toast-law.test.ts +649 -0
- package/src/__tests__/syndication.test.ts +125 -0
- package/src/__tests__/test-profile-escape.test.ts +209 -0
- package/src/__tests__/url-detect.test.ts +79 -0
- package/src/__tests__/utils.test.ts +226 -0
- package/src/api.ts +1366 -0
- package/src/auth-route.ts +38 -0
- package/src/config.ts +80 -0
- package/src/converter.ts +305 -0
- package/src/credential.ts +329 -0
- package/src/domain.ts +183 -0
- package/src/downloader.ts +761 -0
- package/src/main.ts +2092 -0
- package/src/progress.ts +89 -0
- package/src/queries/user.graphql +85 -0
- package/src/queries/viewer.graphql +104 -0
- package/src/social.ts +413 -0
- package/src/sync.ts +818 -0
- package/src/types.ts +477 -0
- package/src/url-detect.ts +49 -0
- package/src/utils.ts +305 -0
- package/test-fixtures/syndication-test-site/input/index.md +8 -0
- package/test-fixtures/syndication-test-site/input/posts/rich-test-article.md +90 -0
- package/test-helpers/TEST_ACCOUNT.md +151 -0
- package/test-helpers/api-client.ts +252 -0
- package/test-helpers/fixtures/articles.ts +147 -0
- package/test-helpers/wallet-auth.ts +305 -0
- package/test-setup/e2e.ts +93 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +39 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step definitions for self-correcting reference updates
|
|
3
|
+
* Tests that image references are updated after download (or skip if already exists)
|
|
4
|
+
*/
|
|
5
|
+
import { loadFeature, describeFeature } from "@amiceli/vitest-cucumber";
|
|
6
|
+
import { expect } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
setupMockTauri,
|
|
9
|
+
type MockTauriContext,
|
|
10
|
+
} from "@symbiosis-lab/moss-api/testing";
|
|
11
|
+
|
|
12
|
+
const feature = await loadFeature("features/download/self-correcting.feature");
|
|
13
|
+
|
|
14
|
+
describeFeature(feature, ({ Scenario }) => {
|
|
15
|
+
let ctx: MockTauriContext | null = null;
|
|
16
|
+
const projectPath = "/test/project";
|
|
17
|
+
let downloadResult: {
|
|
18
|
+
filesProcessed: number;
|
|
19
|
+
imagesDownloaded: number;
|
|
20
|
+
imagesSkipped: number;
|
|
21
|
+
errors: string[];
|
|
22
|
+
} | null = null;
|
|
23
|
+
|
|
24
|
+
// UUIDs used in tests
|
|
25
|
+
const uuid1 = "82ba1757-adc9-4a37-b097-8edbf38e9b9f";
|
|
26
|
+
const uuid2 = "12345678-1234-1234-1234-123456789abc";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Scenario: Updates references when assets already exist
|
|
30
|
+
// ============================================================================
|
|
31
|
+
Scenario("Updates references when assets already exist", ({ Given, When, Then, And }) => {
|
|
32
|
+
Given("a mock Tauri environment", () => {
|
|
33
|
+
ctx = setupMockTauri();
|
|
34
|
+
downloadResult = null;
|
|
35
|
+
expect(ctx).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
And("an in-memory filesystem", () => {
|
|
39
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
And("a markdown file with remote image URLs", () => {
|
|
43
|
+
const markdownContent = `---
|
|
44
|
+
title: "Test Article"
|
|
45
|
+
cover: "https://imagedelivery.net/xxx/prod/embed/${uuid1}/image.jpg/public"
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
Some text here.
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
`;
|
|
52
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/test.md`, markdownContent);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
And("the assets already exist locally", () => {
|
|
56
|
+
// Asset already exists - simulating a previous download
|
|
57
|
+
ctx!.filesystem.setFile(`${projectPath}/assets/${uuid1}.jpg`, "[binary image data]");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
61
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
62
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
Then("no downloads should occur", () => {
|
|
66
|
+
expect(ctx!.downloadTracker.completedDownloads.length).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
And("all image references should be updated to local paths", () => {
|
|
70
|
+
const content = ctx!.filesystem.getFile(`${projectPath}/文章/test.md`)?.content;
|
|
71
|
+
expect(content).toBeDefined();
|
|
72
|
+
// Cover should be updated to relative path
|
|
73
|
+
expect(content).toContain(`cover: "${uuid1}.jpg"`);
|
|
74
|
+
// Body image should be updated to relative path
|
|
75
|
+
expect(content).toContain(`![[${uuid1}.jpg]]`);
|
|
76
|
+
// Should NOT contain remote URLs
|
|
77
|
+
expect(content).not.toContain("https://assets.matters.news");
|
|
78
|
+
expect(content).not.toContain("https://imagedelivery.net");
|
|
79
|
+
ctx?.cleanup();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Scenario: Downloads and updates in single pass
|
|
85
|
+
// ============================================================================
|
|
86
|
+
Scenario("Downloads and updates in single pass", ({ Given, When, Then, And }) => {
|
|
87
|
+
Given("a mock Tauri environment", () => {
|
|
88
|
+
ctx = setupMockTauri();
|
|
89
|
+
downloadResult = null;
|
|
90
|
+
expect(ctx).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
And("an in-memory filesystem", () => {
|
|
94
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
And("a markdown file with remote image URLs", () => {
|
|
98
|
+
const markdownContent = `---
|
|
99
|
+
title: "Test Article"
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
`;
|
|
104
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/test.md`, markdownContent);
|
|
105
|
+
|
|
106
|
+
// Configure mock response for the download
|
|
107
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid1}/image.jpg`, {
|
|
108
|
+
status: 200,
|
|
109
|
+
ok: true,
|
|
110
|
+
contentType: "image/jpeg",
|
|
111
|
+
bytesWritten: 1024,
|
|
112
|
+
actualPath: `assets/${uuid1}.jpg`,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
And("no assets exist locally", () => {
|
|
117
|
+
// No assets in the assets folder - fresh state
|
|
118
|
+
expect(ctx!.filesystem.listFiles(`${projectPath}/assets/*`).length).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
122
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
123
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
Then("all images should be downloaded", () => {
|
|
127
|
+
expect(ctx!.downloadTracker.completedDownloads.length).toBe(1);
|
|
128
|
+
expect(downloadResult!.imagesDownloaded).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
And("all image references should be updated to local paths", () => {
|
|
132
|
+
const content = ctx!.filesystem.getFile(`${projectPath}/文章/test.md`)?.content;
|
|
133
|
+
expect(content).toBeDefined();
|
|
134
|
+
expect(content).toContain(`![[${uuid1}.jpg]]`);
|
|
135
|
+
expect(content).not.toContain("https://assets.matters.news");
|
|
136
|
+
ctx?.cleanup();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Scenario: Resumes correctly after interruption
|
|
142
|
+
// ============================================================================
|
|
143
|
+
Scenario("Resumes correctly after interruption", ({ Given, When, Then, And }) => {
|
|
144
|
+
Given("a mock Tauri environment", () => {
|
|
145
|
+
ctx = setupMockTauri();
|
|
146
|
+
downloadResult = null;
|
|
147
|
+
expect(ctx).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
And("an in-memory filesystem", () => {
|
|
151
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
And("a markdown file with multiple remote image URLs", () => {
|
|
155
|
+
const markdownContent = `---
|
|
156
|
+
title: "Test Article"
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+

|
|
160
|
+

|
|
161
|
+
`;
|
|
162
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/test.md`, markdownContent);
|
|
163
|
+
|
|
164
|
+
// Only configure response for uuid2 (uuid1 already exists)
|
|
165
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid2}/b.jpg`, {
|
|
166
|
+
status: 200,
|
|
167
|
+
ok: true,
|
|
168
|
+
contentType: "image/jpeg",
|
|
169
|
+
bytesWritten: 1024,
|
|
170
|
+
actualPath: `assets/${uuid2}.jpg`,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
And("some assets already exist locally", () => {
|
|
175
|
+
// uuid1 already downloaded (simulating interrupted run)
|
|
176
|
+
ctx!.filesystem.setFile(`${projectPath}/assets/${uuid1}.jpg`, "[binary image data]");
|
|
177
|
+
// uuid2 not downloaded yet
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
181
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
182
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
Then("only missing assets should be downloaded", () => {
|
|
186
|
+
// Only uuid2 should be downloaded
|
|
187
|
+
expect(ctx!.downloadTracker.completedDownloads.length).toBe(1);
|
|
188
|
+
expect(downloadResult!.imagesDownloaded).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
And("all image references should be updated to local paths", () => {
|
|
192
|
+
const content = ctx!.filesystem.getFile(`${projectPath}/文章/test.md`)?.content;
|
|
193
|
+
expect(content).toBeDefined();
|
|
194
|
+
expect(content).toContain(`![[${uuid1}.jpg]]`);
|
|
195
|
+
expect(content).toContain(`![[${uuid2}.jpg]]`);
|
|
196
|
+
expect(content).not.toContain("https://assets.matters.news");
|
|
197
|
+
ctx?.cleanup();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// Scenario: Handles cross-CDN URLs with same UUID
|
|
203
|
+
// ============================================================================
|
|
204
|
+
Scenario("Handles cross-CDN URLs with same UUID", ({ Given, When, Then, And }) => {
|
|
205
|
+
Given("a mock Tauri environment", () => {
|
|
206
|
+
ctx = setupMockTauri();
|
|
207
|
+
downloadResult = null;
|
|
208
|
+
expect(ctx).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
And("an in-memory filesystem", () => {
|
|
212
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
And("a markdown file with cover and body images using different CDNs", () => {
|
|
216
|
+
// Cover uses imagedelivery.net, body uses assets.matters.news
|
|
217
|
+
const markdownContent = `---
|
|
218
|
+
title: "Test Article"
|
|
219
|
+
cover: "https://imagedelivery.net/kDRCweMmqLnTPNlbum-pYA/prod/embed/${uuid1}/image.jpg/public"
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+

|
|
223
|
+
`;
|
|
224
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/test.md`, markdownContent);
|
|
225
|
+
|
|
226
|
+
// Configure response for one of the URLs (both have same UUID)
|
|
227
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid1}/image.jpg`, {
|
|
228
|
+
status: 200,
|
|
229
|
+
ok: true,
|
|
230
|
+
contentType: "image/jpeg",
|
|
231
|
+
bytesWritten: 1024,
|
|
232
|
+
actualPath: `assets/${uuid1}.jpg`,
|
|
233
|
+
});
|
|
234
|
+
ctx!.urlConfig.setResponse(`https://imagedelivery.net/kDRCweMmqLnTPNlbum-pYA/prod/embed/${uuid1}/image.jpg/public`, {
|
|
235
|
+
status: 200,
|
|
236
|
+
ok: true,
|
|
237
|
+
contentType: "image/jpeg",
|
|
238
|
+
bytesWritten: 1024,
|
|
239
|
+
actualPath: `assets/${uuid1}.jpg`,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
And("both URLs contain the same UUID", () => {
|
|
244
|
+
// This is already set up above - both URLs contain uuid1
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
248
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
249
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
Then("only one download should occur", () => {
|
|
253
|
+
// Both URLs have the same UUID, so only one download should happen
|
|
254
|
+
expect(ctx!.downloadTracker.completedDownloads.length).toBeLessThanOrEqual(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
And("both cover and body references should point to the same local file", () => {
|
|
258
|
+
const content = ctx!.filesystem.getFile(`${projectPath}/文章/test.md`)?.content;
|
|
259
|
+
expect(content).toBeDefined();
|
|
260
|
+
// Both should point to the same local path
|
|
261
|
+
expect(content).toContain(`cover: "${uuid1}.jpg"`);
|
|
262
|
+
expect(content).toContain(`![[${uuid1}.jpg]]`);
|
|
263
|
+
expect(content).not.toContain("https://imagedelivery.net");
|
|
264
|
+
expect(content).not.toContain("https://assets.matters.news");
|
|
265
|
+
ctx?.cleanup();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Scenario: Idempotent operation
|
|
271
|
+
// ============================================================================
|
|
272
|
+
Scenario("Idempotent operation", ({ Given, When, Then, And }) => {
|
|
273
|
+
let originalContent: string;
|
|
274
|
+
|
|
275
|
+
Given("a mock Tauri environment", () => {
|
|
276
|
+
ctx = setupMockTauri();
|
|
277
|
+
downloadResult = null;
|
|
278
|
+
expect(ctx).toBeDefined();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
And("an in-memory filesystem", () => {
|
|
282
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
And("a markdown file with local image references", () => {
|
|
286
|
+
const markdownContent = `---
|
|
287
|
+
title: "Test Article"
|
|
288
|
+
cover: "${uuid1}.jpg"
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
![[${uuid1}.jpg]]
|
|
292
|
+
`;
|
|
293
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/test.md`, markdownContent);
|
|
294
|
+
originalContent = markdownContent;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
And("all assets exist locally", () => {
|
|
298
|
+
ctx!.filesystem.setFile(`${projectPath}/assets/${uuid1}.jpg`, "[binary image data]");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
When("I run downloadMediaAndUpdate twice", async () => {
|
|
302
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
303
|
+
await downloadMediaAndUpdate();
|
|
304
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
Then("no downloads should occur", () => {
|
|
308
|
+
expect(ctx!.downloadTracker.completedDownloads.length).toBe(0);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
And("the file should not be modified", () => {
|
|
312
|
+
const content = ctx!.filesystem.getFile(`${projectPath}/文章/test.md`)?.content;
|
|
313
|
+
expect(content).toBe(originalContent);
|
|
314
|
+
ctx?.cleanup();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Incremental Write Behavior Tests
|
|
320
|
+
// These tests verify the core design principle: files are written immediately
|
|
321
|
+
// after processing, not batched at the end.
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
// Additional UUIDs for multi-file tests
|
|
325
|
+
const uuid3 = "33333333-3333-3333-3333-333333333333";
|
|
326
|
+
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// Scenario: Files are written immediately after processing (not batched)
|
|
329
|
+
// ============================================================================
|
|
330
|
+
Scenario("Files are written immediately after processing (not batched)", ({ Given, When, Then, And }) => {
|
|
331
|
+
Given("a mock Tauri environment", () => {
|
|
332
|
+
ctx = setupMockTauri();
|
|
333
|
+
downloadResult = null;
|
|
334
|
+
expect(ctx).toBeDefined();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
And("an in-memory filesystem", () => {
|
|
338
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
And("three markdown files each containing a unique remote image", () => {
|
|
342
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/file1.md`, `---
|
|
343
|
+
title: "File 1"
|
|
344
|
+
---
|
|
345
|
+

|
|
346
|
+
`);
|
|
347
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/file2.md`, `---
|
|
348
|
+
title: "File 2"
|
|
349
|
+
---
|
|
350
|
+

|
|
351
|
+
`);
|
|
352
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/file3.md`, `---
|
|
353
|
+
title: "File 3"
|
|
354
|
+
---
|
|
355
|
+

|
|
356
|
+
`);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
And("downloads are configured to succeed for all files", () => {
|
|
360
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid1}/img1.jpg`, {
|
|
361
|
+
status: 200,
|
|
362
|
+
ok: true,
|
|
363
|
+
contentType: "image/jpeg",
|
|
364
|
+
bytesWritten: 1024,
|
|
365
|
+
actualPath: `assets/${uuid1}.jpg`,
|
|
366
|
+
});
|
|
367
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid2}/img2.jpg`, {
|
|
368
|
+
status: 200,
|
|
369
|
+
ok: true,
|
|
370
|
+
contentType: "image/jpeg",
|
|
371
|
+
bytesWritten: 1024,
|
|
372
|
+
actualPath: `assets/${uuid2}.jpg`,
|
|
373
|
+
});
|
|
374
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid3}/img3.jpg`, {
|
|
375
|
+
status: 200,
|
|
376
|
+
ok: true,
|
|
377
|
+
contentType: "image/jpeg",
|
|
378
|
+
bytesWritten: 1024,
|
|
379
|
+
actualPath: `assets/${uuid3}.jpg`,
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
384
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
385
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
Then("all three files should have updated references", () => {
|
|
389
|
+
const content1 = ctx!.filesystem.getFile(`${projectPath}/文章/file1.md`)?.content;
|
|
390
|
+
const content2 = ctx!.filesystem.getFile(`${projectPath}/文章/file2.md`)?.content;
|
|
391
|
+
const content3 = ctx!.filesystem.getFile(`${projectPath}/文章/file3.md`)?.content;
|
|
392
|
+
|
|
393
|
+
expect(content1).toContain(`![[${uuid1}.jpg]]`);
|
|
394
|
+
expect(content2).toContain(`![[${uuid2}.jpg]]`);
|
|
395
|
+
expect(content3).toContain(`![[${uuid3}.jpg]]`);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
And("all three files should be written to disk", () => {
|
|
399
|
+
// filesProcessed should be 3
|
|
400
|
+
expect(downloadResult!.filesProcessed).toBe(3);
|
|
401
|
+
ctx?.cleanup();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// Scenario: Early files are saved when later downloads fail
|
|
407
|
+
// ============================================================================
|
|
408
|
+
Scenario("Early files are saved when later downloads fail", ({ Given, When, Then, And }) => {
|
|
409
|
+
Given("a mock Tauri environment", () => {
|
|
410
|
+
ctx = setupMockTauri();
|
|
411
|
+
downloadResult = null;
|
|
412
|
+
expect(ctx).toBeDefined();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
And("an in-memory filesystem", () => {
|
|
416
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
And("three markdown files each containing a unique remote image", () => {
|
|
420
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/file1.md`, `---
|
|
421
|
+
title: "File 1"
|
|
422
|
+
---
|
|
423
|
+

|
|
424
|
+
`);
|
|
425
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/file2.md`, `---
|
|
426
|
+
title: "File 2"
|
|
427
|
+
---
|
|
428
|
+

|
|
429
|
+
`);
|
|
430
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/file3.md`, `---
|
|
431
|
+
title: "File 3"
|
|
432
|
+
---
|
|
433
|
+

|
|
434
|
+
`);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
And("the second file's download is configured to fail", () => {
|
|
438
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid1}/img1.jpg`, {
|
|
439
|
+
status: 200,
|
|
440
|
+
ok: true,
|
|
441
|
+
contentType: "image/jpeg",
|
|
442
|
+
bytesWritten: 1024,
|
|
443
|
+
actualPath: `assets/${uuid1}.jpg`,
|
|
444
|
+
});
|
|
445
|
+
// Second file's download fails with 404
|
|
446
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid2}/img2.jpg`, {
|
|
447
|
+
status: 404,
|
|
448
|
+
ok: false,
|
|
449
|
+
});
|
|
450
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid3}/img3.jpg`, {
|
|
451
|
+
status: 200,
|
|
452
|
+
ok: true,
|
|
453
|
+
contentType: "image/jpeg",
|
|
454
|
+
bytesWritten: 1024,
|
|
455
|
+
actualPath: `assets/${uuid3}.jpg`,
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
460
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
461
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
Then("the first file should have updated references and be written", () => {
|
|
465
|
+
const content1 = ctx!.filesystem.getFile(`${projectPath}/文章/file1.md`)?.content;
|
|
466
|
+
expect(content1).toContain(`![[${uuid1}.jpg]]`);
|
|
467
|
+
expect(content1).not.toContain("https://assets.matters.news");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
And("the second file should still have remote references", () => {
|
|
471
|
+
const content2 = ctx!.filesystem.getFile(`${projectPath}/文章/file2.md`)?.content;
|
|
472
|
+
// The remote URL should still be there since download failed
|
|
473
|
+
expect(content2).toContain(`https://assets.matters.news/embed/${uuid2}/img2.jpg`);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
And("the third file should have updated references and be written", () => {
|
|
477
|
+
const content3 = ctx!.filesystem.getFile(`${projectPath}/文章/file3.md`)?.content;
|
|
478
|
+
expect(content3).toContain(`![[${uuid3}.jpg]]`);
|
|
479
|
+
expect(content3).not.toContain("https://assets.matters.news");
|
|
480
|
+
ctx?.cleanup();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// Scenario: Write happens per-file not per-image
|
|
486
|
+
// ============================================================================
|
|
487
|
+
Scenario("Write happens per-file not per-image", ({ Given, When, Then, And }) => {
|
|
488
|
+
let writeCount = 0;
|
|
489
|
+
|
|
490
|
+
Given("a mock Tauri environment", () => {
|
|
491
|
+
ctx = setupMockTauri();
|
|
492
|
+
downloadResult = null;
|
|
493
|
+
writeCount = 0;
|
|
494
|
+
|
|
495
|
+
// Intercept writes to count them
|
|
496
|
+
const originalWriteProjectFile = (ctx!.filesystem as { _originalSetFile?: typeof ctx.filesystem.setFile })._originalSetFile
|
|
497
|
+
|| ctx!.filesystem.setFile.bind(ctx!.filesystem);
|
|
498
|
+
|
|
499
|
+
// Store original if not already stored
|
|
500
|
+
if (!(ctx!.filesystem as { _originalSetFile?: typeof ctx.filesystem.setFile })._originalSetFile) {
|
|
501
|
+
(ctx!.filesystem as { _originalSetFile?: typeof ctx.filesystem.setFile })._originalSetFile = originalWriteProjectFile;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Wrap setFile to count writes to our test file
|
|
505
|
+
const wrappedSetFile = (path: string, content: string) => {
|
|
506
|
+
if (path === `${projectPath}/文章/multi-image.md`) {
|
|
507
|
+
writeCount++;
|
|
508
|
+
}
|
|
509
|
+
return originalWriteProjectFile(path, content);
|
|
510
|
+
};
|
|
511
|
+
ctx!.filesystem.setFile = wrappedSetFile;
|
|
512
|
+
|
|
513
|
+
expect(ctx).toBeDefined();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
And("an in-memory filesystem", () => {
|
|
517
|
+
expect(ctx!.filesystem).toBeDefined();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
And("a markdown file with three remote images", () => {
|
|
521
|
+
ctx!.filesystem.setFile(`${projectPath}/文章/multi-image.md`, `---
|
|
522
|
+
title: "Multi Image"
|
|
523
|
+
---
|
|
524
|
+

|
|
525
|
+

|
|
526
|
+

|
|
527
|
+
`);
|
|
528
|
+
// Reset count after initial setup
|
|
529
|
+
writeCount = 0;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
And("all three downloads are configured to succeed", () => {
|
|
533
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid1}/img1.jpg`, {
|
|
534
|
+
status: 200,
|
|
535
|
+
ok: true,
|
|
536
|
+
contentType: "image/jpeg",
|
|
537
|
+
bytesWritten: 1024,
|
|
538
|
+
actualPath: `assets/${uuid1}.jpg`,
|
|
539
|
+
});
|
|
540
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid2}/img2.jpg`, {
|
|
541
|
+
status: 200,
|
|
542
|
+
ok: true,
|
|
543
|
+
contentType: "image/jpeg",
|
|
544
|
+
bytesWritten: 1024,
|
|
545
|
+
actualPath: `assets/${uuid2}.jpg`,
|
|
546
|
+
});
|
|
547
|
+
ctx!.urlConfig.setResponse(`https://assets.matters.news/embed/${uuid3}/img3.jpg`, {
|
|
548
|
+
status: 200,
|
|
549
|
+
ok: true,
|
|
550
|
+
contentType: "image/jpeg",
|
|
551
|
+
bytesWritten: 1024,
|
|
552
|
+
actualPath: `assets/${uuid3}.jpg`,
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
When("I run downloadMediaAndUpdate", async () => {
|
|
557
|
+
const { downloadMediaAndUpdate } = await import("../../src/downloader");
|
|
558
|
+
downloadResult = await downloadMediaAndUpdate();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
Then("the file should be written exactly once with all three references updated", () => {
|
|
562
|
+
// File should be written exactly once (after all images processed)
|
|
563
|
+
expect(writeCount).toBe(1);
|
|
564
|
+
|
|
565
|
+
// All three references should be updated
|
|
566
|
+
const content = ctx!.filesystem.getFile(`${projectPath}/文章/multi-image.md`)?.content;
|
|
567
|
+
expect(content).toContain(`![[${uuid1}.jpg]]`);
|
|
568
|
+
expect(content).toContain(`![[${uuid2}.jpg]]`);
|
|
569
|
+
expect(content).toContain(`![[${uuid3}.jpg]]`);
|
|
570
|
+
expect(content).not.toContain("https://assets.matters.news");
|
|
571
|
+
|
|
572
|
+
ctx?.cleanup();
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
});
|