@jant/core 0.6.14 → 0.6.15
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/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
assertSnapshotMeta,
|
|
5
5
|
buildSnapshotMeta,
|
|
6
6
|
getSnapshotDialect,
|
|
7
|
+
SNAPSHOT_VERSION,
|
|
8
|
+
upgradeSnapshotSql,
|
|
9
|
+
upgradeV1SnapshotSql,
|
|
7
10
|
} from "../../../bin/lib/site-snapshot.js";
|
|
8
11
|
|
|
9
12
|
const SITE = { id: "sit_test", key: "default" };
|
|
@@ -11,6 +14,7 @@ const SITE = { id: "sit_test", key: "default" };
|
|
|
11
14
|
describe("buildSnapshotMeta", () => {
|
|
12
15
|
it("includes the dialect when provided", () => {
|
|
13
16
|
const meta = buildSnapshotMeta(SITE, { dialect: "pg" });
|
|
17
|
+
expect(meta.version).toBe(2);
|
|
14
18
|
expect(meta.dialect).toBe("pg");
|
|
15
19
|
expect(meta.site).toEqual({ id: "sit_test", key: "default" });
|
|
16
20
|
});
|
|
@@ -38,6 +42,26 @@ describe("assertSnapshotMeta", () => {
|
|
|
38
42
|
expect(() => assertSnapshotMeta(buildSnapshotMeta(SITE))).not.toThrow();
|
|
39
43
|
});
|
|
40
44
|
|
|
45
|
+
it("accepts v1 snapshots for import compatibility", () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
assertSnapshotMeta({
|
|
48
|
+
format: "jant-site-snapshot",
|
|
49
|
+
version: 1,
|
|
50
|
+
site: SITE,
|
|
51
|
+
}),
|
|
52
|
+
).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("rejects unknown snapshot versions", () => {
|
|
56
|
+
expect(() =>
|
|
57
|
+
assertSnapshotMeta({
|
|
58
|
+
format: "jant-site-snapshot",
|
|
59
|
+
version: SNAPSHOT_VERSION + 1,
|
|
60
|
+
site: SITE,
|
|
61
|
+
}),
|
|
62
|
+
).toThrow(/Unsupported snapshot version/);
|
|
63
|
+
});
|
|
64
|
+
|
|
41
65
|
it("rejects an unknown dialect at read time", () => {
|
|
42
66
|
expect(() =>
|
|
43
67
|
assertSnapshotMeta({
|
|
@@ -50,6 +74,82 @@ describe("assertSnapshotMeta", () => {
|
|
|
50
74
|
});
|
|
51
75
|
});
|
|
52
76
|
|
|
77
|
+
describe("upgradeV1SnapshotSql", () => {
|
|
78
|
+
it("unions per-post Collection rows into Thread rows without losing metadata", () => {
|
|
79
|
+
const sql = `
|
|
80
|
+
INSERT INTO "collection" ("id", "site_id") VALUES('col_a', 'sit_test');
|
|
81
|
+
INSERT INTO "collection" ("id", "site_id") VALUES('col_b', 'sit_test');
|
|
82
|
+
INSERT INTO "post" ("id", "site_id", "thread_id") VALUES('pst_root', 'sit_test', 'pst_root');
|
|
83
|
+
INSERT INTO "post" ("id", "site_id", "thread_id") VALUES('pst_reply', 'sit_test', 'pst_root');
|
|
84
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_root', 'col_a', 10, 5, 40);
|
|
85
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_reply', 'col_a', 30, 2, 20);
|
|
86
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_reply', 'col_b', 15, 9, NULL);
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const upgraded = upgradeV1SnapshotSql(sql);
|
|
90
|
+
expect(upgraded).not.toContain('INSERT INTO "post_collection"');
|
|
91
|
+
expect(upgraded).toContain(
|
|
92
|
+
'INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at") VALUES(\'sit_test\', \'pst_root\', \'col_a\', 30, 2, 40)',
|
|
93
|
+
);
|
|
94
|
+
expect(upgraded).toContain(
|
|
95
|
+
'INSERT INTO "thread_collection" ("site_id", "thread_id", "collection_id", "created_at", "position", "pinned_at") VALUES(\'sit_test\', \'pst_root\', \'col_b\', 15, 9, NULL)',
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("preserves comment-like lines inside multiline post content", () => {
|
|
100
|
+
const body = ["before", "-- must survive", "---", "after"].join("\n");
|
|
101
|
+
const sql = `
|
|
102
|
+
-- export metadata must be ignored as a real SQL comment
|
|
103
|
+
INSERT INTO "collection" ("id", "site_id") VALUES('col_a', 'sit_test');
|
|
104
|
+
INSERT INTO "post" ("id", "site_id", "thread_id", "body") VALUES('pst_root', 'sit_test', 'pst_root', '${body}');
|
|
105
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_root', 'col_a', 10, 0, NULL);
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const upgraded = upgradeV1SnapshotSql(sql);
|
|
109
|
+
expect(upgraded).toContain(`'${body}'`);
|
|
110
|
+
expect(upgraded).not.toContain("export metadata must be ignored");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("fails a dangling v1 membership during preflight conversion", () => {
|
|
114
|
+
const sql = `
|
|
115
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_missing', 'col_a', 10, 0, NULL);
|
|
116
|
+
`;
|
|
117
|
+
expect(() => upgradeV1SnapshotSql(sql)).toThrow(
|
|
118
|
+
/references missing post pst_missing.*target content was not changed/,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("fails a v1 membership whose Collection is missing", () => {
|
|
123
|
+
const sql = `
|
|
124
|
+
INSERT INTO "post" ("id", "site_id", "thread_id") VALUES('pst_root', 'sit_test', 'pst_root');
|
|
125
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_root', 'col_missing', 10, 0, NULL);
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
expect(() => upgradeV1SnapshotSql(sql)).toThrow(
|
|
129
|
+
/references missing Collection col_missing.*target content was not changed/,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("fails a v1 membership whose thread_id points at a reply", () => {
|
|
134
|
+
const sql = `
|
|
135
|
+
INSERT INTO "collection" ("id", "site_id") VALUES('col_a', 'sit_test');
|
|
136
|
+
INSERT INTO "post" ("id", "site_id", "thread_id") VALUES('pst_member', 'sit_test', 'pst_reply');
|
|
137
|
+
INSERT INTO "post" ("id", "site_id", "thread_id") VALUES('pst_reply', 'sit_test', 'pst_root');
|
|
138
|
+
INSERT INTO "post_collection" ("site_id", "post_id", "collection_id", "created_at", "position", "pinned_at") VALUES('sit_test', 'pst_member', 'col_a', 10, 0, NULL);
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
expect(() => upgradeV1SnapshotSql(sql)).toThrow(
|
|
142
|
+
/references invalid Thread root pst_reply.*target content was not changed/,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("leaves v2 SQL unchanged", () => {
|
|
147
|
+
const sql =
|
|
148
|
+
'INSERT INTO "thread_collection" ("site_id") VALUES(\'sit_test\');';
|
|
149
|
+
expect(upgradeSnapshotSql(sql, 2)).toBe(sql);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
53
153
|
describe("getSnapshotDialect", () => {
|
|
54
154
|
it("returns the dialect when valid", () => {
|
|
55
155
|
expect(getSnapshotDialect({ dialect: "pg" })).toBe("pg");
|
package/src/node/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { buildThemeStyle } from "../lib/theme.js";
|
|
|
14
14
|
export { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
|
|
15
15
|
export {
|
|
16
16
|
BUILTIN_FONT_THEMES,
|
|
17
|
-
|
|
17
|
+
getCjkFontCssVariables,
|
|
18
18
|
getFontThemeCssVariables,
|
|
19
|
+
resolveCjkFontProfile,
|
|
19
20
|
} from "../ui/font-themes.js";
|
package/src/preset.css
CHANGED
|
@@ -237,9 +237,19 @@ html {
|
|
|
237
237
|
padding-left: 1.4em;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
:where(ol ol) {
|
|
241
|
+
list-style-type: lower-alpha;
|
|
242
|
+
margin-top: 0.4em;
|
|
243
|
+
margin-bottom: 0.4em;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
:where(ol ol ol) {
|
|
247
|
+
list-style-type: lower-roman;
|
|
248
|
+
}
|
|
249
|
+
|
|
240
250
|
:where(li, dt) {
|
|
241
|
-
margin-top: 0.
|
|
242
|
-
margin-bottom: 0.
|
|
251
|
+
margin-top: 0.5em;
|
|
252
|
+
margin-bottom: 0.5em;
|
|
243
253
|
}
|
|
244
254
|
|
|
245
255
|
/* Normalize li > p so list spacing is controlled by li alone,
|
|
@@ -252,6 +262,10 @@ html {
|
|
|
252
262
|
margin-bottom: 0;
|
|
253
263
|
}
|
|
254
264
|
|
|
265
|
+
:where(li > p:has(+ ol)) {
|
|
266
|
+
margin-bottom: 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
255
269
|
:where(code, pre code) {
|
|
256
270
|
font-size: var(--type-code);
|
|
257
271
|
line-height: 1.42;
|
|
@@ -351,4 +351,55 @@ describe("Compose Routes", () => {
|
|
|
351
351
|
expect(data.error).toBeDefined();
|
|
352
352
|
});
|
|
353
353
|
});
|
|
354
|
+
|
|
355
|
+
describe("POST /compose/thread", () => {
|
|
356
|
+
it("keeps the existing thread activity unchanged for a quiet multi-post reply", async () => {
|
|
357
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
358
|
+
app.route("/compose", composeRoutes);
|
|
359
|
+
|
|
360
|
+
const root = await services.posts.create({
|
|
361
|
+
format: "note",
|
|
362
|
+
bodyMarkdown: "Existing root",
|
|
363
|
+
publishedAt: 1000,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const res = await app.request("/compose/thread", {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: {
|
|
369
|
+
"Content-Type": "application/json",
|
|
370
|
+
Accept: "application/json",
|
|
371
|
+
},
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
posts: [
|
|
374
|
+
{
|
|
375
|
+
format: "note",
|
|
376
|
+
bodyMarkdown: "Quiet reply one",
|
|
377
|
+
status: "published",
|
|
378
|
+
replyToId: root.id,
|
|
379
|
+
quietReply: true,
|
|
380
|
+
publishedAt: 9000,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
format: "note",
|
|
384
|
+
bodyMarkdown: "Quiet reply two",
|
|
385
|
+
status: "published",
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
}),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
expect(res.status).toBe(200);
|
|
392
|
+
expect(await res.json()).toMatchObject({ status: "published" });
|
|
393
|
+
|
|
394
|
+
const thread = await services.posts.getThread(root.id);
|
|
395
|
+
expect(thread.map((post) => post.bodyText)).toEqual([
|
|
396
|
+
"Existing root",
|
|
397
|
+
"Quiet reply one",
|
|
398
|
+
"Quiet reply two",
|
|
399
|
+
]);
|
|
400
|
+
expect((await services.posts.getById(root.id))?.lastActivityAt).toBe(
|
|
401
|
+
1000,
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
354
405
|
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { withConfig } from "../../middleware/config.js";
|
|
4
|
+
import type { Bindings } from "../../types.js";
|
|
5
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
6
|
+
import { siteSkillRoutes } from "../site-skill.js";
|
|
7
|
+
|
|
8
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
9
|
+
|
|
10
|
+
function createSiteSkillTestApp(
|
|
11
|
+
env: Partial<Bindings> = {},
|
|
12
|
+
pathPrefix: string | null = null,
|
|
13
|
+
) {
|
|
14
|
+
const app = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
app.use("*", async (c, next) => {
|
|
17
|
+
c.env = env as Bindings;
|
|
18
|
+
c.set("publicRequestUrl", c.req.url);
|
|
19
|
+
c.set(
|
|
20
|
+
"currentSiteDomain",
|
|
21
|
+
pathPrefix === null
|
|
22
|
+
? null
|
|
23
|
+
: ({ pathPrefix } as AppVariables["currentSiteDomain"]),
|
|
24
|
+
);
|
|
25
|
+
c.set("services", {
|
|
26
|
+
settings: {
|
|
27
|
+
getAll: async () => ({}),
|
|
28
|
+
},
|
|
29
|
+
} as AppVariables["services"]);
|
|
30
|
+
await next();
|
|
31
|
+
});
|
|
32
|
+
app.use("*", withConfig());
|
|
33
|
+
app.route("/", siteSkillRoutes);
|
|
34
|
+
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("GET /skill.md", () => {
|
|
39
|
+
it("binds the guide to the request origin when SITE_ORIGIN is absent", async () => {
|
|
40
|
+
const app = createSiteSkillTestApp();
|
|
41
|
+
|
|
42
|
+
const response = await app.request("https://owen.jant.blog/skill.md");
|
|
43
|
+
|
|
44
|
+
expect(response.status).toBe(200);
|
|
45
|
+
expect(response.headers.get("Content-Type")).toBe(
|
|
46
|
+
"text/markdown; charset=utf-8",
|
|
47
|
+
);
|
|
48
|
+
expect(response.headers.get("Cache-Control")).toBe(
|
|
49
|
+
"public, max-age=300, s-maxage=3600",
|
|
50
|
+
);
|
|
51
|
+
expect(response.headers.get("X-Robots-Tag")).toBe("noindex");
|
|
52
|
+
|
|
53
|
+
const markdown = await response.text();
|
|
54
|
+
expect(markdown).toContain("name: jant-site");
|
|
55
|
+
expect(markdown).toContain("# Jant Site Skill");
|
|
56
|
+
expect(markdown).toContain(
|
|
57
|
+
"This site-scoped copy is bound to <https://owen.jant.blog>",
|
|
58
|
+
);
|
|
59
|
+
expect(markdown).toContain(
|
|
60
|
+
'curl -X POST "https://owen.jant.blog/api/upload"',
|
|
61
|
+
);
|
|
62
|
+
expect(markdown).toContain(
|
|
63
|
+
'curl -X POST "https://owen.jant.blog/api/collections"',
|
|
64
|
+
);
|
|
65
|
+
expect(markdown).toContain(
|
|
66
|
+
"Sign in to Jant at `https://owen.jant.blog/signin`",
|
|
67
|
+
);
|
|
68
|
+
expect(markdown).not.toContain("$JANT_SITE");
|
|
69
|
+
expect(markdown).not.toContain("<site>");
|
|
70
|
+
expect(markdown).not.toContain("export JANT_SITE=");
|
|
71
|
+
expect(markdown).toContain("## Common Content Operations");
|
|
72
|
+
expect(markdown).toContain("## Import Content from Another Platform");
|
|
73
|
+
expect(markdown).not.toContain("## If You Need to Start Over");
|
|
74
|
+
expect(markdown).toContain('"url": "https://example.com/post"');
|
|
75
|
+
expect(markdown).toContain("https://jant.me/docs/API.md");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("uses the configured canonical URL and path prefix in single-site mode", async () => {
|
|
79
|
+
const app = createSiteSkillTestApp({
|
|
80
|
+
SITE_ORIGIN: "https://example.com",
|
|
81
|
+
SITE_PATH_PREFIX: "/blog/",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const response = await app.request("https://preview.example/skill.md");
|
|
85
|
+
const markdown = await response.text();
|
|
86
|
+
|
|
87
|
+
expect(markdown).toContain(
|
|
88
|
+
"This site-scoped copy is bound to <https://example.com/blog>",
|
|
89
|
+
);
|
|
90
|
+
expect(markdown).toContain(
|
|
91
|
+
'curl -X POST "https://example.com/blog/api/upload"',
|
|
92
|
+
);
|
|
93
|
+
expect(markdown).not.toContain("https://preview.example");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("includes a resolved domain path prefix in host-based mode", async () => {
|
|
97
|
+
const app = createSiteSkillTestApp(
|
|
98
|
+
{ SITE_RESOLUTION_MODE: "host-based" },
|
|
99
|
+
"/notes",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const response = await app.request("https://owen.example/skill.md");
|
|
103
|
+
const markdown = await response.text();
|
|
104
|
+
|
|
105
|
+
expect(markdown).toContain(
|
|
106
|
+
"This site-scoped copy is bound to <https://owen.example/notes>",
|
|
107
|
+
);
|
|
108
|
+
expect(markdown).toContain(
|
|
109
|
+
'curl -X POST "https://owen.example/notes/api/collections"',
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -18,7 +18,7 @@ describe("Collections API Routes", () => {
|
|
|
18
18
|
expect(body.directoryItems).toEqual([]);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it("returns collections with
|
|
21
|
+
it("returns collections with Thread counts and directory items", async () => {
|
|
22
22
|
const { app, services } = createTestApp();
|
|
23
23
|
app.route("/api/collections", collectionsApiRoutes);
|
|
24
24
|
|
|
@@ -30,14 +30,14 @@ describe("Collections API Routes", () => {
|
|
|
30
30
|
format: "note",
|
|
31
31
|
bodyMarkdown: "tech post",
|
|
32
32
|
});
|
|
33
|
-
await services.collections.
|
|
33
|
+
await services.collections.addThread(col.id, post.id);
|
|
34
34
|
|
|
35
35
|
const res = await app.request("/api/collections");
|
|
36
36
|
const body = await res.json();
|
|
37
37
|
|
|
38
38
|
expect(body.collections).toHaveLength(1);
|
|
39
39
|
expect(body.collections[0].slug).toBe("tech");
|
|
40
|
-
expect(body.collections[0].
|
|
40
|
+
expect(body.collections[0].threadCount).toBe(1);
|
|
41
41
|
expect(body.collections[0].recentActivityAt).toBe(post.lastActivityAt);
|
|
42
42
|
|
|
43
43
|
expect(body.directoryItems).toHaveLength(1);
|
|
@@ -460,8 +460,8 @@ describe("Collections API Routes", () => {
|
|
|
460
460
|
});
|
|
461
461
|
});
|
|
462
462
|
|
|
463
|
-
describe("POST /api/collections/:id/
|
|
464
|
-
it("adds a
|
|
463
|
+
describe("POST /api/collections/:id/threads", () => {
|
|
464
|
+
it("adds a thread to a collection", async () => {
|
|
465
465
|
const { app, services } = createTestApp({ authenticated: true });
|
|
466
466
|
app.route("/api/collections", collectionsApiRoutes);
|
|
467
467
|
|
|
@@ -474,16 +474,46 @@ describe("Collections API Routes", () => {
|
|
|
474
474
|
bodyMarkdown: "test",
|
|
475
475
|
});
|
|
476
476
|
|
|
477
|
-
const res = await app.request(`/api/collections/${col.id}/
|
|
477
|
+
const res = await app.request(`/api/collections/${col.id}/threads`, {
|
|
478
478
|
method: "POST",
|
|
479
479
|
headers: { "Content-Type": "application/json" },
|
|
480
|
-
body: JSON.stringify({
|
|
480
|
+
body: JSON.stringify({ threadId: post.id }),
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
expect(res.status).toBe(201);
|
|
484
484
|
|
|
485
|
-
const
|
|
486
|
-
expect(
|
|
485
|
+
const threadIds = await services.collections.getThreadIds(col.id);
|
|
486
|
+
expect(threadIds).toContain(post.id);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("normalizes a child post ID to the thread root", async () => {
|
|
490
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
491
|
+
app.route("/api/collections", collectionsApiRoutes);
|
|
492
|
+
|
|
493
|
+
const col = await services.collections.create({
|
|
494
|
+
slug: "tech",
|
|
495
|
+
title: "Tech",
|
|
496
|
+
});
|
|
497
|
+
const root = await services.posts.create({
|
|
498
|
+
format: "note",
|
|
499
|
+
bodyMarkdown: "root",
|
|
500
|
+
});
|
|
501
|
+
const child = await services.posts.create({
|
|
502
|
+
format: "note",
|
|
503
|
+
bodyMarkdown: "child",
|
|
504
|
+
replyToId: root.id,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const res = await app.request(`/api/collections/${col.id}/threads`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: { "Content-Type": "application/json" },
|
|
510
|
+
body: JSON.stringify({ threadId: child.id }),
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
expect(res.status).toBe(201);
|
|
514
|
+
expect(await services.collections.getThreadIds(col.id)).toEqual([
|
|
515
|
+
root.id,
|
|
516
|
+
]);
|
|
487
517
|
});
|
|
488
518
|
|
|
489
519
|
it("returns 404 for non-existent collection", async () => {
|
|
@@ -496,10 +526,10 @@ describe("Collections API Routes", () => {
|
|
|
496
526
|
bodyMarkdown: "test",
|
|
497
527
|
});
|
|
498
528
|
|
|
499
|
-
const res = await app.request(`/api/collections/${missingId}/
|
|
529
|
+
const res = await app.request(`/api/collections/${missingId}/threads`, {
|
|
500
530
|
method: "POST",
|
|
501
531
|
headers: { "Content-Type": "application/json" },
|
|
502
|
-
body: JSON.stringify({
|
|
532
|
+
body: JSON.stringify({ threadId: post.id }),
|
|
503
533
|
});
|
|
504
534
|
|
|
505
535
|
expect(res.status).toBe(404);
|
|
@@ -514,11 +544,11 @@ describe("Collections API Routes", () => {
|
|
|
514
544
|
title: "Tech",
|
|
515
545
|
});
|
|
516
546
|
|
|
517
|
-
const res = await app.request(`/api/collections/${col.id}/
|
|
547
|
+
const res = await app.request(`/api/collections/${col.id}/threads`, {
|
|
518
548
|
method: "POST",
|
|
519
549
|
headers: { "Content-Type": "application/json" },
|
|
520
550
|
body: JSON.stringify({
|
|
521
|
-
|
|
551
|
+
threadId: "00000000-0000-0000-0000-000000000001",
|
|
522
552
|
}),
|
|
523
553
|
});
|
|
524
554
|
|
|
@@ -526,8 +556,8 @@ describe("Collections API Routes", () => {
|
|
|
526
556
|
});
|
|
527
557
|
});
|
|
528
558
|
|
|
529
|
-
describe("DELETE /api/collections/:id/
|
|
530
|
-
it("removes a
|
|
559
|
+
describe("DELETE /api/collections/:id/threads/:threadId", () => {
|
|
560
|
+
it("removes a thread from a collection", async () => {
|
|
531
561
|
const { app, services } = createTestApp({ authenticated: true });
|
|
532
562
|
app.route("/api/collections", collectionsApiRoutes);
|
|
533
563
|
|
|
@@ -540,17 +570,59 @@ describe("Collections API Routes", () => {
|
|
|
540
570
|
bodyMarkdown: "test",
|
|
541
571
|
});
|
|
542
572
|
|
|
543
|
-
await services.collections.
|
|
573
|
+
await services.collections.addThread(col.id, post.id);
|
|
544
574
|
|
|
545
575
|
const res = await app.request(
|
|
546
|
-
`/api/collections/${col.id}/
|
|
576
|
+
`/api/collections/${col.id}/threads/${post.id}`,
|
|
547
577
|
{ method: "DELETE" },
|
|
548
578
|
);
|
|
549
579
|
|
|
550
580
|
expect(res.status).toBe(200);
|
|
551
581
|
|
|
552
|
-
const
|
|
553
|
-
expect(
|
|
582
|
+
const threadIds = await services.collections.getThreadIds(col.id);
|
|
583
|
+
expect(threadIds).not.toContain(post.id);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
describe("PUT/DELETE /api/collections/:id/threads/:threadId/pin", () => {
|
|
588
|
+
it("pins and unpins the whole thread when given a child post ID", async () => {
|
|
589
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
590
|
+
app.route("/api/collections", collectionsApiRoutes);
|
|
591
|
+
|
|
592
|
+
const col = await services.collections.create({
|
|
593
|
+
slug: "tech",
|
|
594
|
+
title: "Tech",
|
|
595
|
+
});
|
|
596
|
+
const root = await services.posts.create({
|
|
597
|
+
format: "note",
|
|
598
|
+
bodyMarkdown: "root",
|
|
599
|
+
});
|
|
600
|
+
const child = await services.posts.create({
|
|
601
|
+
format: "note",
|
|
602
|
+
bodyMarkdown: "child",
|
|
603
|
+
replyToId: root.id,
|
|
604
|
+
});
|
|
605
|
+
await services.collections.addThread(col.id, root.id);
|
|
606
|
+
|
|
607
|
+
const pinRes = await app.request(
|
|
608
|
+
`/api/collections/${col.id}/threads/${child.id}/pin`,
|
|
609
|
+
{ method: "PUT" },
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
expect(pinRes.status).toBe(200);
|
|
613
|
+
expect(await services.collections.getPinnedThreadIds([col.id])).toEqual(
|
|
614
|
+
new Set([root.id]),
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
const unpinRes = await app.request(
|
|
618
|
+
`/api/collections/${col.id}/threads/${child.id}/pin`,
|
|
619
|
+
{ method: "DELETE" },
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
expect(unpinRes.status).toBe(200);
|
|
623
|
+
expect(await services.collections.getPinnedThreadIds([col.id])).toEqual(
|
|
624
|
+
new Set(),
|
|
625
|
+
);
|
|
554
626
|
});
|
|
555
627
|
});
|
|
556
628
|
});
|
|
@@ -144,11 +144,75 @@ describe("MCP API Routes", () => {
|
|
|
144
144
|
);
|
|
145
145
|
|
|
146
146
|
expect(toolNames).toContain("jant_posts_list");
|
|
147
|
-
expect(toolNames).toContain("
|
|
147
|
+
expect(toolNames).toContain("jant_collections_add_thread");
|
|
148
|
+
expect(toolNames).toContain("jant_collections_remove_thread");
|
|
148
149
|
expect(toolNames).toContain("jant_settings_update");
|
|
149
150
|
expect(toolNames).toContain("jant_search_posts");
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
it("manages collection membership at the thread root", async () => {
|
|
154
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
155
|
+
app.route("/api/mcp", mcpApiRoutes);
|
|
156
|
+
|
|
157
|
+
const collection = await services.collections.create({
|
|
158
|
+
slug: "notes",
|
|
159
|
+
title: "Notes",
|
|
160
|
+
});
|
|
161
|
+
const root = await services.posts.create({
|
|
162
|
+
format: "note",
|
|
163
|
+
bodyMarkdown: "Root",
|
|
164
|
+
});
|
|
165
|
+
const child = await services.posts.create({
|
|
166
|
+
format: "note",
|
|
167
|
+
bodyMarkdown: "Child",
|
|
168
|
+
replyToId: root.id,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const addRes = await postMcp(
|
|
172
|
+
app,
|
|
173
|
+
{
|
|
174
|
+
jsonrpc: "2.0",
|
|
175
|
+
id: 21,
|
|
176
|
+
method: "tools/call",
|
|
177
|
+
params: {
|
|
178
|
+
name: "jant_collections_add_thread",
|
|
179
|
+
arguments: {
|
|
180
|
+
collectionId: collection.id,
|
|
181
|
+
threadId: child.id,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{ "MCP-Protocol-Version": "2025-06-18" },
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(addRes.status).toBe(200);
|
|
189
|
+
expect((await addRes.json()).result.isError).toBe(false);
|
|
190
|
+
expect(await services.collections.getThreadIds(collection.id)).toEqual([
|
|
191
|
+
root.id,
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const removeRes = await postMcp(
|
|
195
|
+
app,
|
|
196
|
+
{
|
|
197
|
+
jsonrpc: "2.0",
|
|
198
|
+
id: 22,
|
|
199
|
+
method: "tools/call",
|
|
200
|
+
params: {
|
|
201
|
+
name: "jant_collections_remove_thread",
|
|
202
|
+
arguments: {
|
|
203
|
+
collectionId: collection.id,
|
|
204
|
+
threadId: child.id,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{ "MCP-Protocol-Version": "2025-06-18" },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(removeRes.status).toBe(200);
|
|
212
|
+
expect((await removeRes.json()).result.isError).toBe(false);
|
|
213
|
+
expect(await services.collections.getThreadIds(collection.id)).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
152
216
|
it("creates posts through tools/call", async () => {
|
|
153
217
|
const { app, services } = createTestApp({ authenticated: true });
|
|
154
218
|
app.route("/api/mcp", mcpApiRoutes);
|