@oh-my-pi/pi-coding-agent 5.7.67 → 5.7.69

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 (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -6
  3. package/package.json +8 -7
  4. package/src/migrations.ts +1 -34
  5. package/src/vendor/photon/index.js +4 -2
  6. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  7. package/src/core/python-executor-display.test.ts +0 -42
  8. package/src/core/python-executor-lifecycle.test.ts +0 -99
  9. package/src/core/python-executor-mapping.test.ts +0 -41
  10. package/src/core/python-executor-per-call.test.ts +0 -49
  11. package/src/core/python-executor-session.test.ts +0 -103
  12. package/src/core/python-executor-streaming.test.ts +0 -77
  13. package/src/core/python-executor-timeout.test.ts +0 -35
  14. package/src/core/python-executor.lifecycle.test.ts +0 -139
  15. package/src/core/python-executor.result.test.ts +0 -49
  16. package/src/core/python-executor.test.ts +0 -180
  17. package/src/core/python-kernel-display.test.ts +0 -54
  18. package/src/core/python-kernel-env.test.ts +0 -138
  19. package/src/core/python-kernel-session.test.ts +0 -87
  20. package/src/core/python-kernel-ws.test.ts +0 -104
  21. package/src/core/python-kernel.lifecycle.test.ts +0 -249
  22. package/src/core/python-kernel.test.ts +0 -461
  23. package/src/core/python-modules.test.ts +0 -102
  24. package/src/core/python-prelude.test.ts +0 -140
  25. package/src/core/settings-manager-python.test.ts +0 -23
  26. package/src/core/streaming-output.test.ts +0 -26
  27. package/src/core/system-prompt.python.test.ts +0 -17
  28. package/src/core/tools/index.test.ts +0 -212
  29. package/src/core/tools/python-execution.test.ts +0 -68
  30. package/src/core/tools/python-fallback.test.ts +0 -72
  31. package/src/core/tools/python-renderer.test.ts +0 -36
  32. package/src/core/tools/python-tool-mode.test.ts +0 -43
  33. package/src/core/tools/python.test.ts +0 -121
  34. package/src/core/tools/schema-validation.test.ts +0 -530
  35. package/src/core/tools/web-scrapers/academic.test.ts +0 -239
  36. package/src/core/tools/web-scrapers/business.test.ts +0 -82
  37. package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
  38. package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
  39. package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
  40. package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
  41. package/src/core/tools/web-scrapers/media.test.ts +0 -138
  42. package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
  43. package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
  44. package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
  45. package/src/core/tools/web-scrapers/research.test.ts +0 -107
  46. package/src/core/tools/web-scrapers/security.test.ts +0 -103
  47. package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
  48. package/src/core/tools/web-scrapers/social.test.ts +0 -259
  49. package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
  50. package/src/core/tools/web-scrapers/standards.test.ts +0 -122
  51. package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
  52. package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
  53. package/src/discovery/helpers.test.ts +0 -131
@@ -1,107 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { handleBiorxiv } from "./biorxiv";
3
- import { handleOpenLibrary } from "./openlibrary";
4
- import { handleWikidata } from "./wikidata";
5
-
6
- const SKIP = !process.env.WEB_FETCH_INTEGRATION;
7
-
8
- describe.skipIf(SKIP)("handleWikidata", () => {
9
- it("returns null for non-matching URLs", async () => {
10
- const result = await handleWikidata("https://example.com", 20);
11
- expect(result).toBeNull();
12
- });
13
-
14
- it("returns null for non-wikidata URLs", async () => {
15
- const result = await handleWikidata("https://wikipedia.org/wiki/Apple_Inc", 20);
16
- expect(result).toBeNull();
17
- });
18
-
19
- it("fetches Q312 - Apple Inc", async () => {
20
- const result = await handleWikidata("https://www.wikidata.org/wiki/Q312", 20);
21
- expect(result).not.toBeNull();
22
- expect(result?.method).toBe("wikidata");
23
- expect(result?.content).toContain("Apple");
24
- expect(result?.content).toContain("Q312");
25
- expect(result?.contentType).toBe("text/markdown");
26
- expect(result?.fetchedAt).toBeTruthy();
27
- expect(result?.truncated).toBeDefined();
28
- });
29
-
30
- it("fetches Q5 - human (entity)", async () => {
31
- const result = await handleWikidata("https://www.wikidata.org/entity/Q5", 20);
32
- expect(result).not.toBeNull();
33
- expect(result?.method).toBe("wikidata");
34
- expect(result?.content).toContain("human");
35
- expect(result?.content).toContain("Q5");
36
- expect(result?.contentType).toBe("text/markdown");
37
- expect(result?.fetchedAt).toBeTruthy();
38
- expect(result?.truncated).toBeDefined();
39
- });
40
- });
41
-
42
- describe.skipIf(SKIP)("handleOpenLibrary", () => {
43
- it("returns null for non-matching URLs", async () => {
44
- const result = await handleOpenLibrary("https://example.com", 20);
45
- expect(result).toBeNull();
46
- });
47
-
48
- it("returns null for non-openlibrary URLs", async () => {
49
- const result = await handleOpenLibrary("https://amazon.com/books/123", 20);
50
- expect(result).toBeNull();
51
- });
52
-
53
- it("fetches by ISBN - Fantastic Mr Fox", async () => {
54
- const result = await handleOpenLibrary("https://openlibrary.org/isbn/9780140328721", 20);
55
- expect(result).not.toBeNull();
56
- expect(result?.method).toBe("openlibrary");
57
- expect(result?.content).toContain("Fantastic Mr");
58
- expect(result?.content).toContain("Roald Dahl");
59
- expect(result?.contentType).toBe("text/markdown");
60
- expect(result?.fetchedAt).toBeTruthy();
61
- expect(result?.truncated).toBeDefined();
62
- });
63
-
64
- it("fetches work OL45804W - The Lord of the Rings", async () => {
65
- const result = await handleOpenLibrary("https://openlibrary.org/works/OL45804W", 20);
66
- expect(result).not.toBeNull();
67
- expect(result?.method).toBe("openlibrary");
68
- expect(result?.content).toContain("Lord of the Rings");
69
- expect(result?.content).toContain("Tolkien");
70
- expect(result?.contentType).toBe("text/markdown");
71
- expect(result?.fetchedAt).toBeTruthy();
72
- expect(result?.truncated).toBeDefined();
73
- });
74
- });
75
-
76
- describe.skipIf(SKIP)("handleBiorxiv", () => {
77
- it("returns null for non-matching URLs", async () => {
78
- const result = await handleBiorxiv("https://example.com", 20);
79
- expect(result).toBeNull();
80
- });
81
-
82
- it("returns null for non-biorxiv URLs", async () => {
83
- const result = await handleBiorxiv("https://nature.com/articles/123", 20);
84
- expect(result).toBeNull();
85
- });
86
-
87
- // Using the AlphaFold Protein Structure Database paper - highly cited and stable
88
- it("fetches bioRxiv preprint - AlphaFold database", async () => {
89
- const result = await handleBiorxiv("https://www.biorxiv.org/content/10.1101/2021.10.04.463034", 20);
90
- expect(result).not.toBeNull();
91
- expect(result?.method).toBe("biorxiv");
92
- expect(result?.content).toContain("AlphaFold");
93
- expect(result?.content).toContain("Abstract");
94
- expect(result?.contentType).toBe("text/markdown");
95
- expect(result?.fetchedAt).toBeTruthy();
96
- expect(result?.truncated).toBeDefined();
97
- });
98
-
99
- // Testing with version suffix handling
100
- it("fetches bioRxiv preprint with version suffix", async () => {
101
- const result = await handleBiorxiv("https://www.biorxiv.org/content/10.1101/2021.10.04.463034v1", 20);
102
- expect(result).not.toBeNull();
103
- expect(result?.method).toBe("biorxiv");
104
- expect(result?.content).toContain("AlphaFold");
105
- expect(result?.contentType).toBe("text/markdown");
106
- });
107
- });
@@ -1,103 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { handleNvd } from "./nvd";
3
- import { handleOsv } from "./osv";
4
-
5
- const SKIP = !process.env.WEB_FETCH_INTEGRATION;
6
-
7
- describe.skipIf(SKIP)("handleNvd", () => {
8
- it("returns null for non-NVD URLs", async () => {
9
- const result = await handleNvd("https://example.com", 20);
10
- expect(result).toBeNull();
11
- });
12
-
13
- it("returns null for NVD URLs without CVE detail path", async () => {
14
- const result = await handleNvd("https://nvd.nist.gov/", 20);
15
- expect(result).toBeNull();
16
- });
17
-
18
- it("returns null for NVD search URLs", async () => {
19
- const result = await handleNvd("https://nvd.nist.gov/vuln/search/results?query=log4j", 20);
20
- expect(result).toBeNull();
21
- });
22
-
23
- it("fetches CVE-2021-44228 (Log4Shell)", async () => {
24
- const result = await handleNvd("https://nvd.nist.gov/vuln/detail/CVE-2021-44228", 20);
25
- expect(result).not.toBeNull();
26
- expect(result?.method).toBe("nvd");
27
- expect(result?.content).toContain("CVE-2021-44228");
28
- expect(result?.content).toContain("Log4j");
29
- expect(result?.content).toContain("CVSS");
30
- expect(result?.contentType).toBe("text/markdown");
31
- expect(result?.fetchedAt).toBeTruthy();
32
- expect(result?.truncated).toBeDefined();
33
- });
34
-
35
- it("fetches CVE-2014-0160 (Heartbleed)", async () => {
36
- const result = await handleNvd("https://nvd.nist.gov/vuln/detail/CVE-2014-0160", 20);
37
- expect(result).not.toBeNull();
38
- expect(result?.method).toBe("nvd");
39
- expect(result?.content).toContain("CVE-2014-0160");
40
- expect(result?.content).toContain("OpenSSL");
41
- expect(result?.truncated).toBeDefined();
42
- });
43
-
44
- it("handles lowercase CVE IDs", async () => {
45
- const result = await handleNvd("https://nvd.nist.gov/vuln/detail/cve-2021-44228", 20);
46
- expect(result).not.toBeNull();
47
- expect(result?.method).toBe("nvd");
48
- expect(result?.content).toContain("CVE-2021-44228");
49
- });
50
- });
51
-
52
- describe.skipIf(SKIP)("handleOsv", () => {
53
- it("returns null for non-OSV URLs", async () => {
54
- const result = await handleOsv("https://example.com", 20);
55
- expect(result).toBeNull();
56
- });
57
-
58
- it("returns null for OSV homepage", async () => {
59
- const result = await handleOsv("https://osv.dev/", 20);
60
- expect(result).toBeNull();
61
- });
62
-
63
- it("returns null for OSV list URLs", async () => {
64
- const result = await handleOsv("https://osv.dev/list", 20);
65
- expect(result).toBeNull();
66
- });
67
-
68
- it("fetches GHSA-jfh8-c2jp-5v3q (log4j RCE)", async () => {
69
- const result = await handleOsv("https://osv.dev/vulnerability/GHSA-jfh8-c2jp-5v3q", 20);
70
- expect(result).not.toBeNull();
71
- expect(result?.method).toBe("osv");
72
- expect(result?.content).toContain("GHSA-jfh8-c2jp-5v3q");
73
- expect(result?.content).toContain("log4j");
74
- expect(result?.contentType).toBe("text/markdown");
75
- expect(result?.fetchedAt).toBeTruthy();
76
- expect(result?.truncated).toBeDefined();
77
- });
78
-
79
- it("fetches CVE-2021-44228 via OSV", async () => {
80
- const result = await handleOsv("https://osv.dev/vulnerability/CVE-2021-44228", 20);
81
- expect(result).not.toBeNull();
82
- expect(result?.method).toBe("osv");
83
- expect(result?.content).toContain("CVE-2021-44228");
84
- expect(result?.truncated).toBeDefined();
85
- });
86
-
87
- it("fetches PYSEC vulnerability", async () => {
88
- // PYSEC-2021-19 is a well-known pillow vulnerability
89
- const result = await handleOsv("https://osv.dev/vulnerability/PYSEC-2021-19", 20);
90
- expect(result).not.toBeNull();
91
- expect(result?.method).toBe("osv");
92
- expect(result?.content).toContain("PYSEC-2021-19");
93
- expect(result?.content).toContain("Affected Packages");
94
- });
95
-
96
- it("fetches RUSTSEC vulnerability", async () => {
97
- // RUSTSEC-2021-0119 is a well-known actix-web vulnerability
98
- const result = await handleOsv("https://osv.dev/vulnerability/RUSTSEC-2021-0119", 20);
99
- expect(result).not.toBeNull();
100
- expect(result?.method).toBe("osv");
101
- expect(result?.content).toContain("RUSTSEC-2021-0119");
102
- });
103
- });
@@ -1,192 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { handleBluesky } from "./bluesky";
3
- import { handleMastodon } from "./mastodon";
4
-
5
- const SKIP = !process.env.WEB_FETCH_INTEGRATION;
6
-
7
- describe.skipIf(SKIP)("handleMastodon", () => {
8
- it("returns null for non-Mastodon URLs", async () => {
9
- const result = await handleMastodon("https://example.com", 20);
10
- expect(result).toBeNull();
11
- });
12
-
13
- it("returns null for URLs without @user pattern", async () => {
14
- const result = await handleMastodon("https://mastodon.social/about", 20);
15
- expect(result).toBeNull();
16
- });
17
-
18
- it(
19
- "fetches a Mastodon profile",
20
- async () => {
21
- // @Gargron is Eugen Rochko, creator of Mastodon - very stable
22
- const result = await handleMastodon("https://mastodon.social/@Gargron", 20);
23
- expect(result).not.toBeNull();
24
- expect(result?.method).toBe("mastodon");
25
- expect(result?.contentType).toBe("text/markdown");
26
- expect(result?.content).toContain("Gargron");
27
- expect(result?.content).toContain("@Gargron");
28
- expect(result?.content).toContain("**Followers:**");
29
- expect(result?.content).toContain("**Following:**");
30
- expect(result?.content).toContain("**Posts:**");
31
- expect(result?.fetchedAt).toBeTruthy();
32
- expect(result?.truncated).toBeDefined();
33
- expect(result?.notes?.[0]).toContain("Mastodon API");
34
- },
35
- { timeout: 30000 },
36
- );
37
-
38
- it(
39
- "fetches a Mastodon post",
40
- async () => {
41
- // Gargron's post ID 1 - the first ever Mastodon post
42
- const result = await handleMastodon("https://mastodon.social/@Gargron/1", 20);
43
- // Post 1 may not exist anymore; check gracefully
44
- if (result !== null) {
45
- expect(result.method).toBe("mastodon");
46
- expect(result.contentType).toBe("text/markdown");
47
- expect(result.content).toContain("Post by");
48
- expect(result.content).toContain("@Gargron");
49
- expect(result.fetchedAt).toBeTruthy();
50
- expect(result.truncated).toBeDefined();
51
- expect(result.notes?.[0]).toContain("Mastodon API");
52
- }
53
- },
54
- { timeout: 30000 },
55
- );
56
-
57
- it(
58
- "handles a stable pinned post",
59
- async () => {
60
- // Use a well-known post from mastodon.social - Gargron's announcement post
61
- const result = await handleMastodon("https://mastodon.social/@Gargron/109318821117356215", 20);
62
- // May not exist, check gracefully
63
- if (result !== null) {
64
- expect(result.method).toBe("mastodon");
65
- expect(result.contentType).toBe("text/markdown");
66
- expect(result.content).toContain("@Gargron");
67
- expect(result.content).toContain("replies");
68
- expect(result.content).toContain("boosts");
69
- expect(result.content).toContain("favorites");
70
- expect(result.fetchedAt).toBeTruthy();
71
- }
72
- },
73
- { timeout: 30000 },
74
- );
75
-
76
- it(
77
- "includes recent posts in profile",
78
- async () => {
79
- const result = await handleMastodon("https://mastodon.social/@Gargron", 20);
80
- expect(result).not.toBeNull();
81
- // May include recent posts section
82
- if (result?.content?.includes("## Recent Posts")) {
83
- expect(result.content).toMatch(/###\s+\w+/); // Date header
84
- }
85
- },
86
- { timeout: 30000 },
87
- );
88
-
89
- it("returns null for non-Mastodon instance with @user pattern", async () => {
90
- // A site that has @user pattern but isn't Mastodon
91
- const result = await handleMastodon("https://twitter.com/@jack", 20);
92
- expect(result).toBeNull();
93
- });
94
- });
95
-
96
- describe.skipIf(SKIP)("handleBluesky", () => {
97
- it("returns null for non-Bluesky URLs", async () => {
98
- const result = await handleBluesky("https://example.com", 20);
99
- expect(result).toBeNull();
100
- });
101
-
102
- it("returns null for bsky.app URLs without profile path", async () => {
103
- const result = await handleBluesky("https://bsky.app/about", 20);
104
- expect(result).toBeNull();
105
- });
106
-
107
- it(
108
- "fetches a Bluesky profile",
109
- async () => {
110
- // bsky.app official account - stable
111
- const result = await handleBluesky("https://bsky.app/profile/bsky.app", 20);
112
- expect(result).not.toBeNull();
113
- expect(result?.method).toBe("bluesky-api");
114
- expect(result?.contentType).toBe("text/markdown");
115
- expect(result?.content).toContain("bsky.app");
116
- expect(result?.content).toContain("@bsky.app");
117
- expect(result?.content).toContain("**Followers:**");
118
- expect(result?.content).toContain("**Following:**");
119
- expect(result?.content).toContain("**Posts:**");
120
- expect(result?.content).toContain("**DID:**");
121
- expect(result?.fetchedAt).toBeTruthy();
122
- expect(result?.truncated).toBeDefined();
123
- expect(result?.notes).toContain("Fetched via AT Protocol API");
124
- },
125
- { timeout: 30000 },
126
- );
127
-
128
- it(
129
- "fetches Jay Graber's profile",
130
- async () => {
131
- // Jay Graber - CEO of Bluesky, very stable
132
- const result = await handleBluesky("https://bsky.app/profile/jay.bsky.team", 20);
133
- expect(result).not.toBeNull();
134
- expect(result?.method).toBe("bluesky-api");
135
- expect(result?.contentType).toBe("text/markdown");
136
- expect(result?.content).toContain("@jay.bsky.team");
137
- expect(result?.content).toContain("**Followers:**");
138
- expect(result?.fetchedAt).toBeTruthy();
139
- expect(result?.truncated).toBeDefined();
140
- },
141
- { timeout: 30000 },
142
- );
143
-
144
- it(
145
- "fetches a Bluesky post",
146
- async () => {
147
- // A post from bsky.app - use a well-known stable post
148
- const result = await handleBluesky("https://bsky.app/profile/bsky.app/post/3juzlwllznd24", 20);
149
- // Post may not exist, check gracefully
150
- if (result !== null) {
151
- expect(result.method).toBe("bluesky-api");
152
- expect(result.contentType).toBe("text/markdown");
153
- expect(result.content).toContain("# Bluesky Post");
154
- expect(result.content).toContain("@bsky.app");
155
- expect(result.fetchedAt).toBeTruthy();
156
- expect(result.truncated).toBeDefined();
157
- expect(result.notes?.[0]).toContain("AT URI");
158
- }
159
- },
160
- { timeout: 30000 },
161
- );
162
-
163
- it(
164
- "includes post stats",
165
- async () => {
166
- const result = await handleBluesky("https://bsky.app/profile/bsky.app/post/3juzlwllznd24", 20);
167
- // Stats include likes, reposts, replies
168
- if (result?.content) {
169
- // Should have some engagement markers
170
- const hasStats =
171
- result.content.includes("❤️") || result.content.includes("🔁") || result.content.includes("💬");
172
- expect(hasStats || result.content.includes("# Bluesky Post")).toBe(true);
173
- }
174
- },
175
- { timeout: 30000 },
176
- );
177
-
178
- it(
179
- "handles www.bsky.app URLs",
180
- async () => {
181
- const result = await handleBluesky("https://www.bsky.app/profile/bsky.app", 20);
182
- expect(result).not.toBeNull();
183
- expect(result?.method).toBe("bluesky-api");
184
- },
185
- { timeout: 30000 },
186
- );
187
-
188
- it("returns null for invalid profile handle", async () => {
189
- const result = await handleBluesky("https://bsky.app/profile/", 20);
190
- expect(result).toBeNull();
191
- });
192
- });
@@ -1,259 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { handleReddit } from "./reddit";
3
- import { handleStackOverflow } from "./stackoverflow";
4
- import { handleTwitter } from "./twitter";
5
-
6
- const SKIP = !process.env.WEB_FETCH_INTEGRATION;
7
-
8
- describe.skipIf(SKIP)("handleTwitter", () => {
9
- it("returns null for non-Twitter URLs", async () => {
10
- const result = await handleTwitter("https://example.com", 10);
11
- expect(result).toBeNull();
12
- });
13
-
14
- it(
15
- "handles twitter.com status URLs",
16
- async () => {
17
- const result = await handleTwitter("https://twitter.com/jack/status/20", 10000);
18
- expect(result).not.toBeNull();
19
- expect(result?.method).toMatch(/^twitter/);
20
- expect(result?.contentType).toMatch(/^text\/(markdown|plain)$/);
21
- // Either successful fetch or blocked/unavailable message
22
- if (result?.method === "twitter-nitter") {
23
- expect(result?.content).toContain("Tweet by");
24
- expect(result?.notes?.[0]).toContain("Via Nitter");
25
- } else if (result?.method === "twitter-blocked") {
26
- expect(result?.content).toContain("blocks automated access");
27
- expect(result?.notes?.[0]).toContain("Nitter instances unavailable");
28
- }
29
- },
30
- { timeout: 30000 },
31
- );
32
-
33
- it(
34
- "handles x.com status URLs",
35
- async () => {
36
- const result = await handleTwitter("https://x.com/elonmusk/status/1", 10000);
37
- expect(result).not.toBeNull();
38
- expect(result?.method).toMatch(/^twitter/);
39
- expect(result?.contentType).toMatch(/^text\/(markdown|plain)$/);
40
- // Either successful fetch or blocked/unavailable message
41
- if (result?.method === "twitter-nitter") {
42
- expect(result?.finalUrl).toContain("nitter");
43
- } else if (result?.method === "twitter-blocked") {
44
- expect(result?.content).toContain("blocks automated access");
45
- }
46
- },
47
- { timeout: 30000 },
48
- );
49
-
50
- it(
51
- "handles www.twitter.com URLs",
52
- async () => {
53
- const result = await handleTwitter("https://www.twitter.com/twitter/status/1", 10000);
54
- expect(result).not.toBeNull();
55
- expect(result?.method).toMatch(/^twitter/);
56
- },
57
- { timeout: 30000 },
58
- );
59
-
60
- it(
61
- "handles www.x.com URLs",
62
- async () => {
63
- const result = await handleTwitter("https://www.x.com/twitter/status/1", 10000);
64
- expect(result).not.toBeNull();
65
- expect(result?.method).toMatch(/^twitter/);
66
- },
67
- { timeout: 30000 },
68
- );
69
-
70
- it(
71
- "may fail due to Nitter availability",
72
- async () => {
73
- // Test that failure returns helpful message instead of null
74
- const result = await handleTwitter("https://twitter.com/nonexistent/status/999999999999999999", 10000);
75
- expect(result).not.toBeNull();
76
- // Should return blocked message when Nitter fails
77
- if (result?.method === "twitter-blocked") {
78
- expect(result?.content).toContain("Nitter instances were unavailable");
79
- expect(result?.content).toContain("Try:");
80
- }
81
- },
82
- { timeout: 30000 },
83
- );
84
- });
85
-
86
- describe.skipIf(SKIP)("handleReddit", () => {
87
- it("returns null for non-Reddit URLs", async () => {
88
- const result = await handleReddit("https://example.com", 10);
89
- expect(result).toBeNull();
90
- });
91
-
92
- it("fetches subreddit", async () => {
93
- const result = await handleReddit("https://www.reddit.com/r/programming/", 20000);
94
- expect(result).not.toBeNull();
95
- expect(result?.method).toBe("reddit");
96
- expect(result?.contentType).toBe("text/markdown");
97
- expect(result?.content).toContain("# r/programming");
98
- expect(result?.content).toMatch(/\*\*.*\*\*/); // Contains bold formatting
99
- expect(result?.notes).toContain("Fetched via Reddit JSON API");
100
- });
101
-
102
- it("fetches individual post", async () => {
103
- // Use a more reliable recent post URL
104
- const result = await handleReddit("https://www.reddit.com/r/programming/", 20000);
105
- // Individual post may fail if post doesn't exist, check if we get data
106
- if (result !== null) {
107
- expect(result.method).toBe("reddit");
108
- expect(result.contentType).toBe("text/markdown");
109
- expect(result.content).toContain("# r/");
110
- expect(result.notes).toContain("Fetched via Reddit JSON API");
111
- }
112
- });
113
-
114
- it("includes comments in post when available", async () => {
115
- const result = await handleReddit("https://www.reddit.com/r/programming/", 20000);
116
- // Comments test - just verify structure if post with comments is found
117
- if (result?.content?.includes("## Top Comments")) {
118
- expect(result.content).toContain("### u/");
119
- expect(result.content).toContain("points");
120
- }
121
- });
122
-
123
- it("handles old.reddit.com", async () => {
124
- const result = await handleReddit("https://old.reddit.com/r/programming/", 20000);
125
- expect(result).not.toBeNull();
126
- expect(result?.method).toBe("reddit");
127
- expect(result?.contentType).toBe("text/markdown");
128
- expect(result?.content).toContain("# r/");
129
- expect(result?.notes).toContain("Fetched via Reddit JSON API");
130
- });
131
-
132
- it("handles reddit.com without www", async () => {
133
- const result = await handleReddit("https://reddit.com/r/programming/", 20000);
134
- expect(result).not.toBeNull();
135
- expect(result?.method).toBe("reddit");
136
- });
137
-
138
- it("handles URLs with query parameters", async () => {
139
- const result = await handleReddit("https://www.reddit.com/r/programming/?sort=top", 20000);
140
- expect(result).not.toBeNull();
141
- expect(result?.method).toBe("reddit");
142
- expect(result?.content).toContain("# r/");
143
- });
144
-
145
- it("returns null for malformed Reddit URLs", async () => {
146
- const result = await handleReddit("https://www.reddit.com/invalid", 20000);
147
- // May return null or empty result
148
- if (result !== null) {
149
- expect(result.content).toBeDefined();
150
- }
151
- });
152
- });
153
-
154
- describe.skipIf(SKIP)("handleStackOverflow", () => {
155
- it("returns null for non-SO URLs", async () => {
156
- const result = await handleStackOverflow("https://example.com", 10);
157
- expect(result).toBeNull();
158
- });
159
-
160
- it("returns null for SO URLs without question ID", async () => {
161
- const result = await handleStackOverflow("https://stackoverflow.com/", 10);
162
- expect(result).toBeNull();
163
- });
164
-
165
- it("fetches a known question", async () => {
166
- // Use a well-known question that definitely exists
167
- const result = await handleStackOverflow(
168
- "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
169
- 20000,
170
- );
171
- // API may fail or rate limit, check gracefully
172
- if (result !== null) {
173
- expect(result.method).toBe("stackoverflow");
174
- expect(result.contentType).toBe("text/markdown");
175
- expect(result.content).toContain("# ");
176
- expect(result.content).toContain("**Score:");
177
- expect(result.content).toContain("**Tags:");
178
- expect(result.content).toContain("## Question");
179
- expect(result.notes).toContain("Fetched via Stack Exchange API");
180
- }
181
- });
182
-
183
- it("includes answers", async () => {
184
- const result = await handleStackOverflow(
185
- "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
186
- 20000,
187
- );
188
- if (result?.content?.includes("## Answers")) {
189
- expect(result.content).toContain("### Score:");
190
- }
191
- });
192
-
193
- it("shows accepted answer marker when present", async () => {
194
- const result = await handleStackOverflow(
195
- "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
196
- 20000,
197
- );
198
- // Some questions may have accepted answers
199
- if (result?.content?.includes("(Accepted)")) {
200
- expect(result.content).toContain("## Answers");
201
- }
202
- });
203
-
204
- it("handles stackoverflow.com", async () => {
205
- const result = await handleStackOverflow(
206
- "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array",
207
- 20000,
208
- );
209
- expect(result).not.toBeNull();
210
- expect(result?.method).toBe("stackoverflow");
211
- expect(result?.content).toContain("# ");
212
- expect(result?.content).toContain("## Question");
213
- });
214
-
215
- it("handles other StackExchange sites", async () => {
216
- const result = await handleStackOverflow("https://math.stackexchange.com/questions/1000/", 20000);
217
- // API may fail, check gracefully
218
- if (result !== null) {
219
- expect(result.method).toBe("stackoverflow");
220
- expect(result.contentType).toBe("text/markdown");
221
- expect(result.content).toContain("# ");
222
- expect(result.notes).toContain("Fetched via Stack Exchange API");
223
- }
224
- });
225
-
226
- it("extracts question ID from URL", async () => {
227
- const result = await handleStackOverflow(
228
- "https://stackoverflow.com/questions/1234567/some-long-question-title",
229
- 20000,
230
- );
231
- // Should attempt to fetch, may or may not exist
232
- // Either returns valid result or null
233
- if (result !== null) {
234
- expect(result.method).toBe("stackoverflow");
235
- }
236
- });
237
-
238
- it("handles URLs without trailing slash", async () => {
239
- const result = await handleStackOverflow("https://stackoverflow.com/questions/11227809", 20000);
240
- // API may fail, check gracefully
241
- if (result !== null) {
242
- expect(result.method).toBe("stackoverflow");
243
- }
244
- });
245
-
246
- it("includes question metadata", async () => {
247
- const result = await handleStackOverflow(
248
- "https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
249
- 20000,
250
- );
251
- // API may fail, check gracefully
252
- if (result !== null) {
253
- expect(result.content).toContain("**Score:");
254
- expect(result.content).toContain("**Answers:");
255
- expect(result.content).toContain("**Tags:");
256
- expect(result.content).toContain("**Asked by:");
257
- }
258
- });
259
- });