@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
@@ -0,0 +1,83 @@
1
+ Feature: Self-correcting reference updates
2
+ As a Matters user
3
+ I want image references to be automatically updated after download
4
+ So that my markdown files reference local assets correctly
5
+
6
+ Scenario: Updates references when assets already exist
7
+ Given a mock Tauri environment
8
+ And an in-memory filesystem
9
+ And a markdown file with remote image URLs
10
+ And the assets already exist locally
11
+ When I run downloadMediaAndUpdate
12
+ Then no downloads should occur
13
+ And all image references should be updated to local paths
14
+
15
+ Scenario: Downloads and updates in single pass
16
+ Given a mock Tauri environment
17
+ And an in-memory filesystem
18
+ And a markdown file with remote image URLs
19
+ And no assets exist locally
20
+ When I run downloadMediaAndUpdate
21
+ Then all images should be downloaded
22
+ And all image references should be updated to local paths
23
+
24
+ Scenario: Resumes correctly after interruption
25
+ Given a mock Tauri environment
26
+ And an in-memory filesystem
27
+ And a markdown file with multiple remote image URLs
28
+ And some assets already exist locally
29
+ When I run downloadMediaAndUpdate
30
+ Then only missing assets should be downloaded
31
+ And all image references should be updated to local paths
32
+
33
+ Scenario: Handles cross-CDN URLs with same UUID
34
+ Given a mock Tauri environment
35
+ And an in-memory filesystem
36
+ And a markdown file with cover and body images using different CDNs
37
+ And both URLs contain the same UUID
38
+ When I run downloadMediaAndUpdate
39
+ Then only one download should occur
40
+ And both cover and body references should point to the same local file
41
+
42
+ Scenario: Idempotent operation
43
+ Given a mock Tauri environment
44
+ And an in-memory filesystem
45
+ And a markdown file with local image references
46
+ And all assets exist locally
47
+ When I run downloadMediaAndUpdate twice
48
+ Then no downloads should occur
49
+ And the file should not be modified
50
+
51
+ # ============================================================================
52
+ # Incremental Write Behavior Tests
53
+ # These tests verify the core design principle: files are written immediately
54
+ # after processing, not batched at the end. This ensures partial progress is
55
+ # saved if the process is interrupted.
56
+ # ============================================================================
57
+
58
+ Scenario: Files are written immediately after processing (not batched)
59
+ Given a mock Tauri environment
60
+ And an in-memory filesystem
61
+ And three markdown files each containing a unique remote image
62
+ And downloads are configured to succeed for all files
63
+ When I run downloadMediaAndUpdate
64
+ Then all three files should have updated references
65
+ And all three files should be written to disk
66
+
67
+ Scenario: Early files are saved when later downloads fail
68
+ Given a mock Tauri environment
69
+ And an in-memory filesystem
70
+ And three markdown files each containing a unique remote image
71
+ And the second file's download is configured to fail
72
+ When I run downloadMediaAndUpdate
73
+ Then the first file should have updated references and be written
74
+ And the second file should still have remote references
75
+ And the third file should have updated references and be written
76
+
77
+ Scenario: Write happens per-file not per-image
78
+ Given a mock Tauri environment
79
+ And an in-memory filesystem
80
+ And a markdown file with three remote images
81
+ And all three downloads are configured to succeed
82
+ When I run downloadMediaAndUpdate
83
+ Then the file should be written exactly once with all three references updated
@@ -0,0 +1,29 @@
1
+ Feature: Worker Pool Concurrency
2
+ As a developer
3
+ I want downloads to respect concurrency limits
4
+ So that we don't overwhelm the server
5
+
6
+ Scenario: Respects concurrency limit of 5
7
+ Given a mock Tauri environment
8
+ Given an in-memory filesystem
9
+ Given 20 images to download with delay
10
+ When I start downloading all images
11
+ Then at most 5 downloads should run concurrently
12
+ And all 20 downloads should complete successfully
13
+
14
+ Scenario: Tracks download progress
15
+ Given a mock Tauri environment
16
+ Given an in-memory filesystem
17
+ Given 10 images to download
18
+ When I start downloading all images
19
+ Then progress events should be reported
20
+ And the final progress should show all images completed
21
+
22
+ Scenario: Handles mixed success and failure
23
+ Given a mock Tauri environment
24
+ Given an in-memory filesystem
25
+ Given 5 images where 2 will fail with 404
26
+ When I start downloading all images
27
+ Then 3 downloads should succeed
28
+ And 2 downloads should be marked as failed
29
+ And the result should report both successes and failures
@@ -0,0 +1,40 @@
1
+ @e2e @real-api
2
+ Feature: Fetch Social Data
3
+ As a user, I want to download comments, donations, and appreciations
4
+ So that I can display them on my static site
5
+
6
+ Background:
7
+ Given I am using the Matters test environment
8
+
9
+ Scenario: Fetch comments for an article
10
+ Given I have a test article shortHash
11
+ When I fetch comments for the article
12
+ Then I should receive an array of comments
13
+ And each comment should have id, content, createdAt, and author
14
+
15
+ Scenario: Fetch donations for an article
16
+ Given I have a test article shortHash
17
+ When I fetch donations for the article
18
+ Then I should receive an array of donations
19
+ And each donation should have id and sender details
20
+
21
+ Scenario: Fetch appreciations for an article
22
+ Given I have a test article shortHash
23
+ When I fetch appreciations for the article
24
+ Then I should receive an array of appreciations
25
+ And each appreciation should have amount, createdAt, and sender
26
+
27
+ Scenario: Save social data to .moss/social/matters.json
28
+ Given I have fetched social data for an article
29
+ When I save the social data
30
+ Then the file .moss/social/matters.json should exist
31
+ And it should contain the schemaVersion "1.0.0"
32
+ And it should contain data for the article shortHash
33
+
34
+ Scenario: Merge new social data with existing
35
+ Given I have existing social data for an article
36
+ And I fetch new social data
37
+ When I merge the social data
38
+ Then new items should be added
39
+ And existing items should be preserved
40
+ And no items should be duplicated
@@ -0,0 +1,180 @@
1
+ import { loadFeature, describeFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import {
4
+ apiConfig,
5
+ graphqlQueryPublic,
6
+ USER_ARTICLES_QUERY,
7
+ USER_COLLECTIONS_QUERY,
8
+ } from "../../src/api";
9
+ import type {
10
+ UserArticlesQuery,
11
+ UserCollectionsQuery,
12
+ UserProfileQuery,
13
+ } from "../../src/__generated__/types";
14
+
15
+ const feature = await loadFeature("features/api/fetch-articles.feature");
16
+
17
+ describeFeature(feature, ({ Scenario }) => {
18
+ // Test state
19
+ let articlesResult: UserArticlesQuery | null = null;
20
+ let profileResult: UserProfileQuery | null = null;
21
+ let collectionsResult: UserCollectionsQuery | null = null;
22
+ let allArticles: NonNullable<NonNullable<UserArticlesQuery["user"]>["articles"]["edges"]> = [];
23
+ let queryError: Error | null = null;
24
+
25
+ Scenario("Fetch public user articles", ({ Given, When, Then, And }) => {
26
+ Given("the matters.icu test environment", () => {
27
+ apiConfig.endpoint = "https://server.matters.icu/graphql";
28
+ apiConfig.queryMode = "user";
29
+ });
30
+
31
+ When("I query articles for user {string}", async (_ctx, userName: string) => {
32
+ try {
33
+ articlesResult = await graphqlQueryPublic<UserArticlesQuery>(
34
+ USER_ARTICLES_QUERY,
35
+ { userName }
36
+ );
37
+ queryError = null;
38
+ } catch (error) {
39
+ queryError = error as Error;
40
+ articlesResult = null;
41
+ }
42
+ });
43
+
44
+ Then("I should receive a list of articles", () => {
45
+ expect(articlesResult).not.toBeNull();
46
+ expect(articlesResult?.user).not.toBeNull();
47
+ expect(articlesResult?.user?.articles.edges).toBeDefined();
48
+ expect(articlesResult?.user?.articles.edges?.length).toBeGreaterThan(0);
49
+ });
50
+
51
+ And("each article should have id, title, shortHash, and content", () => {
52
+ const edges = articlesResult?.user?.articles.edges ?? [];
53
+ for (const edge of edges) {
54
+ expect(edge.node.id).toBeDefined();
55
+ expect(edge.node.title).toBeDefined();
56
+ expect(edge.node.shortHash).toBeDefined();
57
+ expect(edge.node.content).toBeDefined();
58
+ }
59
+ });
60
+ });
61
+
62
+ Scenario("Handle pagination for users with many articles", ({ Given, When, Then, And }) => {
63
+ Given("the matters.icu test environment", () => {
64
+ apiConfig.endpoint = "https://server.matters.icu/graphql";
65
+ apiConfig.queryMode = "user";
66
+ });
67
+
68
+ When("I fetch all articles for user {string} with pagination", async (_ctx, userName: string) => {
69
+ allArticles = [];
70
+ let cursor: string | undefined;
71
+
72
+ do {
73
+ const data = await graphqlQueryPublic<UserArticlesQuery>(
74
+ USER_ARTICLES_QUERY,
75
+ { userName, after: cursor }
76
+ );
77
+
78
+ if (!data.user) break;
79
+
80
+ const edges = data.user.articles.edges ?? [];
81
+ allArticles.push(...edges);
82
+
83
+ cursor = data.user.articles.pageInfo.hasNextPage
84
+ ? (data.user.articles.pageInfo.endCursor ?? undefined)
85
+ : undefined;
86
+ } while (cursor);
87
+ });
88
+
89
+ Then("I should receive all articles across multiple pages", () => {
90
+ expect(allArticles.length).toBeGreaterThan(0);
91
+ });
92
+
93
+ And("all articles should have unique shortHashes", () => {
94
+ const shortHashes = allArticles.map((e) => e.node.shortHash);
95
+ const uniqueHashes = new Set(shortHashes);
96
+ expect(uniqueHashes.size).toBe(shortHashes.length);
97
+ });
98
+ });
99
+
100
+ Scenario("Fetch user profile", ({ Given, When, Then }) => {
101
+ Given("the matters.icu test environment", () => {
102
+ apiConfig.endpoint = "https://server.matters.icu/graphql";
103
+ apiConfig.queryMode = "user";
104
+ });
105
+
106
+ // Use inline query without settings field (settings is private and requires auth)
107
+ When("I query profile for user {string}", async (_ctx, userName: string) => {
108
+ const PUBLIC_PROFILE_QUERY = `
109
+ query UserProfile($userName: String!) {
110
+ user(input: { userName: $userName }) {
111
+ id
112
+ userName
113
+ displayName
114
+ info {
115
+ description
116
+ profileCover
117
+ }
118
+ avatar
119
+ }
120
+ }
121
+ `;
122
+ profileResult = await graphqlQueryPublic<UserProfileQuery>(
123
+ PUBLIC_PROFILE_QUERY,
124
+ { userName }
125
+ );
126
+ });
127
+
128
+ Then("I should receive profile with userName and displayName", () => {
129
+ expect(profileResult?.user).not.toBeNull();
130
+ expect(profileResult?.user?.userName).toBeDefined();
131
+ expect(profileResult?.user?.displayName).toBeDefined();
132
+ });
133
+ });
134
+
135
+ Scenario("Fetch user collections", ({ Given, When, Then }) => {
136
+ Given("the matters.icu test environment", () => {
137
+ apiConfig.endpoint = "https://server.matters.icu/graphql";
138
+ apiConfig.queryMode = "user";
139
+ });
140
+
141
+ When("I query collections for user {string}", async (_ctx, userName: string) => {
142
+ collectionsResult = await graphqlQueryPublic<UserCollectionsQuery>(
143
+ USER_COLLECTIONS_QUERY,
144
+ { userName }
145
+ );
146
+ });
147
+
148
+ Then("I should receive a list of collections or empty list", () => {
149
+ // User might have no collections, which is valid
150
+ expect(collectionsResult?.user).not.toBeNull();
151
+ expect(collectionsResult?.user?.collections).toBeDefined();
152
+ // edges can be empty array or array with items - both are valid
153
+ expect(Array.isArray(collectionsResult?.user?.collections.edges)).toBe(true);
154
+ });
155
+ });
156
+
157
+ Scenario("Handle non-existent user gracefully", ({ Given, When, Then }) => {
158
+ Given("the matters.icu test environment", () => {
159
+ apiConfig.endpoint = "https://server.matters.icu/graphql";
160
+ apiConfig.queryMode = "user";
161
+ });
162
+
163
+ When("I query articles for user {string}", async (_ctx, userName: string) => {
164
+ try {
165
+ articlesResult = await graphqlQueryPublic<UserArticlesQuery>(
166
+ USER_ARTICLES_QUERY,
167
+ { userName }
168
+ );
169
+ queryError = null;
170
+ } catch (error) {
171
+ queryError = error as Error;
172
+ articlesResult = null;
173
+ }
174
+ });
175
+
176
+ Then("the query should return null user", () => {
177
+ expect(articlesResult?.user).toBeNull();
178
+ });
179
+ });
180
+ });