@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +18 -0
  3. package/assets/icon.svg +1 -0
  4. package/assets/manifest.json +36 -0
  5. package/codegen.ts +26 -0
  6. package/e2e/moss-cli.test.ts +338 -0
  7. package/features/api/fetch-articles.feature +39 -0
  8. package/features/auth/wallet-auth.feature +27 -0
  9. package/features/download/retry-logic.feature +36 -0
  10. package/features/download/self-correcting.feature +83 -0
  11. package/features/download/worker-pool.feature +29 -0
  12. package/features/social/fetch-social-data.feature +40 -0
  13. package/features/steps/api.steps.ts +180 -0
  14. package/features/steps/download.steps.ts +423 -0
  15. package/features/steps/incremental-sync.steps.ts +105 -0
  16. package/features/steps/self-correcting.steps.ts +575 -0
  17. package/features/steps/social.steps.ts +257 -0
  18. package/features/steps/syndication.steps.ts +264 -0
  19. package/features/steps/wallet-auth.steps.ts +185 -0
  20. package/features/sync/article-sync.feature +49 -0
  21. package/features/sync/homepage-grid.feature +43 -0
  22. package/features/sync/incremental-sync.feature +28 -0
  23. package/features/syndication/create-draft.feature +35 -0
  24. package/package.json +58 -0
  25. package/src/__generated__/schema.graphql +4289 -0
  26. package/src/__generated__/types.ts +5355 -0
  27. package/src/__tests__/api.test.ts +678 -0
  28. package/src/__tests__/auth-route.test.ts +38 -0
  29. package/src/__tests__/auth-routing.test.ts +462 -0
  30. package/src/__tests__/auto-detect.test.ts +412 -0
  31. package/src/__tests__/binding-guard.test.ts +256 -0
  32. package/src/__tests__/config.test.ts +212 -0
  33. package/src/__tests__/converter.test.ts +289 -0
  34. package/src/__tests__/credential.test.ts +332 -0
  35. package/src/__tests__/domain.test.ts +341 -0
  36. package/src/__tests__/downloader.test.ts +679 -0
  37. package/src/__tests__/folder-detection.test.ts +289 -0
  38. package/src/__tests__/force-fresh-login.test.ts +236 -0
  39. package/src/__tests__/main.test.ts +2437 -0
  40. package/src/__tests__/progress.test.ts +93 -0
  41. package/src/__tests__/session.test.ts +375 -0
  42. package/src/__tests__/social-integration.test.ts +386 -0
  43. package/src/__tests__/social-sync-logic.test.ts +107 -0
  44. package/src/__tests__/social.test.ts +788 -0
  45. package/src/__tests__/sync.test.ts +1273 -0
  46. package/src/__tests__/syndication-toast-law.test.ts +649 -0
  47. package/src/__tests__/syndication.test.ts +125 -0
  48. package/src/__tests__/test-profile-escape.test.ts +209 -0
  49. package/src/__tests__/url-detect.test.ts +79 -0
  50. package/src/__tests__/utils.test.ts +226 -0
  51. package/src/api.ts +1366 -0
  52. package/src/auth-route.ts +38 -0
  53. package/src/config.ts +80 -0
  54. package/src/converter.ts +305 -0
  55. package/src/credential.ts +329 -0
  56. package/src/domain.ts +183 -0
  57. package/src/downloader.ts +761 -0
  58. package/src/main.ts +2092 -0
  59. package/src/progress.ts +89 -0
  60. package/src/queries/user.graphql +85 -0
  61. package/src/queries/viewer.graphql +104 -0
  62. package/src/social.ts +413 -0
  63. package/src/sync.ts +818 -0
  64. package/src/types.ts +477 -0
  65. package/src/url-detect.ts +49 -0
  66. package/src/utils.ts +305 -0
  67. package/test-fixtures/syndication-test-site/input/index.md +8 -0
  68. package/test-fixtures/syndication-test-site/input/posts/rich-test-article.md +90 -0
  69. package/test-helpers/TEST_ACCOUNT.md +151 -0
  70. package/test-helpers/api-client.ts +252 -0
  71. package/test-helpers/fixtures/articles.ts +147 -0
  72. package/test-helpers/wallet-auth.ts +305 -0
  73. package/test-setup/e2e.ts +93 -0
  74. package/tsconfig.json +23 -0
  75. package/vitest.config.ts +39 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ ## 1.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#738](https://github.com/Symbiosis-Lab/moss/pull/738) [`8539776`](https://github.com/Symbiosis-Lab/moss/commit/853977618a92b5d66853be8ca9558012b45183e5) Thanks [@guoliu](https://github.com/guoliu)! - First publish of the github and matters moss plugins to npm under the @symbiosis-lab scope. Sources consolidated into the moss monorepo; published from the changesets workflow. (The five other plugins originally listed here — douban, linkedin, substack, x, xiaohongshu — do not yet exist as packages and were removed so `changeset version` can resolve.)
