Skip to content

Commit 0e6223d

Browse files
committed
feat: add harness (AGENTS.md, tests/harness.test.ts, Makefile)
- AGENTS.md: stack, layout, commands, 10 engineering rules, done criteria - tests/harness.test.ts: isLoggedIn contract tests + package.json structural guards (encodes lessons from install simulation: bodyText heuristic, broad selectors, missing files field, missing repository.url, missing prepublishOnly) - Makefile: make agent-check = build + test (run before declaring any task done) Made-with: Cursor
1 parent c8eeb3e commit 0e6223d

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# @browserkit-dev/adapter-booking
2+
3+
Booking.com adapter for browserkit. Requires login.
4+
Provides upcoming/past bookings, property search, availability, and reviews.
5+
6+
## Stack
7+
8+
- Language: TypeScript 5.x → compiled to `dist/`
9+
- Runtime: Node.js 20+
10+
- Browser: Patchright (Playwright fork, anti-detection) via `@browserkit-dev/core`
11+
- Key deps: `@browserkit-dev/core` (peer), `patchright` (peer), `zod`
12+
13+
## Repo layout
14+
15+
```
16+
src/
17+
index.ts # adapter definition — tools, isLoggedIn, selectors, rateLimit
18+
scraper.ts # DOM extraction helpers
19+
selectors.ts# CSS/ARIA selectors (edit here when site DOM changes)
20+
tests/
21+
booking.test.ts # L1 unit — metadata, schemas, helpers (no browser)
22+
booking.integration.test.ts# L2 live scraping — real browser, real network
23+
mcp-protocol.test.ts # L3 MCP protocol — server lifecycle, tool dispatch
24+
harness.test.ts # Harness guards — isLoggedIn contract, pkg.json checks
25+
dist/ # compiled output (gitignored; included in npm publish via "files")
26+
```
27+
28+
## How to run
29+
30+
```bash
31+
npm install # install deps
32+
npm run build # tsc → dist/
33+
npm test # unit + harness tests (no browser)
34+
npm run test:integration # live browser tests (requires internet + login)
35+
make agent-check # full verification gate — run before declaring done
36+
```
37+
38+
## Engineering rules
39+
40+
1. **`isLoggedIn` must return `false` by default** when not authenticated — never use body-content
41+
length, cookie presence, or other heuristics; only return `true` when a specific auth element
42+
is confirmed present (a hard lesson: `bodyText.length > 100` matched the public homepage).
43+
2. **Auth selectors must be exclusive to logged-in state** — never `[aria-label*="account"]`
44+
(matches "Create an account" and other public nav). Use `data-testid` or `data-component`.
45+
3. **`package.json` must have `"files": ["dist", "README.md"]`** — without it, npm follows
46+
`.gitignore` and ships source-only packages that the daemon can't load.
47+
4. **`repository.url` required** — npm provenance rejects publishes without it.
48+
5. **`prepublishOnly: "tsc"`** — ensures `dist/` is always rebuilt before `npm publish`.
49+
6. **Selectors belong in `selectors.ts`** — never hardcode CSS strings in `index.ts` or `scraper.ts`.
50+
7. **No `any` types** — use `unknown` with type narrowing or Zod schemas.
51+
8. **`accountMenu` selector must use `data-testid`/`data-component` only**`[aria-label*="account"]` matched the public "Create an account" button, causing `isLoggedIn` to return `true` for unauthenticated sessions.
52+
9. **`isLoggedIn` must return `false` by default** — the old `bodyText.length > 100` fallback was wrong; Booking.com homepage body is always > 100 chars.
53+
10. **Use `www.booking.com` for session cookies**`secure.booking.com` requires a `?sid=` query param embedded in the homepage.
54+
55+
## Done criteria
56+
57+
A task is complete when ALL of the following pass:
58+
59+
- [ ] `npm run build` exits 0 (no TypeScript errors)
60+
- [ ] `npm test` passes (L1 unit + harness tests, no browser needed)
61+
- [ ] `make agent-check` passes (build + test in one command)
62+
- [ ] `npm pack --dry-run` shows `dist/` files in the tarball
63+
- [ ] `isLoggedIn` harness test passes with mock page (no regression)
64+
65+
66+
## Deeper docs
67+
68+
- Architecture: `../../ARCH.md` (monorepo) or `README.md` in this repo
69+
- Selector maintenance: update `src/selectors.ts` when the site changes DOM
70+
- Publishing: push to `main` → CI builds and publishes via OIDC (no token needed)

Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.PHONY: agent-check build test lint
2+
3+
# Run before declaring any task done.
4+
# Builds, then runs all unit + harness tests (no browser, no network).
5+
agent-check: build test
6+
@echo "✓ agent-check passed"
7+
8+
build:
9+
npx tsc
10+
11+
test:
12+
npx vitest run
13+
14+
lint:
15+
npx tsc --noEmit

