Compare commits

...

6 Commits

6 changed files with 321 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
# Live Parser Tests Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add explicit live endpoint test suites for each core marketplace scraper, excluded from default tests and runnable through one script.
**Architecture:** Live tests live under `packages/core/test/live/` and import public scraper entry points directly. Normal package tests remain offline because the new files are outside current explicit test commands and run only through `bun run test:live`.
**Tech Stack:** Bun `1.3.13`, `bun:test`, TypeScript, existing core scraper APIs.
---
## File Structure
- Create `packages/core/test/live/ebay.live.test.ts`: live eBay search smoke test against `fetchEbayItems`.
- Create `packages/core/test/live/kijiji.live.test.ts`: live Kijiji search smoke test against `fetchKijijiItems`.
- Create `packages/core/test/live/facebook.live.test.ts`: strict live Facebook search smoke test against `fetchFacebookItems` and `FACEBOOK_COOKIE`.
- Modify `package.json`: add root script `test:live` running all files under `packages/core/test/live`.
### Task 1: Add eBay Live Suite
**Files:**
- Create: `packages/core/test/live/ebay.live.test.ts`
- [ ] **Step 1: Write the live test file**
```ts
import { describe, expect, test } from "bun:test";
import fetchEbayItems from "../../src/scrapers/ebay";
describe("eBay live parser", () => {
test("scrapes live search results into listing details", async () => {
const results = await fetchEbayItems("iphone", 1, { maxItems: 3 });
expect(results.length).toBeGreaterThan(0);
for (const listing of results) {
expect(listing.url).toStartWith("https://");
expect(listing.title.length).toBeGreaterThan(0);
expect(listing.listingPrice.cents).toBeGreaterThanOrEqual(0);
expect(listing.listingPrice.currency.length).toBeGreaterThan(0);
}
});
});
```
- [ ] **Step 2: Run eBay live test**
Run: `bun test packages/core/test/live/ebay.live.test.ts`
Expected: PASS when eBay returns parseable search results; FAIL on endpoint/rate-limit/parser breakage.
### Task 2: Add Kijiji Live Suite
**Files:**
- Create: `packages/core/test/live/kijiji.live.test.ts`
- [ ] **Step 1: Write the live test file**
```ts
import { describe, expect, test } from "bun:test";
import fetchKijijiItems from "../../src/scrapers/kijiji";
describe("Kijiji live parser", () => {
test("scrapes live search results into detailed listings", async () => {
const results = await fetchKijijiItems(
"iphone",
1,
"https://www.kijiji.ca",
{ maxPages: 1 },
{ includeImages: false, sellerDataDepth: "basic" },
);
expect(results.length).toBeGreaterThan(0);
for (const listing of results) {
expect(listing.url).toStartWith("https://www.kijiji.ca/");
expect(listing.title.length).toBeGreaterThan(0);
expect(listing.listingPrice.cents).toBeGreaterThanOrEqual(0);
expect(listing.listingPrice.currency.length).toBeGreaterThan(0);
}
});
});
```
- [ ] **Step 2: Run Kijiji live test**
Run: `bun test packages/core/test/live/kijiji.live.test.ts`
Expected: PASS when Kijiji returns parseable search and detail pages; FAIL on endpoint/parser breakage.
### Task 3: Add Facebook Live Suite
**Files:**
- Create: `packages/core/test/live/facebook.live.test.ts`
- [ ] **Step 1: Write the live test file**
```ts
import { describe, expect, test } from "bun:test";
import fetchFacebookItems from "../../src/scrapers/facebook";
describe("Facebook live parser", () => {
test("requires FACEBOOK_COOKIE for strict live testing", () => {
expect(process.env.FACEBOOK_COOKIE?.trim().length ?? 0).toBeGreaterThan(0);
});
test("scrapes live marketplace search results into listing details", async () => {
const results = await fetchFacebookItems("iphone", 1, "toronto", 3);
expect(results.length).toBeGreaterThan(0);
for (const listing of results) {
expect(listing.url).toStartWith("https://www.facebook.com/marketplace/item/");
expect(listing.title.length).toBeGreaterThan(0);
expect(listing.listingPrice.cents).toBeGreaterThanOrEqual(0);
expect(listing.listingPrice.currency.length).toBeGreaterThan(0);
}
});
});
```
- [ ] **Step 2: Run Facebook live test**
Run: `bun test packages/core/test/live/facebook.live.test.ts`
Expected: PASS with valid `FACEBOOK_COOKIE`; FAIL when `FACEBOOK_COOKIE` is missing, expired, or parser output is empty.
### Task 4: Add Root Live Test Script
**Files:**
- Modify: `package.json`
- [ ] **Step 1: Add script**
Change root `scripts` to include:
```json
{
"test:live": "bun test packages/core/test/live"
}
```
- [ ] **Step 2: Run all live tests through script**
Run: `bun run test:live`
Expected: runs eBay, Kijiji, and Facebook live suites. Facebook fails if `FACEBOOK_COOKIE` is unset.
### Task 5: Verify Default Suite Exclusion
**Files:**
- No code files modified.
- [ ] **Step 1: Run existing core tests**
Run: `bun test packages/core/test`
Expected: existing mocked tests run. If Bun discovers `packages/core/test/live`, change normal verification command to explicit glob `bun test packages/core/test/*.test.ts` and document that in final notes.
- [ ] **Step 2: Run static checks**
Run: `bun run ci`
Expected: typecheck and Biome pass. Fix code issues without changing lint or TypeScript rules.
## Commit Note
Do not commit during execution unless user explicitly requests a commit. This repo session policy overrides generic plan commit steps.
## Self-Review
- Spec coverage: eBay, Kijiji, Facebook live suites; explicit script; strict Facebook auth; excluded from default flow.
- Placeholder scan: no `TBD`, `TODO`, or underspecified implementation steps.
- Type consistency: tests use current exported scraper signatures and shared listing fields from `ListingDetails`.

