@oh-my-pi/pi-coding-agent 5.6.77 → 5.7.68
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 +15 -0
- package/package.json +8 -8
- package/src/migrations.ts +1 -34
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +2 -2
- package/src/vendor/photon/LICENSE.md +201 -0
- package/src/vendor/photon/README.md +158 -0
- package/src/vendor/photon/index.d.ts +3013 -0
- package/src/vendor/photon/index.js +4461 -0
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
- package/src/vendor/photon/photon_rs_bg.wasm.d.ts +193 -0
- package/src/core/python-executor-display.test.ts +0 -42
- package/src/core/python-executor-lifecycle.test.ts +0 -99
- package/src/core/python-executor-mapping.test.ts +0 -41
- package/src/core/python-executor-per-call.test.ts +0 -49
- package/src/core/python-executor-session.test.ts +0 -103
- package/src/core/python-executor-streaming.test.ts +0 -77
- package/src/core/python-executor-timeout.test.ts +0 -35
- package/src/core/python-executor.lifecycle.test.ts +0 -139
- package/src/core/python-executor.result.test.ts +0 -49
- package/src/core/python-executor.test.ts +0 -180
- package/src/core/python-kernel-display.test.ts +0 -54
- package/src/core/python-kernel-env.test.ts +0 -138
- package/src/core/python-kernel-session.test.ts +0 -87
- package/src/core/python-kernel-ws.test.ts +0 -104
- package/src/core/python-kernel.lifecycle.test.ts +0 -249
- package/src/core/python-kernel.test.ts +0 -461
- package/src/core/python-modules.test.ts +0 -102
- package/src/core/python-prelude.test.ts +0 -140
- package/src/core/settings-manager-python.test.ts +0 -23
- package/src/core/streaming-output.test.ts +0 -26
- package/src/core/system-prompt.python.test.ts +0 -17
- package/src/core/tools/index.test.ts +0 -212
- package/src/core/tools/python-execution.test.ts +0 -68
- package/src/core/tools/python-fallback.test.ts +0 -72
- package/src/core/tools/python-renderer.test.ts +0 -36
- package/src/core/tools/python-tool-mode.test.ts +0 -43
- package/src/core/tools/python.test.ts +0 -121
- package/src/core/tools/schema-validation.test.ts +0 -530
- package/src/core/tools/web-scrapers/academic.test.ts +0 -239
- package/src/core/tools/web-scrapers/business.test.ts +0 -82
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
- package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
- package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
- package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
- package/src/core/tools/web-scrapers/media.test.ts +0 -138
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
- package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
- package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
- package/src/core/tools/web-scrapers/research.test.ts +0 -107
- package/src/core/tools/web-scrapers/security.test.ts +0 -103
- package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
- package/src/core/tools/web-scrapers/social.test.ts +0 -259
- package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
- package/src/core/tools/web-scrapers/standards.test.ts +0 -122
- package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
- package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
- 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
|
-
});
|