tests/harness.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Harness Tests — booking
3+
*
4+
* Guards against regressions found during install simulation.
5+
* No browser, no network — runs in milliseconds.
6+
*
7+
* Lessons encoded here:
8+
* - isLoggedIn must return FALSE on an unauthenticated page (never use body-content heuristics)
9+
* - accountMenu/auth selectors must NOT match public page elements
10+
* - package.json must include "files" → dist or new users get source-only packages
11+
* - repository.url required for npm provenance publishing
12+
* - prepublishOnly ensures build runs before every publish
13+
*/
14+
import { describe, it, expect } from "vitest";
15+
import adapter from "../src/index.js";
16+
import { readFileSync } from "node:fs";
17+
import { fileURLToPath } from "node:url";
18+
import path from "node:path";
19+
20+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
21+
const pkg = JSON.parse(readFileSync(path.join(__dirname, "../package.json"), "utf8"));
22+
23+
// ── package.json structural guards ────────────────────────────────────────────
24+
25+
describe("package.json harness guards", () => {
26+
it('has "files" field containing "dist"', () => {
27+
expect(pkg.files, 'Missing "files" in package.json — dist/ will be excluded from npm publish').toBeDefined();
28+
expect(pkg.files).toContain("dist");
29+
});
30+
31+
it("has repository.url — required for npm provenance publishing", () => {
32+
expect(pkg.repository?.url, 'Missing repository.url — npm provenance will reject the publish').toBeTruthy();
33+
expect(pkg.repository.url).toContain("github.com");
34+
});
35+
36+
it("has prepublishOnly script — ensures build runs before every publish", () => {
37+
expect(pkg.scripts?.prepublishOnly, 'Missing prepublishOnly — packages may publish without compiling').toBeTruthy();
38+
});
39+
});
40+
41+
// ── Minimal Page mock (no browser, no network) ────────────────────────────────
42+
43+
/**
44+
* isLoggedIn contract:
45+
* - MUST return false on an unauthenticated page
46+
* - MUST return false on the login page
47+
* - MUST return true when the account/nav element is present
48+
*
49+
* This was violated in the Booking adapter (bodyText.length > 100 heuristic)
50+
* causing health_check to report loggedIn=true for unauthenticated sessions.
51+
*/
52+
function makeMockPage(url: string, hasAuthElement = false) {
53+
return {
54+
url: () => url,
55+
locator: (sel: string) => {
56+
const found = hasAuthElement && sel.includes("account-menu");
57+
return {
58+
count: async () => (found ? 1 : 0),
59+
isVisible: async () => (found ? true : false),
60+
first: () => ({
61+
isVisible: async (_opts?: unknown) => (found ? true : false),
62+
click: async () => {},
63+
}),
64+
};
65+
},
66+
evaluate: async (_fn: unknown) => "",
67+
waitForTimeout: async () => {},
68+
goto: async (_url: string) => null,
69+
waitForSelector: async () => null,
70+
};
71+
}
72+
73+
describe("isLoggedIn contract — unauthenticated", () => {
74+
it("returns false on adapter domain with no auth elements present", async () => {
75+
const page = makeMockPage(`https://${adapter.domain}/`, false);
76+
expect(await adapter.isLoggedIn(page as never)).toBe(false);
77+
});
78+
79+
it("returns false on the login URL", async () => {
80+
const page = makeMockPage(adapter.loginUrl, false);
81+
expect(await adapter.isLoggedIn(page as never)).toBe(false);
82+
});
83+
});
84+
85+
describe("isLoggedIn contract — authenticated", () => {
86+
it("returns true when the auth nav element is present", async () => {
87+
const page = makeMockPage(`https://${adapter.domain}/`, true);
88+
expect(await adapter.isLoggedIn(page as never)).toBe(true);
89+
});
90+
});
91+
92+
// ── Tool registry ─────────────────────────────────────────────────────────────
93+
94+
describe("tool registry", () => {
95+
it("tools() returns a non-empty array", () => {
96+
expect(adapter.tools().length).toBeGreaterThan(0);
97+
});
98+
99+
it("every tool has a non-empty name and description", () => {
100+
for (const tool of adapter.tools()) {
101+
expect(tool.name.length, `tool missing name`).toBeGreaterThan(0);
102+
expect(tool.description?.length ?? 0, `"${tool.name}" missing description`).toBeGreaterThan(10);
103+
}
104+
});
105+
106+
it("every tool has a handler", () => {
107+
for (const tool of adapter.tools()) {
108+
expect(typeof tool.handler, `"${tool.name}" missing handler`).toBe("function");
109+
}
110+
});
111+
});

0 commit comments

Comments
 (0)