8
+
9
+ All notable changes to this plugin are documented in this file.
10
+
11
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
12
+
13
+ ## [Unreleased]
14
+
15
+ _Pending publish — cumulative since `1.1.2` (last released on main); full detail under [1.4.0] and [1.2.0]._
16
+
17
+ - Changed (`1.4.1`): the login flow is quieter and recoverable — a cancelled or failed Matters login now returns you to the editor (empty-folder onboarding) instead of leaving an empty action panel, the login status label reads calmer, and the in-flight watchdog is preserved across the cancel.
18
+ - Fixed (`1.4.1`): a freshly imported vault's homepage title comes from the vault folder name, not the Matters display name.
19
+ - Fixed: the import progress bar no longer stalls during media download. Per-article sync and image-download progress now drive the unified progress surface, so the hairline advances smoothly through the heaviest phase. (These previously used a legacy progress channel that moss's panel drops for background imports, so the bar appeared frozen while images downloaded.)
20
+ - Changed: the sync receipt now leads with a noun and reads as one fact — e.g. "12 articles already up to date" instead of a bare, truncated "5 unchanged, images: 1 failed, 0 new comments". Image/link/comment outcomes no longer clutter it (zero-count clauses dropped), and a failed image download is surfaced as its own advisory carrying the image's **URL** (the dead CDN reference still in the body) so you can see which image broke, rather than an opaque "1 failed" count.
21
+ - Fixed: section headings no longer show a stray `#` on Matters. moss appends a hover-only `<a class="moss-heading-anchor">#</a>` permalink to every heading; Matters' sanitizer kept the `#` text, so headings synced as e.g. "1.#". The anchor (web-only chrome) is now stripped during syndication. Verified against `server.matters.icu`.
22
+ - Fixed: comments now download for articles syndicated with a Matters **short-link** URL (`https://matters.town/a/<shortHash>`), not only the canonical `https://matters.town/@user/<slug>-<shortHash>` form. Previously `extractShortHash` required a hyphen in the final path segment, so short-link articles were silently dropped from `scanLocalArticles` and never fetched comments. The two duplicate `extractShortHash` implementations (sync + downloader) are now one shared function in `domain.ts` that understands both forms; an unparseable syndicated URL is now logged instead of dropped silently.
23
+ - Fixed: images, covers, and audio now upload to Matters correctly. Assets are uploaded by **bytes** read from the local build output (multipart `singleFileUpload`), not by URL. Matters' server cannot reliably fetch assets by URL from a deployed site (Caddy/moss-seta hosts return `UNABLE_TO_UPLOAD_FROM_URL`), and `embedaudio` rejects url-upload entirely — so previously images often broke and audio never uploaded. On upload failure, image/audio srcs fall back to the absolutized deployed URL so they still display. Verified end-to-end against `server.matters.icu`.
24
+ - Fixed: audio embeds now syndicate to Matters at all. moss's `<audio class="moss-embed">` is rewritten into Matters' required `<figure class="audio">` shape (the only shape its sanitizer keeps; previously the entire `<audio>` was stripped to stray fallback text), then the audio bytes are uploaded via `embedaudio`.
25
+ - Local-first comments: `uid` contract, env-derived Artalk server URL, tombstone reconcile, morph-proof preview stub.
26
+ - Social data written to `.moss/data/social/`; stranded-comment recovery.
27
+ - Session-expiry handling: JWT decode, tri-state session, dead-token filter, trigger-aware auth routing, honest receipts.
28
+
29
+ ## [1.4.0] - 2026-06-11
30
+
31
+ ### Added
32
+
33
+ - Local-first comments integration: `uid` contract, env-derived Artalk server URL,
34
+ tombstone reconcile, morph-proof preview stub.
35
+ - Social data written to `.moss/data/social/` alongside build; recovers stranded comments.
36
+ - Diagnostic advisories on hook-failure with full refetch on cleared counts.
37
+
38
+ ### Fixed
39
+
40
+ - Full refetch on cleared platform counts to avoid stale display.
41
+
42
+ ## [1.2.0] - 2026-06-04
43
+
44
+ ### Added
45
+
46
+ - Session-expiry handling across 7 implementation tasks (T1–T7):
47
+ - Decode JWT `exp` claim locally (T1).
48
+ - Tri-state session check with dead-token filtering and persisted nudge stamp (T2).
49
+ - Typed `MattersAuthError` from response bodies (T3).
50
+ - Pure trigger-aware auth router (T4).
51
+ - Trigger-aware auth routing, gated binding guard, honest receipts (T5).
52
+ - Mid-sync auth failure copy and tri-state syndicate gate (T6).
53
+ - Review fixes: cookie dead-token filter, `queryMode` reset, receipt copy (T6.5).
54
+ - Reconciled plugin version after rebase (T7).
55
+ - `MOSS_MATTERS_DOMAIN` env var to switch test/prod domain in-webview.
56
+ - `MOSS_MATTERS_TEST_PROFILE` env var bypasses login in test builds.
57
+ - moss-injected trigger context; terminated leaked background tasks.
58
+
59
+ ## [1.1.2] - 2026-05-30
60
+
61
+ ### Changed
62
+
63
+ - Reconcile plugin manifest after QuickJS runtime upgrade in moss v0.7.x.
64
+
65
+ ## [1.1.1] - 2026-05-29
66
+
67
+ ### Fixed
68
+
69
+ - Rebuild bundled plugin correctly on release build (`cargo build --release`).
70
+ - Language-aware article folder (文章 for Chinese content).
71
+ - Convert HTML via moss's shared htmd converter.
72
+ - Emit filename-only wikilinks for assets.
73
+ - Trim tag whitespace; localize legacy non-UUID assets.
74
+ - Pre-merge review fixes: G runtime no-op, title wikilink, hairline clip.
75
+
76
+ ### Added
77
+
78
+ - Generate self-named home article with `home: true` marker.
79
+ - Marker-aware home detection exposed to plugin-facing API.
80
+
81
+ ## [0.0.2] - 2026-05-29
82
+
83
+ > **Note:** This npm publication was experimental. The plugin version lineage returned to
84
+ > 1.x after the open-source consolidation; `0.0.2` is documented here for history only
85
+ > and was never successfully published to npm.
86
+
87
+ Initial publication attempt under the `@symbiosis-lab` scope, bundled with the
88
+ open-source release pipeline.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @symbiosis-lab/moss-plugin-matters
2
+
3
+ > Publish moss posts to matters.town.
4
+
5
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
6
+ [![status](https://img.shields.io/badge/status-experimental-orange)](../#stability)
7
+
8
+ > **Read-only mirror.** Source lives in the private moss monorepo. PRs cannot be merged in the mirror — see [CONTRIBUTING.md](../CONTRIBUTING.md).
9
+
10
+ A moss publishing plugin for matters.town. See [moss.pub](https://mosspub.com) and the [plugin index](../README.md) for the full plugin lineup.
11
+
12
+ ## Stability
13
+
14
+ This plugin is 0.x. APIs may change between minor versions until 1.0. See [CHANGELOG.md](../CHANGELOG.md).
15
+
16
+ ## License
17
+
18
+ MIT — see [LICENSE](../LICENSE).
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg id="_圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 385.45 265"><defs><style>.cls-1{fill:url(#_未命名漸層_4);}.cls-1,.cls-2{fill-rule:evenodd;}.cls-2{fill:url(#_未命名漸層_3);}</style><linearGradient id="_未命名漸層_4" x1="181.63" y1="-2654.34" x2="358.94" y2="-2793.05" gradientTransform="translate(0 -2587.71) scale(1 -1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d7eae1"/><stop offset="1" stop-color="#79b1a6"/></linearGradient><linearGradient id="_未命名漸層_3" x1="62.75" y1="-2647.18" x2="215.96" y2="-2806.64" gradientTransform="translate(0 -2587.71) scale(1 -1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f4e2bc"/><stop offset="1" stop-color="#bf9f5e"/></linearGradient></defs><path class="cls-1" d="M265.83,251.75c66.07,0,119.62-53.39,119.62-119.25S331.9,13.25,265.83,13.25s-119.62,53.39-119.62,119.25,53.55,119.25,119.62,119.25Z"/><path class="cls-2" d="M132.92,265c73.41,0,132.92-59.32,132.92-132.5S206.33,0,132.92,0,0,59.32,0,132.5s59.5,132.5,132.92,132.5Z"/></svg>
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "matters",
3
+ "version": "1.4.2",
4
+ "description": "Syndicate your articles to Matters.town (POSSE)",
5
+ "author": "moss team",
6
+ "entry": "main.bundle.js",
7
+ "capabilities": [
8
+ "process",
9
+ "syndicate",
10
+ "import"
11
+ ],
12
+ "global_name": "MattersPlugin",
13
+ "domain": "matters.town",
14
+ "domains": ["matters.town", "matters.icu"],
15
+ "icon": "icon.svg",
16
+ "config": {
17
+ "auto_publish": false,
18
+ "add_canonical_link": true,
19
+ "sync_on_build": true,
20
+ "sync_drafts": true
21
+ },
22
+ "config_schema": {
23
+ "auto_publish": "boolean",
24
+ "add_canonical_link": "boolean",
25
+ "sync_on_build": "boolean",
26
+ "sync_drafts": "boolean"
27
+ },
28
+ "contributes": {
29
+ "jobs": {
30
+ "syndicate": {
31
+ "verb": "Syndicated",
32
+ "noun": "posts"
33
+ }
34
+ }
35
+ }
36
+ }
package/codegen.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { CodegenConfig } from "@graphql-codegen/cli";
2
+
3
+ const config: CodegenConfig = {
4
+ // Fetch schema via introspection from live endpoint
5
+ schema: "https://server.matters.town/graphql",
6
+ documents: ["src/queries/**/*.graphql"],
7
+ generates: {
8
+ // Generate TypeScript types from schema and operations
9
+ "./src/__generated__/types.ts": {
10
+ plugins: ["typescript", "typescript-operations"],
11
+ config: {
12
+ enumsAsTypes: true,
13
+ preResolveTypes: true,
14
+ skipTypename: true,
15
+ // Use 'Maybe' for nullable fields
16
+ maybeValue: "T | null | undefined",
17
+ },
18
+ },
19
+ // Save introspected schema locally for reference
20
+ "./src/__generated__/schema.graphql": {
21
+ plugins: ["schema-ast"],
22
+ },
23
+ },
24
+ };
25
+
26
+ export default config;
@@ -0,0 +1,338 @@
1
+ /**
2
+ * E2E Tests for Matters Plugin using moss CLI
3
+ *
4
+ * These tests verify the Matters plugin works correctly when invoked through
5
+ * the moss CLI, testing real-world scenarios.
6
+ *
7
+ * The Matters plugin has capabilities:
8
+ * - process: Syncs articles from Matters.town during build
9
+ * - syndicate: Publishes articles to Matters.town after deploy
10
+ *
11
+ * Requirements:
12
+ * - moss binary with --wait-plugins support (v0.3.1+)
13
+ * - Plugin built (npm run build)
14
+ * - Display server available (xvfb-run on Linux CI)
15
+ *
16
+ * Note: Tests verify graceful handling when not authenticated.
17
+ * Full integration tests with real Matters API require authentication.
18
+ */
19
+
20
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
21
+ import { execSync, spawn } from "child_process";
22
+ import * as fs from "fs";
23
+ import * as path from "path";
24
+ import * as os from "os";
25
+
26
+ // Path to moss binary - check env var first, then fallback to local dev path
27
+ const MOSS_BINARY =
28
+ process.env.MOSS_BINARY ||
29
+ path.join(__dirname, "../../../../moss/develop/src-tauri/target/debug/moss");
30
+
31
+ // Check if --wait-plugins is supported (v0.3.1+)
32
+ let HAS_WAIT_PLUGINS = false;
33
+
34
+ // Path to plugin dist
35
+ const PLUGIN_DIST = path.join(__dirname, "../dist");
36
+
37
+ // Test fixture directory
38
+ let testDir: string;
39
+ let fixtureCounter = 0;
40
+
41
+ /**
42
+ * Create a test fixture directory with optional configuration
43
+ */
44
+ function createFixture(options: {
45
+ withGit?: boolean;
46
+ withRemote?: string;
47
+ withPlugin?: boolean;
48
+ withMattersConfig?: Record<string, unknown>;
49
+ content?: Record<string, string>;
50
+ }): string {
51
+ const fixtureName = `moss-matters-e2e-${Date.now()}-${fixtureCounter++}`;
52
+ const fixturePath = path.join(testDir, fixtureName);
53
+ fs.mkdirSync(fixturePath, { recursive: true });
54
+
55
+ // Create content files
56
+ const defaultContent = {
57
+ "index.md": "# Hello World\n\nThis is a test site.",
58
+ "posts/article1.md": `---
59
+ title: Test Article
60
+ date: 2024-01-01
61
+ tags:
62
+ - test
63
+ ---
64
+
65
+ # Test Article
66
+
67
+ This is a test article for Matters plugin testing.
68
+ `,
69
+ };
70
+
71
+ const content = options.content || defaultContent;
72
+ for (const [filename, fileContent] of Object.entries(content)) {
73
+ const filePath = path.join(fixturePath, filename);
74
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
75
+ fs.writeFileSync(filePath, fileContent);
76
+ }
77
+
78
+ // Initialize git if requested
79
+ if (options.withGit) {
80
+ execSync("git init", { cwd: fixturePath, stdio: "pipe" });
81
+ execSync("git config user.email 'test@example.com'", {
82
+ cwd: fixturePath,
83
+ stdio: "pipe",
84
+ });
85
+ execSync("git config user.name 'Test User'", {
86
+ cwd: fixturePath,
87
+ stdio: "pipe",
88
+ });
89
+ execSync("git add .", { cwd: fixturePath, stdio: "pipe" });
90
+ execSync('git commit -m "Initial commit"', {
91
+ cwd: fixturePath,
92
+ stdio: "pipe",
93
+ });
94
+
95
+ if (options.withRemote) {
96
+ execSync(`git remote add origin ${options.withRemote}`, {
97
+ cwd: fixturePath,
98
+ stdio: "pipe",
99
+ });
100
+ }
101
+ }
102
+
103
+ // Install plugin if requested
104
+ if (options.withPlugin) {
105
+ const mossDir = path.join(fixturePath, ".moss");
106
+ const pluginsDir = path.join(mossDir, "plugins");
107
+ const mattersPluginDir = path.join(pluginsDir, "matters");
108
+
109
+ fs.mkdirSync(mattersPluginDir, { recursive: true });
110
+
111
+ // Copy plugin files
112
+ const pluginFiles = ["main.bundle.js", "manifest.json", "icon.svg"];
113
+ for (const file of pluginFiles) {
114
+ const src = path.join(PLUGIN_DIST, file);
115
+ const dest = path.join(mattersPluginDir, file);
116
+ if (fs.existsSync(src)) {
117
+ fs.copyFileSync(src, dest);
118
+ }
119
+ }
120
+
121
+ // Write plugin config if provided
122
+ if (options.withMattersConfig) {
123
+ const configPath = path.join(mattersPluginDir, "config.json");
124
+ fs.writeFileSync(
125
+ configPath,
126
+ JSON.stringify(options.withMattersConfig, null, 2)
127
+ );
128
+ }
129
+ }
130
+
131
+ return fixturePath;
132
+ }
133
+
134
+ /**
135
+ * Run moss CLI and return stdout/stderr
136
+ */
137
+ function runMoss(
138
+ args: string[],
139
+ options?: { cwd?: string; timeout?: number }
140
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
141
+ return new Promise((resolve) => {
142
+ const proc = spawn(MOSS_BINARY, args, {
143
+ cwd: options?.cwd || testDir,
144
+ timeout: options?.timeout || 30000,
145
+ env: {
146
+ ...process.env,
147
+ CI: "true",
148
+ },
149
+ });
150
+
151
+ let stdout = "";
152
+ let stderr = "";
153
+
154
+ proc.stdout?.on("data", (data) => {
155
+ stdout += data.toString();
156
+ });
157
+
158
+ proc.stderr?.on("data", (data) => {
159
+ stderr += data.toString();
160
+ });
161
+
162
+ proc.on("close", (code) => {
163
+ resolve({ stdout, stderr, code: code ?? 1 });
164
+ });
165
+
166
+ proc.on("error", (err) => {
167
+ stderr += err.message;
168
+ resolve({ stdout, stderr, code: 1 });
169
+ });
170
+ });
171
+ }
172
+
173
+ describe("Matters Plugin E2E Tests", () => {
174
+ beforeAll(async () => {
175
+ // Verify moss binary exists
176
+ if (!fs.existsSync(MOSS_BINARY)) {
177
+ throw new Error(
178
+ `moss binary not found at ${MOSS_BINARY}. ` +
179
+ `Set MOSS_BINARY environment variable or build moss locally.`
180
+ );
181
+ }
182
+
183
+ // Verify plugin dist exists
184
+ if (!fs.existsSync(PLUGIN_DIST)) {
185
+ throw new Error(
186
+ `Plugin dist not found at ${PLUGIN_DIST}. Run 'npm run build' first.`
187
+ );
188
+ }
189
+
190
+ // Create temp directory
191
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), "moss-matters-e2e-"));
192
+
193
+ // Check if --wait-plugins is supported
194
+ const { stdout } = await runMoss(["--help"]);
195
+ HAS_WAIT_PLUGINS = stdout.includes("--wait-plugins");
196
+ });
197
+
198
+ afterAll(() => {
199
+ if (testDir && fs.existsSync(testDir)) {
200
+ fs.rmSync(testDir, { recursive: true, force: true });
201
+ }
202
+ });
203
+
204
+ describe("Plugin discovery", () => {
205
+ it("has correct manifest structure", () => {
206
+ const fixture = createFixture({ withPlugin: true });
207
+
208
+ const manifestPath = path.join(
209
+ fixture,
210
+ ".moss",
211
+ "plugins",
212
+ "matters",
213
+ "manifest.json"
214
+ );
215
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
216
+
217
+ expect(manifest.name).toBe("matters");
218
+ expect(manifest.capabilities).toContain("process");
219
+ expect(manifest.capabilities).toContain("syndicate");
220
+ expect(manifest.domain).toBe("matters.town");
221
+ expect(manifest.entry).toBe("main.bundle.js");
222
+ });
223
+
224
+ it("plugin files are copied correctly", () => {
225
+ const fixture = createFixture({ withPlugin: true });
226
+
227
+ const pluginDir = path.join(fixture, ".moss", "plugins", "matters");
228
+ expect(fs.existsSync(path.join(pluginDir, "manifest.json"))).toBe(true);
229
+ expect(fs.existsSync(path.join(pluginDir, "main.bundle.js"))).toBe(true);
230
+ });
231
+ });
232
+
233
+ /**
234
+ * Process Hook Tests
235
+ *
236
+ * The process hook syncs articles from Matters.town.
237
+ * Without authentication, it should gracefully handle the missing auth
238
+ * and continue with the build (or show auth prompt).
239
+ */
240
+ describe("Process hook (with --wait-plugins)", () => {
241
+ it.skipIf(!HAS_WAIT_PLUGINS)(
242
+ "builds with matters plugin - handles missing auth gracefully",
243
+ async () => {
244
+ const fixture = createFixture({
245
+ withPlugin: true,
246
+ content: {
247
+ "index.md": "# My Blog\n\nWelcome!",
248
+ "posts/hello.md": `---
249
+ title: Hello World
250
+ date: 2024-01-01
251
+ ---
252
+
253
+ Hello from my blog!
254
+ `,
255
+ },
256
+ });
257
+
258
+ // Build with plugins - process hook will run
259
+ const { stdout, stderr, code } = await runMoss(
260
+ ["build", fixture, "--wait-plugins"],
261
+ { timeout: 60000 }
262
+ );
263
+
264
+ const output = stdout + stderr;
265
+
266
+ // Compilation should succeed (plugin handles auth gracefully)
267
+ // The site should be generated regardless of Matters sync status
268
+ expect(code).toBe(0);
269
+
270
+ // Site should be created
271
+ const siteDir = path.join(fixture, ".moss", "site");
272
+ expect(fs.existsSync(siteDir)).toBe(true);
273
+
274
+ // Output should mention Matters or process hook activity
275
+ // Could be auth prompt, sync status, or graceful skip
276
+ // The exact message depends on plugin behavior
277
+ expect(output).toMatch(/matters|process|sync|compil/i);
278
+ }
279
+ );
280
+
281
+ it.skipIf(!HAS_WAIT_PLUGINS)(
282
+ "shows waiting messages for process hook",
283
+ async () => {
284
+ const fixture = createFixture({
285
+ withPlugin: true,
286
+ content: {
287
+ "index.md": "# Test\n\nContent",
288
+ },
289
+ });
290
+
291
+ const { stdout, stderr, code } = await runMoss(
292
+ ["build", fixture, "--wait-plugins"],
293
+ { timeout: 60000 }
294
+ );
295
+
296
+ const output = stdout + stderr;
297
+
298
+ // Should show hook waiting messages (from --wait-plugins)
299
+ expect(output).toMatch(/waiting|hook|before_build|process/i);
300
+ }
301
+ );
302
+ });
303
+
304
+ /**
305
+ * Build Integration Tests
306
+ *
307
+ * Tests that the plugin doesn't break normal build flow.
308
+ */
309
+ describe("Build integration", () => {
310
+ it.skipIf(!HAS_WAIT_PLUGINS)(
311
+ "build completes even when matters sync fails",
312
+ async () => {
313
+ const fixture = createFixture({
314
+ withPlugin: true,
315
+ withMattersConfig: {
316
+ // No username configured - sync will fail gracefully
317
+ sync_on_build: true,
318
+ },
319
+ content: {
320
+ "index.md": "# Blog\n\nWelcome to my blog.",
321
+ },
322
+ });
323
+
324
+ const { stdout, stderr, code } = await runMoss(
325
+ ["build", fixture, "--wait-plugins"],
326
+ { timeout: 60000 }
327
+ );
328
+
329
+ // Build should still complete successfully
330
+ expect(code).toBe(0);
331
+
332
+ // Site should be generated
333
+ const siteDir = path.join(fixture, ".moss", "site");
334
+ expect(fs.existsSync(siteDir)).toBe(true);
335
+ }
336
+ );
337
+ });
338
+ });
@@ -0,0 +1,39 @@
1
+ Feature: Fetch Articles from Matters API
2
+ As a plugin
3
+ I want to fetch articles via GraphQL
4
+ So that I can sync content locally
5
+
6
+ # Note: Uses matters.icu test environment with user "yhh354" (has articles)
7
+ # Can override with MATTERS_TEST_USER env var
8
+
9
+ @e2e @real-api
10
+ Scenario: Fetch public user articles
11
+ Given the matters.icu test environment
12
+ When I query articles for user "yhh354"
13
+ Then I should receive a list of articles
14
+ And each article should have id, title, shortHash, and content
15
+
16
+ @e2e @real-api
17
+ Scenario: Handle pagination for users with many articles
18
+ Given the matters.icu test environment
19
+ When I fetch all articles for user "yhh354" with pagination
20
+ Then I should receive all articles across multiple pages
21
+ And all articles should have unique shortHashes
22
+
23
+ @e2e @real-api
24
+ Scenario: Fetch user profile
25
+ Given the matters.icu test environment
26
+ When I query profile for user "yhh354"
27
+ Then I should receive profile with userName and displayName
28
+
29
+ @e2e @real-api
30
+ Scenario: Fetch user collections
31
+ Given the matters.icu test environment
32
+ When I query collections for user "yhh354"
33
+ Then I should receive a list of collections or empty list
34
+
35
+ @e2e @real-api
36
+ Scenario: Handle non-existent user gracefully
37
+ Given the matters.icu test environment
38
+ When I query articles for user "nonexistent_user_xyz_12345"
39
+ Then the query should return null user
@@ -0,0 +1,27 @@
1
+ @e2e @real-api
2
+ Feature: Wallet Authentication
3
+ As a developer, I want to authenticate with Ethereum wallet
4
+ So that I can run e2e tests without email verification
5
+
6
+ Background:
7
+ Given I am using the Matters test environment
8
+
9
+ Scenario: Login with valid wallet signature
10
+ Given I have a valid Ethereum private key
11
+ When I complete the wallet login flow
12
+ Then I should receive an auth token
13
+ And I should receive my user info
14
+ And the type should be "Login" or "Signup"
15
+
16
+ Scenario: Generate signing message
17
+ Given I have a valid Ethereum address
18
+ When I request a signing message for login
19
+ Then I should receive a nonce
20
+ And I should receive a signingMessage
21
+ And the message should contain the address
22
+
23
+ Scenario: Create authenticated client
24
+ Given I have completed wallet login
25
+ And I have an auth token
26
+ When I create an authenticated client
27
+ Then the client should be able to make authenticated requests
@@ -0,0 +1,36 @@
1
+ Feature: Download Retry Logic
2
+ As a developer
3
+ I want downloads to retry on transient failures
4
+ So that temporary issues don't cause permanent failures
5
+
6
+ Scenario: Retries with Fibonacci backoff on 503
7
+ Given a mock Tauri environment
8
+ Given an in-memory filesystem
9
+ Given an image URL that returns 503 twice then succeeds
10
+ When I download the image with retry enabled
11
+ Then it should retry with Fibonacci delays
12
+ And the download should succeed on attempt 3
13
+
14
+ Scenario: Gives up after max retries
15
+ Given a mock Tauri environment
16
+ Given an in-memory filesystem
17
+ Given an image URL that always returns 503
18
+ When I download the image with max 3 retries
19
+ Then it should attempt 4 times total
20
+ And the download should fail with 503 error
21
+
22
+ Scenario: Does not retry on 404
23
+ Given a mock Tauri environment
24
+ Given an in-memory filesystem
25
+ Given an image URL that returns 404
26
+ When I download the image with retry enabled
27
+ Then it should not retry
28
+ And the download should fail immediately with 404 error
29
+
30
+ Scenario: Retries on network timeout
31
+ Given a mock Tauri environment
32
+ Given an in-memory filesystem
33
+ Given an image URL that times out twice then succeeds
34
+ When I download the image with retry enabled
35
+ Then it should retry after timeouts
36
+ And the download should succeed on attempt 3