forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
1import { describe, expect, test } from "bun:test";
2import { resolveInternalLinks, findPostsWithStaleLinks } from "./remanso";
3import type { BlogPost } from "../lib/types";
4
5function makePost(
6 slug: string,
7 atUri?: string,
8 options?: { content?: string; draft?: boolean; filePath?: string },
9): BlogPost {
10 return {
11 filePath: options?.filePath ?? `content/${slug}.md`,
12 slug,
13 frontmatter: {
14 title: slug,
15 publishDate: "2024-01-01",
16 atUri,
17 draft: options?.draft,
18 },
19 content: options?.content ?? "",
20 rawContent: "",
21 rawFrontmatter: {},
22 };
23}
24
25describe("resolveInternalLinks", () => {
26 test("strips link for unpublished local path", () => {
27 const posts = [makePost("other-post")];
28 const content = "See [my post](./other-post)";
29 expect(resolveInternalLinks(content, posts)).toBe("See my post");
30 });
31
32 test("rewrites published link to remanso atUri", () => {
33 const posts = [
34 makePost("other-post", "at://did:plc:abc/site.standard.document/abc123"),
35 ];
36 const content = "See [my post](./other-post)";
37 expect(resolveInternalLinks(content, posts)).toBe(
38 "See [my post](at://did:plc:abc/space.remanso.note/abc123)",
39 );
40 });
41
42 test("leaves external links unchanged", () => {
43 const posts = [makePost("other-post")];
44 const content = "See [example](https://example.com)";
45 expect(resolveInternalLinks(content, posts)).toBe(
46 "See [example](https://example.com)",
47 );
48 });
49
50 test("leaves anchor links unchanged", () => {
51 const posts: BlogPost[] = [];
52 const content = "See [section](#heading)";
53 expect(resolveInternalLinks(content, posts)).toBe(
54 "See [section](#heading)",
55 );
56 });
57
58 test("handles .md extension in link path", () => {
59 const posts = [
60 makePost("guide", "at://did:plc:abc/site.standard.document/guide123"),
61 ];
62 const content = "Read the [guide](guide.md)";
63 expect(resolveInternalLinks(content, posts)).toBe(
64 "Read the [guide](at://did:plc:abc/space.remanso.note/guide123)",
65 );
66 });
67
68 test("handles nested slug matching", () => {
69 const posts = [
70 makePost("blog/my-post", "at://did:plc:abc/site.standard.document/rkey1"),
71 ];
72 const content = "See [post](my-post)";
73 expect(resolveInternalLinks(content, posts)).toBe(
74 "See [post](at://did:plc:abc/space.remanso.note/rkey1)",
75 );
76 });
77
78 test("does not rewrite image embeds", () => {
79 const posts = [
80 makePost("photo", "at://did:plc:abc/site.standard.document/photo1"),
81 ];
82 const content = "";
83 expect(resolveInternalLinks(content, posts)).toBe("");
84 });
85
86 test("does not rewrite @mention links", () => {
87 const posts = [
88 makePost("mention", "at://did:plc:abc/site.standard.document/m1"),
89 ];
90 const content = "@[name](mention)";
91 expect(resolveInternalLinks(content, posts)).toBe("@[name](mention)");
92 });
93
94 test("handles multiple links in same content", () => {
95 const posts = [
96 makePost("published", "at://did:plc:abc/site.standard.document/pub1"),
97 makePost("unpublished"),
98 ];
99 const content =
100 "See [a](published) and [b](unpublished) and [c](https://ext.com)";
101 expect(resolveInternalLinks(content, posts)).toBe(
102 "See [a](at://did:plc:abc/space.remanso.note/pub1) and b and [c](https://ext.com)",
103 );
104 });
105
106 test("handles index path normalization", () => {
107 const posts = [
108 makePost("docs", "at://did:plc:abc/site.standard.document/docs1"),
109 ];
110 const content = "See [docs](./docs/index)";
111 expect(resolveInternalLinks(content, posts)).toBe(
112 "See [docs](at://did:plc:abc/space.remanso.note/docs1)",
113 );
114 });
115});
116
117describe("findPostsWithStaleLinks", () => {
118 test("finds published post containing link to a newly created slug", () => {
119 const posts = [
120 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
121 content: "Check out [post B](./post-b)",
122 }),
123 ];
124 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
125 expect(result).toHaveLength(1);
126 expect(result[0]!.slug).toBe("post-a");
127 });
128
129 test("excludes posts in the exclude set (current batch)", () => {
130 const posts = [
131 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
132 content: "Check out [post B](./post-b)",
133 }),
134 ];
135 const result = findPostsWithStaleLinks(
136 posts,
137 ["post-b"],
138 new Set(["content/post-a.md"]),
139 );
140 expect(result).toHaveLength(0);
141 });
142
143 test("excludes unpublished posts (no atUri)", () => {
144 const posts = [
145 makePost("post-a", undefined, {
146 content: "Check out [post B](./post-b)",
147 }),
148 ];
149 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
150 expect(result).toHaveLength(0);
151 });
152
153 test("excludes drafts", () => {
154 const posts = [
155 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
156 content: "Check out [post B](./post-b)",
157 draft: true,
158 }),
159 ];
160 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
161 expect(result).toHaveLength(0);
162 });
163
164 test("ignores external links", () => {
165 const posts = [
166 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
167 content: "Check out [post B](https://example.com/post-b)",
168 }),
169 ];
170 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
171 expect(result).toHaveLength(0);
172 });
173
174 test("ignores image embeds", () => {
175 const posts = [
176 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
177 content: "",
178 }),
179 ];
180 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
181 expect(result).toHaveLength(0);
182 });
183
184 test("ignores @mention links", () => {
185 const posts = [
186 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
187 content: "@[post B](./post-b)",
188 }),
189 ];
190 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
191 expect(result).toHaveLength(0);
192 });
193
194 test("handles nested slug matching", () => {
195 const posts = [
196 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
197 content: "Check out [post](my-post)",
198 }),
199 ];
200 const result = findPostsWithStaleLinks(posts, ["blog/my-post"], new Set());
201 expect(result).toHaveLength(1);
202 });
203
204 test("does not match posts without matching links", () => {
205 const posts = [
206 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", {
207 content: "Check out [post C](./post-c)",
208 }),
209 ];
210 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set());
211 expect(result).toHaveLength(0);
212 });
213});