@danypops/pi-web-spider 0.10.4 → 0.11.0
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/LICENSE +21 -0
- package/package.json +14 -4
- package/src/constants.ts +7 -0
- package/src/daemon-client.ts +190 -0
- package/src/index.ts +346 -351
- package/src/presentation.ts +430 -0
- package/test/cache-paths.test.ts +38 -91
- package/test/daemon-client.test.ts +158 -0
- package/test/daemon-isolation.ts +56 -0
- package/test/e2e-jiti.test.ts +46 -56
- package/test/execute-contract.test.ts +62 -32
- package/test/fixtures/mock-pi-cli.mjs +5 -9
- package/test/harness/extension-harness.ts +554 -0
- package/test/harness/index.ts +10 -0
- package/test/harness/jiti-native-modules.ts +13 -0
- package/test/helpers/fixture-server.ts +48 -0
- package/test/live-fetch.test.ts +10 -3
- package/test/load.test.ts +13 -3
- package/test/paths.test.ts +61 -96
- package/test/presentation.test.ts +190 -0
- package/src/format.ts +0 -142
- package/test/format.test.ts +0 -373
- package/test/playwright-fallback.test.ts +0 -248
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright fallback path — error propagation and happy-path coverage.
|
|
3
|
-
*
|
|
4
|
-
* The extension has two Playwright entry points:
|
|
5
|
-
*
|
|
6
|
-
* 1. Auto-fallback — triggered when spider() detects jsRendered:true and
|
|
7
|
-
* params.enhanced is false. The extension retries with
|
|
8
|
-
* a PlaywrightHttpClient transparently.
|
|
9
|
-
*
|
|
10
|
-
* 2. enhanced=true — caller explicitly opts into the headless browser for
|
|
11
|
-
* the first (and only) fetch attempt.
|
|
12
|
-
*
|
|
13
|
-
* Both paths must:
|
|
14
|
-
* - Return a well-formed page on success.
|
|
15
|
-
* - Return { error: string } when the Playwright client throws — never crash,
|
|
16
|
-
* never leak the raw exception type.
|
|
17
|
-
*
|
|
18
|
-
* We use vi.mock + importActual to replace only PlaywrightHttpClient while
|
|
19
|
-
* keeping the real spider(), crawl(), etc. alive. The mock is a controllable
|
|
20
|
-
* stub whose fetch() behaviour is set per-test via a shared ref.
|
|
21
|
-
*
|
|
22
|
-
* Fixture: fixtures/gh-shell.html — a GitHub-like minimal app shell.
|
|
23
|
-
* Readability finds no article content → jsRendered:true.
|
|
24
|
-
* The real article HTML from fixtures/article-with-images.html is returned
|
|
25
|
-
* by the stub Playwright client to simulate a successful JS-rendered fetch.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { readFileSync } from "node:fs"
|
|
29
|
-
import { dirname, join } from "node:path"
|
|
30
|
-
import { fileURLToPath } from "node:url"
|
|
31
|
-
import {
|
|
32
|
-
afterEach,
|
|
33
|
-
beforeEach,
|
|
34
|
-
describe,
|
|
35
|
-
expect,
|
|
36
|
-
it,
|
|
37
|
-
vi,
|
|
38
|
-
} from "vitest"
|
|
39
|
-
import {
|
|
40
|
-
createExtensionHarness,
|
|
41
|
-
type ExtensionHarness,
|
|
42
|
-
} from "@earendil-works/pi-coding-agent/testing"
|
|
43
|
-
import type { HttpRequest, HttpResponse } from "@danypops/web-spider"
|
|
44
|
-
|
|
45
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
46
|
-
const FIXTURES = join(__dirname, "../../web-spider/fixtures")
|
|
47
|
-
|
|
48
|
-
const GH_SHELL_HTML = readFileSync(join(FIXTURES, "gh-shell.html"), "utf8")
|
|
49
|
-
const ARTICLE_HTML = readFileSync(join(FIXTURES, "article-with-images.html"), "utf8")
|
|
50
|
-
|
|
51
|
-
const MOCK_URL = "https://github.com/hyprwm/aquamarine/issues"
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Controllable Playwright stub
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Shared ref so each test can swap the Playwright behaviour without
|
|
59
|
-
* reloading the module mock.
|
|
60
|
-
*/
|
|
61
|
-
let playwrightFetchImpl: (req: HttpRequest) => Promise<HttpResponse> = async () => {
|
|
62
|
-
throw new Error("playwrightFetchImpl not set — call setPlaywrightBehaviour() in each test")
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function makeOkResponse(html: string): HttpResponse {
|
|
66
|
-
return {
|
|
67
|
-
ok: true,
|
|
68
|
-
status: 200,
|
|
69
|
-
statusText: "OK",
|
|
70
|
-
headers: { get: () => null },
|
|
71
|
-
text: async () => html,
|
|
72
|
-
arrayBuffer: async () => new ArrayBuffer(0),
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Replace PlaywrightHttpClient with a stub whose fetch() delegates to the
|
|
77
|
-
// per-test ref. All other exports are real (importActual).
|
|
78
|
-
vi.mock("@danypops/web-spider", async (importActual) => {
|
|
79
|
-
const real = await importActual<typeof import("@danypops/web-spider")>()
|
|
80
|
-
class StubPlaywrightHttpClient {
|
|
81
|
-
async fetch(req: HttpRequest): Promise<HttpResponse> {
|
|
82
|
-
return playwrightFetchImpl(req)
|
|
83
|
-
}
|
|
84
|
-
async close() {}
|
|
85
|
-
}
|
|
86
|
-
return { ...real, PlaywrightHttpClient: StubPlaywrightHttpClient }
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// Global fetch mock — returns the gh-shell for the target URL, robots + 404
|
|
91
|
-
// for everything else.
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
function installFetchMock(pageHtml = GH_SHELL_HTML) {
|
|
95
|
-
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
|
|
96
|
-
const url = input.toString()
|
|
97
|
-
if (url.includes("robots.txt")) return makeOkResponse("User-agent: *\nAllow: /")
|
|
98
|
-
if (url.includes("sitemap")) return makeOkResponse("") // triggers 200 → empty
|
|
99
|
-
if (url.startsWith(MOCK_URL)) return makeOkResponse(pageHtml)
|
|
100
|
-
return makeOkResponse("")
|
|
101
|
-
}))
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Harness
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
let h: ExtensionHarness
|
|
109
|
-
|
|
110
|
-
beforeEach(async () => {
|
|
111
|
-
const { default: factory } = await import("../src/index.js")
|
|
112
|
-
// Unique path per test — prevents cache hits from a previous test's successful
|
|
113
|
-
// fetch from bypassing Playwright in subsequent error-propagation tests.
|
|
114
|
-
// Without isolation the first test caches the page, later tests return it
|
|
115
|
-
// immediately from DiskCache.load(), and Playwright is never invoked.
|
|
116
|
-
const cachePath = `/tmp/ws-playwright-fallback-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
|
|
117
|
-
h = createExtensionHarness(factory, {
|
|
118
|
-
cwd: "/tmp",
|
|
119
|
-
env: { WEB_SPIDER_CACHE_PATH: cachePath },
|
|
120
|
-
})
|
|
121
|
-
await h.boot()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
afterEach(async () => {
|
|
125
|
-
await h.shutdown()
|
|
126
|
-
vi.restoreAllMocks()
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Auto-fallback — jsRendered:true → retry with Playwright
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
describe("auto-fallback: jsRendered:true → Playwright retry", () => {
|
|
134
|
-
it("returns article content when Playwright succeeds", async () => {
|
|
135
|
-
installFetchMock(GH_SHELL_HTML)
|
|
136
|
-
playwrightFetchImpl = async () => makeOkResponse(ARTICLE_HTML)
|
|
137
|
-
|
|
138
|
-
const result = await h.invokeTool("web_fetch", {
|
|
139
|
-
url: MOCK_URL,
|
|
140
|
-
format: "lean",
|
|
141
|
-
}) as { content: { text: string }[] }
|
|
142
|
-
|
|
143
|
-
const text = JSON.parse(result.content[0].text)
|
|
144
|
-
expect(text).not.toHaveProperty("error")
|
|
145
|
-
expect(text).toHaveProperty("title")
|
|
146
|
-
expect(text.title.length).toBeGreaterThan(0)
|
|
147
|
-
expect(text).toHaveProperty("wordCount")
|
|
148
|
-
expect(text.wordCount).toBeGreaterThan(0)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it("propagates error cleanly when Playwright throws a generic error", async () => {
|
|
152
|
-
installFetchMock(GH_SHELL_HTML)
|
|
153
|
-
playwrightFetchImpl = async () => { throw new Error("Browser closed unexpectedly") }
|
|
154
|
-
|
|
155
|
-
const result = await h.invokeTool("web_fetch", { url: MOCK_URL }) as { content: { text: string }[] }
|
|
156
|
-
const text = JSON.parse(result.content[0].text)
|
|
157
|
-
|
|
158
|
-
expect(text).toHaveProperty("error")
|
|
159
|
-
expect(typeof text.error).toBe("string")
|
|
160
|
-
expect(text.error).toContain("Browser closed unexpectedly")
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it("propagates error cleanly when Playwright throws 'Map operation called on non-Map object'", async () => {
|
|
164
|
-
installFetchMock(GH_SHELL_HTML)
|
|
165
|
-
playwrightFetchImpl = async () => { throw new TypeError("Map operation called on non-Map object") }
|
|
166
|
-
|
|
167
|
-
const result = await h.invokeTool("web_fetch", { url: MOCK_URL }) as { content: { text: string }[] }
|
|
168
|
-
const text = JSON.parse(result.content[0].text)
|
|
169
|
-
|
|
170
|
-
expect(text).toHaveProperty("error")
|
|
171
|
-
expect(typeof text.error).toBe("string")
|
|
172
|
-
expect(text.error).toContain("Map operation called on non-Map object")
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it("propagates error cleanly when Playwright throws a timeout", async () => {
|
|
176
|
-
installFetchMock(GH_SHELL_HTML)
|
|
177
|
-
playwrightFetchImpl = async () => { throw new Error("Timeout 30000ms exceeded.") }
|
|
178
|
-
|
|
179
|
-
const result = await h.invokeTool("web_fetch", { url: MOCK_URL }) as { content: { text: string }[] }
|
|
180
|
-
const text = JSON.parse(result.content[0].text)
|
|
181
|
-
|
|
182
|
-
expect(text).toHaveProperty("error")
|
|
183
|
-
expect(text.error).toContain("Timeout")
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it("propagates error cleanly when Playwright throws a non-Error value", async () => {
|
|
187
|
-
installFetchMock(GH_SHELL_HTML)
|
|
188
|
-
playwrightFetchImpl = async () => { throw "chromium launch failed" }
|
|
189
|
-
|
|
190
|
-
const result = await h.invokeTool("web_fetch", { url: MOCK_URL }) as { content: { text: string }[] }
|
|
191
|
-
const text = JSON.parse(result.content[0].text)
|
|
192
|
-
|
|
193
|
-
expect(text).toHaveProperty("error")
|
|
194
|
-
expect(typeof text.error).toBe("string")
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it("does not call Playwright when direct fetch returns readable content", async () => {
|
|
198
|
-
// ARTICLE_HTML has enough content for Readability → jsRendered stays false
|
|
199
|
-
installFetchMock(ARTICLE_HTML)
|
|
200
|
-
playwrightFetchImpl = async () => {
|
|
201
|
-
throw new Error("Playwright should NOT have been called for a readable page")
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const result = await h.invokeTool("web_fetch", {
|
|
205
|
-
url: MOCK_URL,
|
|
206
|
-
format: "lean",
|
|
207
|
-
}) as { content: { text: string }[] }
|
|
208
|
-
|
|
209
|
-
const text = JSON.parse(result.content[0].text)
|
|
210
|
-
expect(text).not.toHaveProperty("error")
|
|
211
|
-
expect(text.wordCount).toBeGreaterThan(0)
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
// enhanced=true — Playwright is used for the first fetch, no fallback needed
|
|
217
|
-
// ---------------------------------------------------------------------------
|
|
218
|
-
|
|
219
|
-
describe("enhanced=true: Playwright used for initial fetch", () => {
|
|
220
|
-
it("returns content when Playwright succeeds", async () => {
|
|
221
|
-
installFetchMock() // fetch mock won't be called for the main page
|
|
222
|
-
playwrightFetchImpl = async () => makeOkResponse(ARTICLE_HTML)
|
|
223
|
-
|
|
224
|
-
const result = await h.invokeTool("web_fetch", {
|
|
225
|
-
url: MOCK_URL,
|
|
226
|
-
format: "lean",
|
|
227
|
-
enhanced: true,
|
|
228
|
-
}) as { content: { text: string }[] }
|
|
229
|
-
|
|
230
|
-
const text = JSON.parse(result.content[0].text)
|
|
231
|
-
expect(text).not.toHaveProperty("error")
|
|
232
|
-
expect(text.wordCount).toBeGreaterThan(0)
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it("returns { error } when Playwright throws — does not crash", async () => {
|
|
236
|
-
installFetchMock()
|
|
237
|
-
playwrightFetchImpl = async () => { throw new Error("executable doesn't exist at /nonexistent") }
|
|
238
|
-
|
|
239
|
-
const result = await h.invokeTool("web_fetch", {
|
|
240
|
-
url: MOCK_URL,
|
|
241
|
-
enhanced: true,
|
|
242
|
-
}) as { content: { text: string }[] }
|
|
243
|
-
|
|
244
|
-
const text = JSON.parse(result.content[0].text)
|
|
245
|
-
expect(text).toHaveProperty("error")
|
|
246
|
-
expect(typeof text.error).toBe("string")
|
|
247
|
-
})
|
|
248
|
-
})
|