@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,120 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { handleStackOverflow } from "./stackoverflow";
|
|
3
|
-
|
|
4
|
-
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
5
|
-
|
|
6
|
-
describe.skipIf(SKIP)("handleStackOverflow", () => {
|
|
7
|
-
it("returns null for non-SE URLs", async () => {
|
|
8
|
-
const result = await handleStackOverflow("https://example.com", 20);
|
|
9
|
-
expect(result).toBeNull();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns null for SE site without question path", async () => {
|
|
13
|
-
const result = await handleStackOverflow("https://stackoverflow.com/tags", 20);
|
|
14
|
-
expect(result).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("returns null for SE user profile URLs", async () => {
|
|
18
|
-
const result = await handleStackOverflow("https://stackoverflow.com/users/1", 20);
|
|
19
|
-
expect(result).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// stackoverflow.com - "What is a NullPointerException" (classic, highly voted)
|
|
23
|
-
it("fetches stackoverflow.com question", async () => {
|
|
24
|
-
const result = await handleStackOverflow(
|
|
25
|
-
"https://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it",
|
|
26
|
-
20,
|
|
27
|
-
);
|
|
28
|
-
expect(result).not.toBeNull();
|
|
29
|
-
expect(result?.method).toBe("stackexchange");
|
|
30
|
-
expect(result?.content).toContain("NullPointerException");
|
|
31
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
32
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
33
|
-
expect(result?.truncated).toBeDefined();
|
|
34
|
-
expect(result?.notes?.[0]).toContain("site=stackoverflow");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// unix.stackexchange.com - "Why does my shell script choke on whitespace" (classic)
|
|
38
|
-
it("fetches unix.stackexchange.com question", async () => {
|
|
39
|
-
const result = await handleStackOverflow(
|
|
40
|
-
"https://unix.stackexchange.com/questions/131766/why-does-my-shell-script-choke-on-whitespace-or-other-special-characters",
|
|
41
|
-
20,
|
|
42
|
-
);
|
|
43
|
-
expect(result).not.toBeNull();
|
|
44
|
-
expect(result?.method).toBe("stackexchange");
|
|
45
|
-
expect(result?.content).toContain("whitespace");
|
|
46
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
47
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
48
|
-
expect(result?.notes?.[0]).toContain("site=unix");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// superuser.com - "What are PATH and other environment variables" (stable)
|
|
52
|
-
it("fetches superuser.com question", async () => {
|
|
53
|
-
const result = await handleStackOverflow(
|
|
54
|
-
"https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them",
|
|
55
|
-
20,
|
|
56
|
-
);
|
|
57
|
-
expect(result).not.toBeNull();
|
|
58
|
-
expect(result?.method).toBe("stackexchange");
|
|
59
|
-
expect(result?.content).toContain("PATH");
|
|
60
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
61
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
62
|
-
expect(result?.notes?.[0]).toContain("site=superuser");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// askubuntu.com - "What is the difference between apt and apt-get" (iconic)
|
|
66
|
-
it("fetches askubuntu.com question", async () => {
|
|
67
|
-
const result = await handleStackOverflow(
|
|
68
|
-
"https://askubuntu.com/questions/445384/what-is-the-difference-between-apt-and-apt-get",
|
|
69
|
-
20,
|
|
70
|
-
);
|
|
71
|
-
expect(result).not.toBeNull();
|
|
72
|
-
expect(result?.method).toBe("stackexchange");
|
|
73
|
-
expect(result?.content).toContain("apt");
|
|
74
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
75
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
76
|
-
expect(result?.notes?.[0]).toContain("site=askubuntu");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// serverfault.com - "What is a reverse proxy" (stable sysadmin topic)
|
|
80
|
-
it("fetches serverfault.com question", async () => {
|
|
81
|
-
const result = await handleStackOverflow(
|
|
82
|
-
"https://serverfault.com/questions/127021/what-is-the-difference-between-a-proxy-and-a-reverse-proxy",
|
|
83
|
-
20,
|
|
84
|
-
);
|
|
85
|
-
expect(result).not.toBeNull();
|
|
86
|
-
expect(result?.method).toBe("stackexchange");
|
|
87
|
-
expect(result?.content).toMatch(/proxy/i);
|
|
88
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
89
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
90
|
-
expect(result?.notes?.[0]).toContain("site=serverfault");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Test with www. prefix
|
|
94
|
-
it("handles www.stackoverflow.com URLs", async () => {
|
|
95
|
-
const result = await handleStackOverflow(
|
|
96
|
-
"https://www.stackoverflow.com/questions/218384/what-is-a-nullpointerexception",
|
|
97
|
-
20,
|
|
98
|
-
);
|
|
99
|
-
expect(result).not.toBeNull();
|
|
100
|
-
expect(result?.method).toBe("stackexchange");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Verify response structure
|
|
104
|
-
it("returns complete response structure", async () => {
|
|
105
|
-
const result = await handleStackOverflow("https://stackoverflow.com/questions/218384", 20);
|
|
106
|
-
expect(result).not.toBeNull();
|
|
107
|
-
expect(result).toHaveProperty("url");
|
|
108
|
-
expect(result).toHaveProperty("finalUrl");
|
|
109
|
-
expect(result).toHaveProperty("contentType", "text/markdown");
|
|
110
|
-
expect(result).toHaveProperty("method", "stackexchange");
|
|
111
|
-
expect(result).toHaveProperty("content");
|
|
112
|
-
expect(result).toHaveProperty("fetchedAt");
|
|
113
|
-
expect(result).toHaveProperty("truncated");
|
|
114
|
-
expect(result).toHaveProperty("notes");
|
|
115
|
-
// Content should have question structure
|
|
116
|
-
expect(result?.content).toContain("# ");
|
|
117
|
-
expect(result?.content).toContain("Score:");
|
|
118
|
-
expect(result?.content).toContain("Tags:");
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { handleCheatSh } from "./cheatsh";
|
|
3
|
-
import { handleRfc } from "./rfc";
|
|
4
|
-
import { handleTldr } from "./tldr";
|
|
5
|
-
|
|
6
|
-
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
7
|
-
|
|
8
|
-
describe.skipIf(SKIP)("handleRfc", () => {
|
|
9
|
-
it("returns null for non-RFC URLs", async () => {
|
|
10
|
-
const result = await handleRfc("https://example.com", 20);
|
|
11
|
-
expect(result).toBeNull();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("returns null for non-matching RFC domains", async () => {
|
|
15
|
-
const result = await handleRfc("https://www.ietf.org/about/", 20);
|
|
16
|
-
expect(result).toBeNull();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("fetches RFC 2616 (HTTP/1.1)", async () => {
|
|
20
|
-
const result = await handleRfc("https://www.rfc-editor.org/rfc/rfc2616", 20);
|
|
21
|
-
expect(result).not.toBeNull();
|
|
22
|
-
expect(result?.method).toBe("rfc");
|
|
23
|
-
expect(result?.content).toContain("HTTP/1.1");
|
|
24
|
-
expect(result?.content).toContain("Hypertext Transfer Protocol");
|
|
25
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
26
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
27
|
-
expect(result?.truncated).toBeDefined();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("fetches RFC 2616 via datatracker URL", async () => {
|
|
31
|
-
const result = await handleRfc("https://datatracker.ietf.org/doc/rfc2616/", 20);
|
|
32
|
-
expect(result).not.toBeNull();
|
|
33
|
-
expect(result?.method).toBe("rfc");
|
|
34
|
-
expect(result?.content).toContain("HTTP/1.1");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("fetches RFC 2616 via tools.ietf.org URL", async () => {
|
|
38
|
-
const result = await handleRfc("https://tools.ietf.org/html/rfc2616", 20);
|
|
39
|
-
expect(result).not.toBeNull();
|
|
40
|
-
expect(result?.method).toBe("rfc");
|
|
41
|
-
expect(result?.content).toContain("HTTP/1.1");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("fetches RFC 793 (TCP)", async () => {
|
|
45
|
-
const result = await handleRfc("https://www.rfc-editor.org/rfc/rfc793", 20);
|
|
46
|
-
expect(result).not.toBeNull();
|
|
47
|
-
expect(result?.method).toBe("rfc");
|
|
48
|
-
expect(result?.content).toContain("Transmission Control Protocol");
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe.skipIf(SKIP)("handleCheatSh", () => {
|
|
53
|
-
it("returns null for non-cheat.sh URLs", async () => {
|
|
54
|
-
const result = await handleCheatSh("https://example.com", 20);
|
|
55
|
-
expect(result).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns null for empty topic", async () => {
|
|
59
|
-
const result = await handleCheatSh("https://cheat.sh/", 20);
|
|
60
|
-
expect(result).toBeNull();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("fetches curl cheatsheet", async () => {
|
|
64
|
-
const result = await handleCheatSh("https://cheat.sh/curl", 20);
|
|
65
|
-
expect(result).not.toBeNull();
|
|
66
|
-
expect(result?.method).toBe("cheat.sh");
|
|
67
|
-
expect(result?.content).toContain("curl");
|
|
68
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
69
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
70
|
-
expect(result?.truncated).toBeDefined();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("fetches tar cheatsheet", async () => {
|
|
74
|
-
const result = await handleCheatSh("https://cheat.sh/tar", 20);
|
|
75
|
-
expect(result).not.toBeNull();
|
|
76
|
-
expect(result?.method).toBe("cheat.sh");
|
|
77
|
-
expect(result?.content).toContain("tar");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("fetches cheatsheet via cht.sh alias", async () => {
|
|
81
|
-
const result = await handleCheatSh("https://cht.sh/curl", 20);
|
|
82
|
-
expect(result).not.toBeNull();
|
|
83
|
-
expect(result?.method).toBe("cheat.sh");
|
|
84
|
-
expect(result?.content).toContain("curl");
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe.skipIf(SKIP)("handleTldr", () => {
|
|
89
|
-
it("returns null for non-tldr URLs", async () => {
|
|
90
|
-
const result = await handleTldr("https://example.com", 20);
|
|
91
|
-
expect(result).toBeNull();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("returns null for nested paths", async () => {
|
|
95
|
-
const result = await handleTldr("https://tldr.sh/nested/path", 20);
|
|
96
|
-
expect(result).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("fetches git tldr page", async () => {
|
|
100
|
-
const result = await handleTldr("https://tldr.sh/git", 20);
|
|
101
|
-
expect(result).not.toBeNull();
|
|
102
|
-
expect(result?.method).toBe("tldr");
|
|
103
|
-
expect(result?.content).toContain("git");
|
|
104
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
105
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
106
|
-
expect(result?.truncated).toBeDefined();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("fetches curl tldr page", async () => {
|
|
110
|
-
const result = await handleTldr("https://tldr.sh/curl", 20);
|
|
111
|
-
expect(result).not.toBeNull();
|
|
112
|
-
expect(result?.method).toBe("tldr");
|
|
113
|
-
expect(result?.content).toContain("curl");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("fetches via tldr.ostera.io alias", async () => {
|
|
117
|
-
const result = await handleTldr("https://tldr.ostera.io/git", 20);
|
|
118
|
-
expect(result).not.toBeNull();
|
|
119
|
-
expect(result?.method).toBe("tldr");
|
|
120
|
-
expect(result?.content).toContain("git");
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { handleWikipedia } from "./wikipedia";
|
|
3
|
-
|
|
4
|
-
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
5
|
-
|
|
6
|
-
describe.skipIf(SKIP)("handleWikipedia", () => {
|
|
7
|
-
it("returns null for non-Wikipedia URLs", async () => {
|
|
8
|
-
const result = await handleWikipedia("https://example.com", 10);
|
|
9
|
-
expect(result).toBeNull();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns null for Wikipedia URLs without /wiki/ path", async () => {
|
|
13
|
-
const result = await handleWikipedia("https://en.wikipedia.org/", 10);
|
|
14
|
-
expect(result).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("fetches a known article with full metadata", async () => {
|
|
18
|
-
// "Computer" is a stable, well-established article
|
|
19
|
-
const result = await handleWikipedia("https://en.wikipedia.org/wiki/Computer", 20);
|
|
20
|
-
expect(result).not.toBeNull();
|
|
21
|
-
expect(result?.method).toBe("wikipedia");
|
|
22
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
23
|
-
expect(result?.content).toContain("Computer");
|
|
24
|
-
expect(result?.url).toBe("https://en.wikipedia.org/wiki/Computer");
|
|
25
|
-
expect(result?.finalUrl).toBe("https://en.wikipedia.org/wiki/Computer");
|
|
26
|
-
expect(result?.truncated).toBe(false);
|
|
27
|
-
expect(result?.notes).toContain("Fetched via Wikipedia API");
|
|
28
|
-
expect(result?.fetchedAt).toBeDefined();
|
|
29
|
-
// Should be a valid ISO timestamp
|
|
30
|
-
expect(() => new Date(result?.fetchedAt ?? "")).not.toThrow();
|
|
31
|
-
// The handler should filter out References and External links sections
|
|
32
|
-
const content = result?.content ?? "";
|
|
33
|
-
const hasReferencesHeading = /^## References$/m.test(content);
|
|
34
|
-
const hasExternalLinksHeading = /^## External links$/m.test(content);
|
|
35
|
-
// At least one of these should be filtered out
|
|
36
|
-
expect(hasReferencesHeading || hasExternalLinksHeading).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("handles different language wikis", async () => {
|
|
40
|
-
// German Wikipedia article for "Computer"
|
|
41
|
-
const result = await handleWikipedia("https://de.wikipedia.org/wiki/Computer", 20);
|
|
42
|
-
expect(result).not.toBeNull();
|
|
43
|
-
expect(result?.method).toBe("wikipedia");
|
|
44
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
45
|
-
expect(result?.content).toContain("Computer");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("handles article with special characters in title", async () => {
|
|
49
|
-
// Article with special characters: "C++"
|
|
50
|
-
const result = await handleWikipedia("https://en.wikipedia.org/wiki/C%2B%2B", 20);
|
|
51
|
-
expect(result).not.toBeNull();
|
|
52
|
-
expect(result?.method).toBe("wikipedia");
|
|
53
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
54
|
-
expect(result?.content).toMatch(/C\+\+/);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("handles article with spaces and parentheses in title", async () => {
|
|
58
|
-
// Artificial intelligence uses underscores for spaces
|
|
59
|
-
const result = await handleWikipedia("https://en.wikipedia.org/wiki/Artificial_intelligence", 20);
|
|
60
|
-
expect(result).not.toBeNull();
|
|
61
|
-
expect(result?.method).toBe("wikipedia");
|
|
62
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
63
|
-
expect(result?.content).toMatch(/[Aa]rtificial intelligence/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("handles non-existent articles gracefully", async () => {
|
|
67
|
-
const result = await handleWikipedia(
|
|
68
|
-
"https://en.wikipedia.org/wiki/ThisArticleDefinitelyDoesNotExist123456789",
|
|
69
|
-
20,
|
|
70
|
-
);
|
|
71
|
-
expect(result).toBeNull();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { handleYouTube } from "./youtube";
|
|
3
|
-
|
|
4
|
-
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
5
|
-
|
|
6
|
-
describe.skipIf(SKIP)("handleYouTube", () => {
|
|
7
|
-
it("returns null for non-YouTube URLs", async () => {
|
|
8
|
-
const result = await handleYouTube("https://example.com", 10);
|
|
9
|
-
expect(result).toBeNull();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("returns null for invalid YouTube URLs", async () => {
|
|
13
|
-
const result = await handleYouTube("https://youtube.com/invalid", 10);
|
|
14
|
-
expect(result).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("handles youtube.com/watch?v= format", async () => {
|
|
18
|
-
// Use Rick Astley's "Never Gonna Give You Up" - a stable, well-known video
|
|
19
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
20
|
-
expect(result).not.toBeNull();
|
|
21
|
-
expect(result?.method).toMatch(/^youtube/);
|
|
22
|
-
expect(result?.contentType).toBe("text/markdown");
|
|
23
|
-
expect(result?.content).toContain("Video ID");
|
|
24
|
-
expect(result?.content).toContain("dQw4w9WgXcQ");
|
|
25
|
-
}, 30000);
|
|
26
|
-
|
|
27
|
-
it("handles youtu.be/ short format", async () => {
|
|
28
|
-
const result = await handleYouTube("https://youtu.be/dQw4w9WgXcQ", 30);
|
|
29
|
-
expect(result).not.toBeNull();
|
|
30
|
-
expect(result?.method).toMatch(/^youtube/);
|
|
31
|
-
expect(result?.content).toContain("dQw4w9WgXcQ");
|
|
32
|
-
}, 30000);
|
|
33
|
-
|
|
34
|
-
it("handles youtube.com/shorts/ format", async () => {
|
|
35
|
-
// Use a stable YouTube Shorts video
|
|
36
|
-
const result = await handleYouTube("https://www.youtube.com/shorts/jNQXAC9IVRw", 30);
|
|
37
|
-
expect(result).not.toBeNull();
|
|
38
|
-
expect(result?.method).toMatch(/^youtube/);
|
|
39
|
-
expect(result?.content).toContain("jNQXAC9IVRw");
|
|
40
|
-
}, 30000);
|
|
41
|
-
|
|
42
|
-
it("handles youtube.com/embed/ format", async () => {
|
|
43
|
-
const result = await handleYouTube("https://www.youtube.com/embed/dQw4w9WgXcQ", 30);
|
|
44
|
-
expect(result).not.toBeNull();
|
|
45
|
-
expect(result?.method).toMatch(/^youtube/);
|
|
46
|
-
expect(result?.content).toContain("dQw4w9WgXcQ");
|
|
47
|
-
}, 30000);
|
|
48
|
-
|
|
49
|
-
it("handles youtube.com/v/ format", async () => {
|
|
50
|
-
const result = await handleYouTube("https://www.youtube.com/v/dQw4w9WgXcQ", 30);
|
|
51
|
-
expect(result).not.toBeNull();
|
|
52
|
-
expect(result?.method).toMatch(/^youtube/);
|
|
53
|
-
expect(result?.content).toContain("dQw4w9WgXcQ");
|
|
54
|
-
}, 30000);
|
|
55
|
-
|
|
56
|
-
it("handles m.youtube.com mobile URLs", async () => {
|
|
57
|
-
const result = await handleYouTube("https://m.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
58
|
-
expect(result).not.toBeNull();
|
|
59
|
-
expect(result?.method).toMatch(/^youtube/);
|
|
60
|
-
expect(result?.content).toContain("dQw4w9WgXcQ");
|
|
61
|
-
}, 30000);
|
|
62
|
-
|
|
63
|
-
it("extracts video metadata when yt-dlp is available", async () => {
|
|
64
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
65
|
-
expect(result).not.toBeNull();
|
|
66
|
-
|
|
67
|
-
// If yt-dlp is available, should have metadata
|
|
68
|
-
if (result?.method === "youtube") {
|
|
69
|
-
expect(result.content).toContain("Video ID");
|
|
70
|
-
expect(result.content).toContain("Channel");
|
|
71
|
-
// May have duration, views, upload date, etc.
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// If yt-dlp is not available, should indicate that
|
|
75
|
-
if (result?.method === "youtube-no-ytdlp") {
|
|
76
|
-
expect(result.content).toContain("yt-dlp could not be installed");
|
|
77
|
-
expect(result.notes).toContain("yt-dlp installation failed");
|
|
78
|
-
}
|
|
79
|
-
}, 30000);
|
|
80
|
-
|
|
81
|
-
it("handles videos with transcripts gracefully", async () => {
|
|
82
|
-
// This video should have captions
|
|
83
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
84
|
-
expect(result).not.toBeNull();
|
|
85
|
-
|
|
86
|
-
if (result?.method === "youtube") {
|
|
87
|
-
// Either has transcript or explicitly notes it's not available
|
|
88
|
-
const hasTranscript = result.content.includes("Transcript");
|
|
89
|
-
const noTranscriptNote = result.content.includes("No transcript available");
|
|
90
|
-
expect(hasTranscript || noTranscriptNote).toBe(true);
|
|
91
|
-
}
|
|
92
|
-
}, 30000);
|
|
93
|
-
|
|
94
|
-
it("handles videos without transcripts gracefully", async () => {
|
|
95
|
-
// Many music videos lack captions, but this is not guaranteed
|
|
96
|
-
// Just verify the handler doesn't crash and provides some info
|
|
97
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=kJQP7kiw5Fk", 30);
|
|
98
|
-
expect(result).not.toBeNull();
|
|
99
|
-
|
|
100
|
-
if (result?.method === "youtube") {
|
|
101
|
-
// Should still have basic metadata
|
|
102
|
-
expect(result.content).toContain("Video ID");
|
|
103
|
-
}
|
|
104
|
-
}, 30000);
|
|
105
|
-
|
|
106
|
-
it("returns appropriate response when yt-dlp is not available", async () => {
|
|
107
|
-
// We can't force yt-dlp to be unavailable in tests, but we can verify
|
|
108
|
-
// the return structure matches expectations for both cases
|
|
109
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
110
|
-
expect(result).not.toBeNull();
|
|
111
|
-
|
|
112
|
-
// Should have one of these two methods
|
|
113
|
-
expect(["youtube", "youtube-no-ytdlp"]).toContain(result!.method);
|
|
114
|
-
|
|
115
|
-
// Both should have required fields
|
|
116
|
-
expect(result?.url).toBe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
|
117
|
-
expect(result?.finalUrl).toContain("youtube.com");
|
|
118
|
-
expect(result?.fetchedAt).toBeTruthy();
|
|
119
|
-
expect(typeof result?.truncated).toBe("boolean");
|
|
120
|
-
expect(Array.isArray(result?.notes)).toBe(true);
|
|
121
|
-
}, 30000);
|
|
122
|
-
|
|
123
|
-
it("normalizes video URLs to canonical format", async () => {
|
|
124
|
-
// Different input formats should normalize to same canonical URL
|
|
125
|
-
const result = await handleYouTube("https://youtu.be/dQw4w9WgXcQ", 30);
|
|
126
|
-
expect(result).not.toBeNull();
|
|
127
|
-
expect(result?.finalUrl).toBe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
|
128
|
-
}, 30000);
|
|
129
|
-
|
|
130
|
-
it("handles playlist URLs by extracting video ID", async () => {
|
|
131
|
-
const result = await handleYouTube(
|
|
132
|
-
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf",
|
|
133
|
-
30,
|
|
134
|
-
);
|
|
135
|
-
expect(result).not.toBeNull();
|
|
136
|
-
expect(result?.content).toContain("dQw4w9WgXcQ");
|
|
137
|
-
}, 30000);
|
|
138
|
-
|
|
139
|
-
it("includes subtitle source information when available", async () => {
|
|
140
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
141
|
-
|
|
142
|
-
if (result?.method === "youtube") {
|
|
143
|
-
// If transcript is present, should note the source
|
|
144
|
-
const hasManualNote = result.notes.includes("Using manual subtitles");
|
|
145
|
-
const hasAutoNote = result.notes.includes("Using auto-generated captions");
|
|
146
|
-
const hasNoSubsNote = result.notes.includes("No subtitles/captions available");
|
|
147
|
-
|
|
148
|
-
// Should have exactly one of these
|
|
149
|
-
const noteCount = [hasManualNote, hasAutoNote, hasNoSubsNote].filter(Boolean).length;
|
|
150
|
-
expect(noteCount).toBeGreaterThanOrEqual(1);
|
|
151
|
-
}
|
|
152
|
-
}, 30000);
|
|
153
|
-
|
|
154
|
-
it("formats duration in human readable format", async () => {
|
|
155
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
156
|
-
|
|
157
|
-
if (result?.method === "youtube" && result.content.includes("Duration")) {
|
|
158
|
-
// Should have duration in M:SS or H:MM:SS format
|
|
159
|
-
expect(result.content).toMatch(/Duration.*\d+:\d{2}/);
|
|
160
|
-
}
|
|
161
|
-
}, 30000);
|
|
162
|
-
|
|
163
|
-
it("formats view count in readable format", async () => {
|
|
164
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
165
|
-
|
|
166
|
-
if (result?.method === "youtube" && result.content.includes("Views")) {
|
|
167
|
-
// Should have views formatted (e.g., 1.5B, 100M, 10.5K)
|
|
168
|
-
expect(result.content).toMatch(/Views.*\d+(\.\d+)?[KM]?/);
|
|
169
|
-
}
|
|
170
|
-
}, 30000);
|
|
171
|
-
|
|
172
|
-
it("includes upload date when available", async () => {
|
|
173
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
174
|
-
|
|
175
|
-
if (result?.method === "youtube" && result.content.includes("Uploaded")) {
|
|
176
|
-
// Should have date in YYYY-MM-DD format
|
|
177
|
-
expect(result.content).toMatch(/Uploaded.*\d{4}-\d{2}-\d{2}/);
|
|
178
|
-
}
|
|
179
|
-
}, 30000);
|
|
180
|
-
|
|
181
|
-
it("truncates long descriptions", async () => {
|
|
182
|
-
const result = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
183
|
-
|
|
184
|
-
if (result?.method === "youtube" && result.content.includes("Description")) {
|
|
185
|
-
// Description section should exist
|
|
186
|
-
expect(result.content).toContain("## Description");
|
|
187
|
-
}
|
|
188
|
-
}, 30000);
|
|
189
|
-
|
|
190
|
-
it("handles www prefix variations", async () => {
|
|
191
|
-
const withWww = await handleYouTube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
192
|
-
const withoutWww = await handleYouTube("https://youtube.com/watch?v=dQw4w9WgXcQ", 30);
|
|
193
|
-
|
|
194
|
-
expect(withWww).not.toBeNull();
|
|
195
|
-
expect(withoutWww).not.toBeNull();
|
|
196
|
-
expect(withWww?.finalUrl).toBe(withoutWww?.finalUrl);
|
|
197
|
-
}, 30000);
|
|
198
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { parseFrontmatter } from "../core/frontmatter";
|
|
3
|
-
|
|
4
|
-
describe("parseFrontmatter", () => {
|
|
5
|
-
const parse = (content: string) => parseFrontmatter(content, { source: "tests:frontmatter", level: "off" });
|
|
6
|
-
|
|
7
|
-
test("parses simple key-value pairs", () => {
|
|
8
|
-
const content = `---
|
|
9
|
-
name: test
|
|
10
|
-
enabled: true
|
|
11
|
-
---
|
|
12
|
-
Body content`;
|
|
13
|
-
|
|
14
|
-
const result = parse(content);
|
|
15
|
-
expect(result.frontmatter).toEqual({ name: "test", enabled: true });
|
|
16
|
-
expect(result.body).toBe("Body content");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("parses YAML list syntax", () => {
|
|
20
|
-
const content = `---
|
|
21
|
-
tags:
|
|
22
|
-
- javascript
|
|
23
|
-
- typescript
|
|
24
|
-
- react
|
|
25
|
-
---
|
|
26
|
-
Body content`;
|
|
27
|
-
|
|
28
|
-
const result = parse(content);
|
|
29
|
-
expect(result.frontmatter).toEqual({
|
|
30
|
-
tags: ["javascript", "typescript", "react"],
|
|
31
|
-
});
|
|
32
|
-
expect(result.body).toBe("Body content");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("parses multi-line string values", () => {
|
|
36
|
-
const content = `---
|
|
37
|
-
description: |
|
|
38
|
-
This is a multi-line
|
|
39
|
-
description block
|
|
40
|
-
with several lines
|
|
41
|
-
---
|
|
42
|
-
Body content`;
|
|
43
|
-
|
|
44
|
-
const result = parse(content);
|
|
45
|
-
expect(result.frontmatter).toEqual({
|
|
46
|
-
description: "This is a multi-line\ndescription block\nwith several lines\n",
|
|
47
|
-
});
|
|
48
|
-
expect(result.body).toBe("Body content");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("parses nested objects", () => {
|
|
52
|
-
const content = `---
|
|
53
|
-
config:
|
|
54
|
-
server:
|
|
55
|
-
port: 3000
|
|
56
|
-
host: localhost
|
|
57
|
-
database:
|
|
58
|
-
name: mydb
|
|
59
|
-
---
|
|
60
|
-
Body content`;
|
|
61
|
-
|
|
62
|
-
const result = parse(content);
|
|
63
|
-
expect(result.frontmatter).toEqual({
|
|
64
|
-
config: {
|
|
65
|
-
server: { port: 3000, host: "localhost" },
|
|
66
|
-
database: { name: "mydb" },
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
expect(result.body).toBe("Body content");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("parses mixed complex YAML", () => {
|
|
73
|
-
const content = `---
|
|
74
|
-
name: complex-test
|
|
75
|
-
version: 1.0.0
|
|
76
|
-
tags:
|
|
77
|
-
- prod
|
|
78
|
-
- critical
|
|
79
|
-
metadata:
|
|
80
|
-
author: tester
|
|
81
|
-
created: 2024-01-01
|
|
82
|
-
description: |
|
|
83
|
-
Multi-line description
|
|
84
|
-
with formatting
|
|
85
|
-
---
|
|
86
|
-
Body content`;
|
|
87
|
-
|
|
88
|
-
const result = parse(content);
|
|
89
|
-
expect(result.frontmatter).toEqual({
|
|
90
|
-
name: "complex-test",
|
|
91
|
-
version: "1.0.0",
|
|
92
|
-
tags: ["prod", "critical"],
|
|
93
|
-
metadata: {
|
|
94
|
-
author: "tester",
|
|
95
|
-
created: "2024-01-01",
|
|
96
|
-
},
|
|
97
|
-
description: "Multi-line description\nwith formatting\n",
|
|
98
|
-
});
|
|
99
|
-
expect(result.body).toBe("Body content");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("handles missing frontmatter", () => {
|
|
103
|
-
const content = "Just body content";
|
|
104
|
-
const result = parse(content);
|
|
105
|
-
expect(result.frontmatter).toEqual({});
|
|
106
|
-
expect(result.body).toBe("Just body content");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("handles invalid YAML in frontmatter", () => {
|
|
110
|
-
const content = `---
|
|
111
|
-
invalid: [unclosed array
|
|
112
|
-
---
|
|
113
|
-
Body content`;
|
|
114
|
-
|
|
115
|
-
const result = parse(content);
|
|
116
|
-
// Simple fallback parser extracts key:value pairs it can parse
|
|
117
|
-
expect(result.frontmatter).toEqual({ invalid: "[unclosed array" });
|
|
118
|
-
// Body is still extracted even with invalid YAML
|
|
119
|
-
expect(result.body).toBe("Body content");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("handles empty frontmatter", () => {
|
|
123
|
-
const content = `---
|
|
124
|
-
---
|
|
125
|
-
Body content`;
|
|
126
|
-
|
|
127
|
-
const result = parse(content);
|
|
128
|
-
expect(result.frontmatter).toEqual({});
|
|
129
|
-
expect(result.body).toBe("Body content");
|
|
130
|
-
});
|
|
131
|
-
});
|