View File

@@ -0,0 +1,37 @@
# Live Parser Tests Design
## Summary
Add explicit live endpoint tests for each core scraper parser path. These tests are excluded from normal deterministic test commands and run only through a dedicated package script.
## Scope
- Add one live suite per parser: eBay, Kijiji, Facebook.
- Place suites under `packages/core/test/live/` so normal `bun test packages/core/test/*.test.ts` patterns do not include them accidentally.
- Add a root `test:live` script that runs all live suites together.
- Keep existing mocked tests unchanged.
## Behavior
- Each suite calls the public scraper entry point for that marketplace with a narrow query and low max item count.
- Assertions verify scrape output shape and parser viability, not exact listing identity.
- eBay and Kijiji require live network access and fail on endpoint/parser breakage.
- Facebook is strict: missing or expired `FACEBOOK_COOKIE` fails the live suite instead of skipping.
## Test Data
- Use stable broad Canadian queries such as `iphone` or `laptop` to reduce empty-result risk.
- Use low limits to avoid unnecessary load and rate-limit pressure.
- Avoid exact prices, titles, listing IDs, or ordering assumptions.
## Failure Meaning
- Empty result arrays fail because live parser logic did not produce usable listings.
- Missing required fields fail because adapter contracts depend on those fields.
- Authentication failures fail for Facebook because selected scope is strict.
## Verification
- Normal suite remains offline: `bun test packages/core/test`.
- Live suite runs by explicit script: `bun run test:live`.
- Full static checks remain via `bun run ci`.

View File

@@ -12,6 +12,7 @@
"build:mcp": "bun build ./packages/mcp-server/src/index.ts --target=bun --outdir=./dist/mcp --minify", "build:mcp": "bun build ./packages/mcp-server/src/index.ts --target=bun --outdir=./dist/mcp --minify",
"build:all": "bun run build:api && bun run build:mcp", "build:all": "bun run build:api && bun run build:mcp",
"ci": "bun run typecheck && biome check --write", "ci": "bun run typecheck && biome check --write",
"test:live": "bun test --cwd packages/core test/live",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"start": "./scripts/start.sh" "start": "./scripts/start.sh"
}, },

