@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,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
|
+
});
|