View File

@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import fetchEbayItems from "../../src/scrapers/ebay";
const LIVE_RESULT_LIMIT = 3;
const LIVE_TEST_TIMEOUT_MS = 30_000;
describe("eBay live parser", () => {
test(
"scrapes live search results into listing details",
async () => {
const results = await fetchEbayItems("iphone", 1, {
maxItems: LIVE_RESULT_LIMIT,
});
expect(results.length).toBeGreaterThan(0);
for (const listing of results) {
if (!listing.listingPrice) {
throw new Error(`Expected listing price for ${listing.url}`);
}
if (typeof listing.listingPrice.cents !== "number") {
throw new Error(`Expected listing cents for ${listing.url}`);
}
if (!listing.listingPrice.currency) {
throw new Error(`Expected listing currency for ${listing.url}`);
}
expect(listing.url).toStartWith("https://");
expect(listing.title.length).toBeGreaterThan(0);
expect(listing.listingPrice.cents).toBeGreaterThanOrEqual(0);
expect(listing.listingPrice.currency.length).toBeGreaterThan(0);
}
},
LIVE_TEST_TIMEOUT_MS,
);
});

View File

@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test";
import fetchFacebookItems from "../../src/scrapers/facebook";
const LIVE_RESULT_LIMIT = 3;
const LIVE_TEST_TIMEOUT_MS = 30_000;
describe("Facebook live parser", () => {
test(
"scrapes live marketplace search results into listing details",
async () => {
if (!process.env.FACEBOOK_COOKIE?.trim()) {
throw new Error("FACEBOOK_COOKIE is required for Facebook live tests");
}
const results = await fetchFacebookItems(
"iphone",
1,
"toronto",
LIVE_RESULT_LIMIT,
);
expect(results.length).toBeGreaterThan(0);
for (const listing of results) {
if (!listing.listingPrice) {
throw new Error(`Expected listing price for ${listing.url}`);
}
if (typeof listing.listingPrice.cents !== "number") {
throw new Error(`Expected listing cents for ${listing.url}`);
}
if (!listing.listingPrice.currency) {
throw new Error(`Expected listing currency for ${listing.url}`);
}
expect(listing.url).toStartWith(
"https://www.facebook.com/marketplace/item/",
);
expect(listing.title.length).toBeGreaterThan(0);
expect(listing.listingPrice.cents).toBeGreaterThanOrEqual(0);
expect(listing.listingPrice.currency.length).toBeGreaterThan(0);
}
},
LIVE_TEST_TIMEOUT_MS,
);
});

View File

@@ -0,0 +1,38 @@
import { describe, expect, test } from "bun:test";
import fetchKijijiItems from "../../src/scrapers/kijiji";
const LIVE_TEST_TIMEOUT_MS = 30_000;
describe("Kijiji live parser", () => {
test(
"scrapes live search results into detailed listings",
async () => {
const results = await fetchKijijiItems(
"iphone",
1,
"https://www.kijiji.ca",
{ maxPages: 1 },
{ includeImages: false, sellerDataDepth: "basic" },
);
expect(results.length).toBeGreaterThan(0);
for (const listing of results) {
if (!listing.listingPrice) {
throw new Error(`Expected listing price for ${listing.url}`);
}
if (typeof listing.listingPrice.cents !== "number") {
throw new Error(`Expected listing cents for ${listing.url}`);
}
if (!listing.listingPrice.currency) {
throw new Error(`Expected listing currency for ${listing.url}`);
}
expect(listing.url).toStartWith("https://www.kijiji.ca/");
expect(listing.title.length).toBeGreaterThan(0);
expect(listing.listingPrice.cents).toBeGreaterThanOrEqual(0);
expect(listing.listingPrice.currency.length).toBeGreaterThan(0);
}
},
LIVE_TEST_TIMEOUT_MS,
);
});