From a2041ca9fe2e2fac0aea0fcd2469d5a18ff4c25c Mon Sep 17 00:00:00 2001 From: "zenrows-dev-pool[bot]" Date: Wed, 21 Oct 2026 18:42:41 +0000 Subject: [PATCH 1/2] feat(extract): steer prepare callers to send several example URLs of one page type Preparing a domain maps a page type more completely when it gets several example pages of that type: Extract compares them to check the fields it found and fill in fields one page is missing. The help, skills and docs said "one per page kind", which steered users to send a single page of each type. The prepare help, the skills, the README and the capability notes now say one URL works and at least 3 of the same page type work better, up to 5, with an example of three product pages. With fewer than 3 --url the command prints a tip on stderr. 1 to 5 URLs are still accepted; the JSON output and the exit code do not change. --- README.md | 14 ++++++- agent-plugin/README.md | 2 +- agent-plugin/skills/extract/SKILL.md | 15 +++++-- agent-plugin/skills/zenrows/SKILL.md | 2 +- docs/capabilities.md | 2 +- registry/capabilities.json | 2 +- skills/extract/SKILL.md | 17 +++++--- skills/zenrows/SKILL.md | 3 +- src/cli/commands/extract-prepare.ts | 23 +++++++++-- src/core/http.ts | 2 +- src/core/prepared-domains-api.ts | 22 +++++++++- tests/capabilities.test.ts | 1 + tests/extract-prepare.test.ts | 62 +++++++++++++++++++++++++++- 13 files changed, 144 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e094496..0589f6f 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,20 @@ few minutes if it should work, or use `--css`; preparing again does not change it. `zenrows extract` never falls back to Autoparse. The `fallbackAutoparse` adapter option is deprecated and has no effect. +One example URL works; up to 5 related URLs work better: several pages of each +page type you need parsed (several types can share the 5, e.g. 3 product pages + +2 search-result pages). Extract compares the pages of one type against each other, +so it can check the fields it found and fill in fields one page is missing; more +pages of a type give more complete, more reliable results for that type. Pass at +least 3 of the same page type when you can. + ```bash -zenrows extract prepare example.com --url https://example.com/p/1 --url https://example.com/list --wait +zenrows extract prepare example.com \ + --url https://www.example.com/products/a \ + --url https://www.example.com/products/b \ + --url https://www.example.com/products/c --wait zenrows extract prepare status example.com # progress + monthly allowance -zenrows extract https://example.com/p/1 # then extract again +zenrows extract https://www.example.com/products/a # then extract again ``` ## 9. Batch diff --git a/agent-plugin/README.md b/agent-plugin/README.md index fcef399..bd8ad25 100644 --- a/agent-plugin/README.md +++ b/agent-plugin/README.md @@ -62,7 +62,7 @@ Use MCP tool names — not CLI `zenrows fetch`: | --- | --- | | `zenrows` | Decision tree across all primitives | | `protected-fetch` | `scrape` | -| `extract` | `extract` (fallback: `scrape` + css_extractor); on REQS007 with reason `domain_not_prepared`, `prepare_domain` then `prepared_domain_status` | +| `extract` | `extract` (fallback: `scrape` + css_extractor); on REQS007 with reason `domain_not_prepared`, `prepare_domain` (at least 3 example URLs of the same page type when you can, up to 5) then `prepared_domain_status` | | `batch-jobs` | `batch_create`, `batch_status`, `batch_results`, `batch_cancel` | | `interact-browser` | `browser_*` (escalation only) | | `cost-control` | Cost multipliers + escalate-with-evidence | diff --git a/agent-plugin/skills/extract/SKILL.md b/agent-plugin/skills/extract/SKILL.md index 084b508..a3b2e17 100644 --- a/agent-plugin/skills/extract/SKILL.md +++ b/agent-plugin/skills/extract/SKILL.md @@ -48,15 +48,22 @@ Preparing a domain: ``` extract({ url }) # 1. REQS007, reason domain_not_prepared -prepare_domain({ domain: "example.com", - example_urls: ["https://example.com/p/1", "https://example.com/list"] }) +prepare_domain({ domain: "example.com", example_urls: [ + "https://www.example.com/products/a", + "https://www.example.com/products/b", + "https://www.example.com/products/c"] }) # 2. returns at once, status "preparing" prepared_domain_status({ domain: "example.com" }) # 3. about every 30 seconds until "ready" extract({ url }) # 4. extract again ``` -- `example_urls`: 1 to 5 real pages you want to extract, one per page kind - (for example a product page and a listing page). +- `example_urls`: real pages you want to extract, 1 to 5. One works; pass at + least 3 example URLs of the same page type when you can, up to 5: several + pages of each page type you need parsed (several types can share the 5, e.g. + 3 product pages + 2 search-result pages). Extract compares the pages of one + type against each other, so it can check the fields it found and fill in + fields one page is missing; more pages of a type give more complete, more + reliable results. - The domain is the host: `shop.example.com` and `example.com` are prepared separately, and a leading `www.` is dropped. - Preparing takes a few minutes and costs no credits. Each new domain uses one diff --git a/agent-plugin/skills/zenrows/SKILL.md b/agent-plugin/skills/zenrows/SKILL.md index e0da8f9..7127149 100644 --- a/agent-plugin/skills/zenrows/SKILL.md +++ b/agent-plugin/skills/zenrows/SKILL.md @@ -68,7 +68,7 @@ If the user needs login, clicks, forms, sessions, or persistent state: | --- | --- | --- | | Protected Fetch | `scrape` | Default markdown; HTML / plaintext / PDF / screenshot options | | Extract | `extract` | Structured JSON (`extract=auto` on any prepared domain, css_extractor; autoparse deprecated). `extract=auto` is currently free; billing may apply later | -| Prepare a domain | `prepare_domain`, `prepared_domain_status` | After an `extract` REQS007 with reason `domain_not_prepared`: prepare the domain with 1 to 5 example URLs, poll about every 30 seconds until ready, extract again. No credits; uses one monthly allowance slot. Not for `page_not_recognised`: that domain is already prepared | +| Prepare a domain | `prepare_domain`, `prepared_domain_status` | After an `extract` REQS007 with reason `domain_not_prepared`: prepare the domain with 1 to 5 example URLs (pass at least 3 example URLs of the same page type when you can, up to 5: Extract compares them to check and fill in fields), poll about every 30 seconds until ready, extract again. No credits; uses one monthly allowance slot. Not for `page_not_recognised`: that domain is already prepared | | Batch | `batch_create`, `batch_status`, `batch_results`, `batch_cancel` | Same credits as Fetch; a 403 maps to `BATCH_ACCESS_DENIED` | | Browser | `browser_*` (start with `browser_navigate`) | Escalation-only; same backend as Browser Sessions | diff --git a/docs/capabilities.md b/docs/capabilities.md index 84bf420..384724d 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -18,7 +18,7 @@ Classification is based on the public Zenrows documentation: | Capability | Backend evidence | Status | | --- | --- | --- | | `protected_fetch` | Fetch `GET https://api.zenrows.com/v1/` with `mode`, `js_render`, `premium_proxy`, `proxy_country`, `wait`/`wait_for`, `js_instructions`, `response_type`, `screenshot`, `original_status`, … | available | -| `extract` | Extract `GET https://api.zenrows.com/v1/` via `extract=auto` (any prepared domain; on one not prepared yet the CLI reports REQS007 as `EXTRACT_DOMAIN_NOT_PREPARED` with the command to prepare it: `zenrows extract prepare --url `, which calls `POST /v1/extract/prepared-domains`; a page Extract did not recognise on a prepared domain is `EXTRACT_PAGE_NOT_RECOGNISED`, which preparing does not change), plus deprecated `autoparse`, `css_extractor`, `outputs`, `response_type=markdown\|plaintext` | available | +| `extract` | Extract `GET https://api.zenrows.com/v1/` via `extract=auto` (any prepared domain; on one not prepared yet the CLI reports REQS007 as `EXTRACT_DOMAIN_NOT_PREPARED` with the command to prepare it: `zenrows extract prepare --url `, which calls `POST /v1/extract/prepared-domains` with 1 to 5 example URLs: one works, at least 3 of the same page type work better; a page Extract did not recognise on a prepared domain is `EXTRACT_PAGE_NOT_RECOGNISED`, which preparing does not change), plus deprecated `autoparse`, `css_extractor`, `outputs`, `response_type=markdown\|plaintext` | available | | `batch` | Batch `https://async.api.zenrows.com/v1` (separate host, `X-API-Key` header). Local JSONL spec validation + credit estimation work with no key. | available | | `crawl` | Crawl `https://api.zenrows.com/v1/crawls` (`X-API-Key` header; same host as Fetch/Extract). Follows the links on each page from one start URL, up to `depth` when set (optional; no depth limit when omitted), on the start URL's registrable domain (subdomains count); returns URLs, and with `output_format: "html"` each page's HTML. Subcommands: `create`, `get`, `list`, `results`, `content`, `download`, `stop`, `wait`. Without Crawl access the API returns 403 `REQS008` → `CRAWL_NOT_ENABLED`. | available | | `browser` | Browser Sessions REST API `https://mcp.zenrows.com/browser/sessions/*` (Bearer; same backend as `@zenrows/mcp` `browser_*`); CDP via `zenrows browser connect`. Escalation-only (prefer fetch/extract); on by default, opt out via `policy.allow_browser`; bills by bandwidth + session time (15-min max) | available | diff --git a/registry/capabilities.json b/registry/capabilities.json index 13b2f71..a60ddb5 100644 --- a/registry/capabilities.json +++ b/registry/capabilities.json @@ -17,7 +17,7 @@ "command": "zenrows extract", "backend": "Extract — GET https://api.zenrows.com/v1/ (extract=auto, css_extractor, outputs, response_type; autoparse deprecated)", "requiresAuth": true, - "notes": "Default is extract=auto, which works on any prepared domain with any key. A REQS007 (legacy AUTH010) is reported, never retried, with the next step for its reason: on a domain not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED), prepare it with `zenrows extract prepare --url [--wait]` (POST /v1/extract/prepared-domains: 1 to 5 example URLs, a few minutes, no credits) and check it with `zenrows extract prepare status `; on a page Extract did not recognise on a prepared domain (EXTRACT_PAGE_NOT_RECOGNISED), try a product, article or listing page, retry it once in a few minutes, or use --css, because preparing again does not change it. CSS / outputs / markdown work on any domain; autoparse is deprecated and runs only with --autoparse." + "notes": "Default is extract=auto, which works on any prepared domain with any key. A REQS007 (legacy AUTH010) is reported, never retried, with the next step for its reason: on a domain not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED), prepare it with `zenrows extract prepare --url [--wait]` (POST /v1/extract/prepared-domains: 1 to 5 example URLs; one works, at least 3 of the same page type work better because Extract compares them to check and fill in fields; a few minutes, no credits) and check it with `zenrows extract prepare status `; on a page Extract did not recognise on a prepared domain (EXTRACT_PAGE_NOT_RECOGNISED), try a product, article or listing page, retry it once in a few minutes, or use --css, because preparing again does not change it. CSS / outputs / markdown work on any domain; autoparse is deprecated and runs only with --autoparse." }, "batch": { "key": "batch", diff --git a/skills/extract/SKILL.md b/skills/extract/SKILL.md index 00d4b96..481150b 100644 --- a/skills/extract/SKILL.md +++ b/skills/extract/SKILL.md @@ -43,15 +43,22 @@ of three cases it is and prints the next step. It never retries with Autoparse. Preparing a domain: ``` -zenrows extract https://example.com/p/1 # 1. fails: EXTRACT_DOMAIN_NOT_PREPARED +zenrows extract https://www.example.com/products/a # 1. fails: EXTRACT_DOMAIN_NOT_PREPARED zenrows extract prepare example.com \ - --url https://example.com/p/1 --url https://example.com/list --wait + --url https://www.example.com/products/a \ + --url https://www.example.com/products/b \ + --url https://www.example.com/products/c --wait # 2. prepare it and wait until ready (a few minutes) -zenrows extract https://example.com/p/1 # 3. extract again +zenrows extract https://www.example.com/products/a # 3. extract again ``` -- `--url`: 1 to 5 real pages you want to extract, one per page kind (for - example a product page and a listing page). +- `--url`: real pages you want to extract, 1 to 5. One works; pass at least 3 + example URLs of the same page type when you can, up to 5: several pages of + each page type you need parsed (several types can share the 5, e.g. 3 product + pages + 2 search-result pages). Extract compares the pages of one type against + each other, so it can check the fields it found and fill in fields one page is + missing; more pages of a type give more complete, more reliable results. With + fewer than 3 the command prints a tip on stderr; it is never an error. - Without `--wait` the command returns at once; then check about every 30 seconds with `zenrows extract prepare status example.com` until it says `ready`. `--wait` polls at the same pace. diff --git a/skills/zenrows/SKILL.md b/skills/zenrows/SKILL.md index d8fe34a..f94814c 100644 --- a/skills/zenrows/SKILL.md +++ b/skills/zenrows/SKILL.md @@ -83,7 +83,8 @@ Run `zenrows status` for the live capability matrix. As of this toolkit: - Extract: `extract=auto` works on any prepared domain. On one not prepared yet (`EXTRACT_DOMAIN_NOT_PREPARED`), run `zenrows extract prepare --url - --wait`, then extract again. `EXTRACT_PAGE_NOT_RECOGNISED` means the + --wait` (repeat `--url`: at least 3 pages of the same page type when + you can, up to 5), then extract again. `EXTRACT_PAGE_NOT_RECOGNISED` means the domain is prepared but not this page: do not prepare again; see [[extract]]. - Batch: the reliable path at scale, at Fetch's prices (create, list, status, wait, results, content, pause, resume, rerun, delete). Estimate specs locally with no key. - Crawl: collects the URLs behind one start page, optionally with each page's HTML. diff --git a/src/cli/commands/extract-prepare.ts b/src/cli/commands/extract-prepare.ts index fb3d3b6..1d85cd8 100644 --- a/src/cli/commands/extract-prepare.ts +++ b/src/cli/commands/extract-prepare.ts @@ -5,7 +5,8 @@ * The way out of REQS007: prepare the domain so `extract=auto` works on it. * Submitting returns at once; `--wait` polls until the preparation is ready or * failed (a few minutes). Costs no credits; each new domain uses one slot of the - * account's monthly allowance. + * account's monthly allowance. One --url works; fewer than 3 prints a hint to stderr + * (never an error) to add more pages of the same page type, up to 5. */ import { ensureApiKey } from "../../core/ensure-key.ts"; import { loadConfig } from "../../core/config.ts"; @@ -15,6 +16,8 @@ import { ToolkitError } from "../../core/errors.ts"; import { DEFAULT_WAIT_TIMEOUT_MS, MAX_EXAMPLE_URLS, + RECOMMENDED_EXAMPLE_URLS, + exampleUrlsHint, failureNextStep, getAllowance, getPreparedDomain, @@ -39,7 +42,8 @@ export const PREPARE_HELP = [ "Not for EXTRACT_PAGE_NOT_RECOGNISED: that domain is already prepared, and preparing it", "again does not change that answer.", "", - ` --url 1 to ${MAX_EXAMPLE_URLS} real pages you want to extract, one per page kind (repeat --url)`, + ` --url a real page you want to extract (repeat --url, 1 to ${MAX_EXAMPLE_URLS}). One works;`, + ` at least ${RECOMMENDED_EXAMPLE_URLS} pages of the same page type work better`, " --wait poll every 30 seconds until the domain is ready or failed; off by default", ` --timeout give up waiting after this long (default ${DEFAULT_WAIT_TIMEOUT_MS / 1000})`, " status show progress and the account's monthly allowance", @@ -49,6 +53,16 @@ export const PREPARE_HELP = [ "account's monthly allowance; a domain that is already prepared answers ready at once and", "uses none. The domain is the host (shop.example.com and example.com are separate); a", "leading www. is dropped.", + "", + `One example URL works; up to ${MAX_EXAMPLE_URLS} related URLs work better: several pages of each`, + `page type you need parsed. Several types can share the ${MAX_EXAMPLE_URLS}, e.g. 3 product pages +`, + "2 search-result pages. Extract compares the pages of one type against each other, so it", + "can check the fields it found and fill in fields one page is missing: more pages of a type", + `give more complete, more reliable results for that type. Recommended: at least ${RECOMMENDED_EXAMPLE_URLS}`, + "pages of one type, e.g. three product pages:", + "", + " zenrows extract prepare example.com --url https://www.example.com/products/a \\", + " --url https://www.example.com/products/b --url https://www.example.com/products/c --wait", ].join("\n"); /** Injectable for tests: network, sleep and clock. */ @@ -99,6 +113,9 @@ export async function runPrepare(argv: string[], ctx: RunContext, deps: PrepareD } log.step(`Prepare ${domain} for Extract (${urls.length} example URL${urls.length === 1 ? "" : "s"})…`); + // Advice on stderr only: the --json payload on stdout and the exit code are unchanged. + const hint = exampleUrlsHint(urls.length); + if (hint) log.dim(hint); let record: PreparedDomain = await prepareDomain(domain, urls, opts); if (record.already_in_progress) log.info(`${domain} was already being prepared; following that preparation.`); // A submit that answers ready at once found the domain already prepared. @@ -137,7 +154,7 @@ function validateUrls(raw: unknown, domain: string): string[] { code: "INVALID_USAGE", message: `Pass 1 to ${MAX_EXAMPLE_URLS} example page URLs with --url (got ${urls.length}).`, likely_cause: "Preparation learns the domain from real pages you want to extract.", - next_action: "Add one --url per page kind, e.g. a product page and a listing page.", + next_action: `Add 1 to ${MAX_EXAMPLE_URLS} --url pages; at least ${RECOMMENDED_EXAMPLE_URLS} pages of the same page type work best.`, suggested_commands: [`zenrows extract prepare ${domain} --url https://${domain}/`], }); } diff --git a/src/core/http.ts b/src/core/http.ts index 9dd4dca..39c1af6 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -369,7 +369,7 @@ export function extractDomainNotPrepared(url: string, body: string): ToolkitErro code: "EXTRACT_DOMAIN_NOT_PREPARED", message: `Extract hasn't prepared ${domain} yet. Prepare it with \`${prepare}\` (a few example URLs, a few minutes, no credits), then retry; meanwhile use --css.`, likely_cause: detail ?? `Extract has not prepared ${domain} yet.`, - next_action: `Run \`${prepare} --wait\` (up to 5 --url pages, one per page kind you want), then retry \`zenrows extract ${url}\`. Without --wait, check progress about every 30 seconds with \`zenrows extract prepare status ${domain}\`. Meanwhile extract with --css and explicit selectors.`, + next_action: `Run \`${prepare} --wait\` (add --url pages: one works, at least 3 of the same page type work better, up to 5), then retry \`zenrows extract ${url}\`. Without --wait, check progress about every 30 seconds with \`zenrows extract prepare status ${domain}\`. Meanwhile extract with --css and explicit selectors.`, suggested_commands: [`${prepare} --wait`, `zenrows extract prepare status ${domain}`, css], server_code: "REQS007", }); diff --git a/src/core/prepared-domains-api.ts b/src/core/prepared-domains-api.ts index 69bb5e0..89504a8 100644 --- a/src/core/prepared-domains-api.ts +++ b/src/core/prepared-domains-api.ts @@ -4,7 +4,8 @@ * `extract=auto` answers REQS007 on a domain Extract has not prepared yet. Preparing * it is a call any key can make: `POST {apiBase}/extract/prepared-domains` with the * domain and 1 to 5 example URLs answers 202 at once; the preparation itself takes a - * few minutes and costs no credits. `GET .../{domain}` reports progress and + * few minutes and costs no credits. One example URL works; at least 3 of the same + * page type work better (see `RECOMMENDED_EXAMPLE_URLS`). `GET .../{domain}` reports progress and * `GET .../allowance` the account's monthly allowance of new domains. Auth is the * `X-API-Key` header; errors are problem+json and we branch on `status` + `code`. * `fetchImpl` / `sleepImpl` / `now` are injectable for tests. @@ -15,6 +16,25 @@ import { CLI_USER_AGENT, loadConfig } from "./config.ts"; import { agentClientHeader } from "./agent-client.ts"; export const MAX_EXAMPLE_URLS = 5; +/** + * How many example URLs to recommend: 3 pages of one page type. Extract compares + * the pages of a type against each other, so it can check the fields it found and + * fill in fields one page is missing. Advice only: 1 and 2 URLs are still valid. + */ +export const RECOMMENDED_EXAMPLE_URLS = 3; + +/** + * A short, non-blocking hint when fewer than `RECOMMENDED_EXAMPLE_URLS` example + * URLs were given; undefined otherwise. Never an error. + */ +export function exampleUrlsHint(count: number): string | undefined { + if (count >= RECOMMENDED_EXAMPLE_URLS) return undefined; + return ( + `Tip: ${count === 1 ? "one example URL works" : `${count} example URLs work`}, but more related URLs work better. ` + + `Add pages of the same page type (at least ${RECOMMENDED_EXAMPLE_URLS}, up to ${MAX_EXAMPLE_URLS} in total): ` + + "Extract compares them to check the fields it finds and fill in fields one page is missing." + ); +} /** How often `--wait` polls, and for how long by default. */ export const DEFAULT_POLL_INTERVAL_MS = 30_000; export const DEFAULT_WAIT_TIMEOUT_MS = 15 * 60_000; diff --git a/tests/capabilities.test.ts b/tests/capabilities.test.ts index 846fcf5..49dde23 100644 --- a/tests/capabilities.test.ts +++ b/tests/capabilities.test.ts @@ -18,6 +18,7 @@ test("extract notes name the prepare step and no automatic autoparse fallback", assert.match(notes, /POST \/v1\/extract\/prepared-domains/); assert.match(notes, /zenrows extract prepare --url /); assert.match(notes, /zenrows extract prepare status /); + assert.match(notes, /1 to 5 example URLs; one works, at least 3 of the same page type work better/); assert.match(notes, /EXTRACT_PAGE_NOT_RECOGNISED/); assert.match(notes, /preparing again does not change it/); assert.match(notes, /autoparse is deprecated/); diff --git a/tests/extract-prepare.test.ts b/tests/extract-prepare.test.ts index fe0e657..592d8e2 100644 --- a/tests/extract-prepare.test.ts +++ b/tests/extract-prepare.test.ts @@ -1,8 +1,8 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { runPrepare } from "../src/cli/commands/extract-prepare.ts"; +import { PREPARE_HELP, runPrepare } from "../src/cli/commands/extract-prepare.ts"; import { extract } from "../src/cli/commands/extract.ts"; -import { failureNextStep, normalizeDomain, prepareCommand } from "../src/core/prepared-domains-api.ts"; +import { exampleUrlsHint, failureNextStep, normalizeDomain, prepareCommand } from "../src/core/prepared-domains-api.ts"; const ctx = { json: true, yes: false }; const BASE = "/v1/extract/prepared-domains"; @@ -273,3 +273,61 @@ test("zenrows extract routes `prepare` to the prepare command", async () => { assert.equal(code, 1); assert.equal(JSON.parse(raw).error.code, "INVALID_USAGE", "no --url: rejected locally, no extract of a URL named prepare"); }); + +async function captureErr(fn: () => unknown): Promise { + const orig = process.stderr.write.bind(process.stderr); + let buf = ""; + process.stderr.write = ((s: string | Uint8Array, ...rest: unknown[]) => { + if (typeof s !== "string") return (orig as (...a: unknown[]) => boolean)(s, ...rest); + buf += s; + return true; + }) as typeof process.stderr.write; + try { + await fn(); + } finally { + process.stderr.write = orig; + } + return buf; +} + +test("prepare with fewer than 3 URLs prints a tip on stderr, keeps the JSON and the exit code", async () => { + for (const urls of [["https://example.com/products/a"], ["https://example.com/products/a", "https://example.com/products/b"]]) { + const { calls, fetchImpl } = stub({ [`POST ${BASE}`]: () => res(RECORD, 202) }); + let result: Awaited> | undefined; + const err = await captureErr(async () => { + result = await run(["example.com", ...urls.flatMap((u) => ["--url", u])], fetchImpl); + }); + assert.equal(result!.code, 0); + assert.equal(result!.out.ok, true); + assert.equal(result!.out.hint, undefined, "the tip never enters the stdout JSON"); + assert.deepEqual((calls[0]!.body as { example_urls: string[] }).example_urls, urls, "1 or 2 URLs are still sent as given"); + assert.match(err, /Tip: .*more related URLs work better/); + assert.match(err, /same page type \(at least 3, up to 5 in total\)/); + } +}); + +test("prepare with 3 or more URLs prints no tip", async () => { + const { fetchImpl } = stub({ [`POST ${BASE}`]: () => res(RECORD, 202) }); + const urls = ["a", "b", "c"].map((p) => `https://example.com/products/${p}`); + let code = -1; + const err = await captureErr(async () => { + ({ code } = await run(["example.com", ...urls.flatMap((u) => ["--url", u])], fetchImpl)); + }); + assert.equal(code, 0); + assert.doesNotMatch(err, /Tip:/); +}); + +test("exampleUrlsHint recommends 3 and is silent from 3 on", () => { + assert.match(exampleUrlsHint(1) ?? "", /one example URL works/); + assert.match(exampleUrlsHint(2) ?? "", /2 example URLs work/); + assert.equal(exampleUrlsHint(3), undefined); + assert.equal(exampleUrlsHint(5), undefined); +}); + +test("prepare help recommends several URLs of one page type, not one per page kind", () => { + assert.doesNotMatch(PREPARE_HELP, /one per page kind/); + assert.match(PREPARE_HELP, /One works;/); + assert.match(PREPARE_HELP, /at least 3 pages of the same page type work better/); + assert.match(PREPARE_HELP, /3 product pages \+\s+2 search-result pages/); + assert.match(PREPARE_HELP, /products\/a[\s\S]*products\/b[\s\S]*products\/c/); +}); From f1a17e5b92117542957e9a65bc2db67b5128cf82 Mon Sep 17 00:00:00 2001 From: "zenrows-dev-pool[bot]" Date: Mon, 26 Oct 2026 07:23:43 +0000 Subject: [PATCH 2/2] fix(extract): recommend 3 or more example URLs of the same page type in one consistent wording The prepare guidance used several phrasings ("at least 3 of each page type", "work best") that read as a requirement or disagreed with each other. One works; 3 or more of the same page type is a recommendation, now stated the same way in help, errors, docs and skills, including the discovery_failed next step. --- README.md | 10 ++++------ agent-plugin/README.md | 2 +- agent-plugin/skills/extract/SKILL.md | 12 +++++------- agent-plugin/skills/zenrows/SKILL.md | 2 +- docs/capabilities.md | 2 +- registry/capabilities.json | 2 +- skills/extract/SKILL.md | 13 ++++++------- skills/zenrows/SKILL.md | 4 ++-- src/cli/commands/extract-prepare.ts | 18 ++++++++---------- src/core/http.ts | 2 +- src/core/prepared-domains-api.ts | 18 +++++++++--------- tests/capabilities.test.ts | 2 +- tests/extract-prepare.test.ts | 11 +++++++---- 13 files changed, 47 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 0589f6f..2f1263f 100644 --- a/README.md +++ b/README.md @@ -134,12 +134,10 @@ few minutes if it should work, or use `--css`; preparing again does not change it. `zenrows extract` never falls back to Autoparse. The `fallbackAutoparse` adapter option is deprecated and has no effect. -One example URL works; up to 5 related URLs work better: several pages of each -page type you need parsed (several types can share the 5, e.g. 3 product pages + -2 search-result pages). Extract compares the pages of one type against each other, -so it can check the fields it found and fill in fields one page is missing; more -pages of a type give more complete, more reliable results for that type. Pass at -least 3 of the same page type when you can. +Pass 1 to 5 example URLs. One works; we recommend 3 or more of the same page +type (e.g. 3 product pages), because Extract compares pages of one type to check +the fields it found and fill in fields one page is missing. Several page types +can share the 5 (e.g. 3 product pages + 2 search-result pages). ```bash zenrows extract prepare example.com \ diff --git a/agent-plugin/README.md b/agent-plugin/README.md index bd8ad25..6d202d3 100644 --- a/agent-plugin/README.md +++ b/agent-plugin/README.md @@ -62,7 +62,7 @@ Use MCP tool names — not CLI `zenrows fetch`: | --- | --- | | `zenrows` | Decision tree across all primitives | | `protected-fetch` | `scrape` | -| `extract` | `extract` (fallback: `scrape` + css_extractor); on REQS007 with reason `domain_not_prepared`, `prepare_domain` (at least 3 example URLs of the same page type when you can, up to 5) then `prepared_domain_status` | +| `extract` | `extract` (fallback: `scrape` + css_extractor); on REQS007 with reason `domain_not_prepared`, `prepare_domain` (1 to 5 example URLs; we recommend 3 or more of the same page type) then `prepared_domain_status` | | `batch-jobs` | `batch_create`, `batch_status`, `batch_results`, `batch_cancel` | | `interact-browser` | `browser_*` (escalation only) | | `cost-control` | Cost multipliers + escalate-with-evidence | diff --git a/agent-plugin/skills/extract/SKILL.md b/agent-plugin/skills/extract/SKILL.md index a3b2e17..415ae1b 100644 --- a/agent-plugin/skills/extract/SKILL.md +++ b/agent-plugin/skills/extract/SKILL.md @@ -57,13 +57,11 @@ prepared_domain_status({ domain: "example.com" }) # 3. about every 30 se extract({ url }) # 4. extract again ``` -- `example_urls`: real pages you want to extract, 1 to 5. One works; pass at - least 3 example URLs of the same page type when you can, up to 5: several - pages of each page type you need parsed (several types can share the 5, e.g. - 3 product pages + 2 search-result pages). Extract compares the pages of one - type against each other, so it can check the fields it found and fill in - fields one page is missing; more pages of a type give more complete, more - reliable results. +- `example_urls`: 1 to 5 real pages you want to extract. One works; we + recommend 3 or more of the same page type (e.g. 3 product pages), because + Extract compares pages of one type to check the fields it found and fill in + fields one page is missing. Several page types can share the 5 (e.g. 3 + product pages + 2 search-result pages). - The domain is the host: `shop.example.com` and `example.com` are prepared separately, and a leading `www.` is dropped. - Preparing takes a few minutes and costs no credits. Each new domain uses one diff --git a/agent-plugin/skills/zenrows/SKILL.md b/agent-plugin/skills/zenrows/SKILL.md index 7127149..f1a9a24 100644 --- a/agent-plugin/skills/zenrows/SKILL.md +++ b/agent-plugin/skills/zenrows/SKILL.md @@ -68,7 +68,7 @@ If the user needs login, clicks, forms, sessions, or persistent state: | --- | --- | --- | | Protected Fetch | `scrape` | Default markdown; HTML / plaintext / PDF / screenshot options | | Extract | `extract` | Structured JSON (`extract=auto` on any prepared domain, css_extractor; autoparse deprecated). `extract=auto` is currently free; billing may apply later | -| Prepare a domain | `prepare_domain`, `prepared_domain_status` | After an `extract` REQS007 with reason `domain_not_prepared`: prepare the domain with 1 to 5 example URLs (pass at least 3 example URLs of the same page type when you can, up to 5: Extract compares them to check and fill in fields), poll about every 30 seconds until ready, extract again. No credits; uses one monthly allowance slot. Not for `page_not_recognised`: that domain is already prepared | +| Prepare a domain | `prepare_domain`, `prepared_domain_status` | After an `extract` REQS007 with reason `domain_not_prepared`: prepare the domain with 1 to 5 example URLs (one works; we recommend 3 or more of the same page type, because Extract compares pages of one type to check the fields it found and fill in fields one page is missing), poll about every 30 seconds until ready, extract again. No credits; uses one monthly allowance slot. Not for `page_not_recognised`: that domain is already prepared | | Batch | `batch_create`, `batch_status`, `batch_results`, `batch_cancel` | Same credits as Fetch; a 403 maps to `BATCH_ACCESS_DENIED` | | Browser | `browser_*` (start with `browser_navigate`) | Escalation-only; same backend as Browser Sessions | diff --git a/docs/capabilities.md b/docs/capabilities.md index 384724d..8a9e0c1 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -18,7 +18,7 @@ Classification is based on the public Zenrows documentation: | Capability | Backend evidence | Status | | --- | --- | --- | | `protected_fetch` | Fetch `GET https://api.zenrows.com/v1/` with `mode`, `js_render`, `premium_proxy`, `proxy_country`, `wait`/`wait_for`, `js_instructions`, `response_type`, `screenshot`, `original_status`, … | available | -| `extract` | Extract `GET https://api.zenrows.com/v1/` via `extract=auto` (any prepared domain; on one not prepared yet the CLI reports REQS007 as `EXTRACT_DOMAIN_NOT_PREPARED` with the command to prepare it: `zenrows extract prepare --url `, which calls `POST /v1/extract/prepared-domains` with 1 to 5 example URLs: one works, at least 3 of the same page type work better; a page Extract did not recognise on a prepared domain is `EXTRACT_PAGE_NOT_RECOGNISED`, which preparing does not change), plus deprecated `autoparse`, `css_extractor`, `outputs`, `response_type=markdown\|plaintext` | available | +| `extract` | Extract `GET https://api.zenrows.com/v1/` via `extract=auto` (any prepared domain; on one not prepared yet the CLI reports REQS007 as `EXTRACT_DOMAIN_NOT_PREPARED` with the command to prepare it: `zenrows extract prepare --url `, which calls `POST /v1/extract/prepared-domains` with 1 to 5 example URLs: one works; we recommend 3 or more of the same page type; a page Extract did not recognise on a prepared domain is `EXTRACT_PAGE_NOT_RECOGNISED`, which preparing does not change), plus deprecated `autoparse`, `css_extractor`, `outputs`, `response_type=markdown\|plaintext` | available | | `batch` | Batch `https://async.api.zenrows.com/v1` (separate host, `X-API-Key` header). Local JSONL spec validation + credit estimation work with no key. | available | | `crawl` | Crawl `https://api.zenrows.com/v1/crawls` (`X-API-Key` header; same host as Fetch/Extract). Follows the links on each page from one start URL, up to `depth` when set (optional; no depth limit when omitted), on the start URL's registrable domain (subdomains count); returns URLs, and with `output_format: "html"` each page's HTML. Subcommands: `create`, `get`, `list`, `results`, `content`, `download`, `stop`, `wait`. Without Crawl access the API returns 403 `REQS008` → `CRAWL_NOT_ENABLED`. | available | | `browser` | Browser Sessions REST API `https://mcp.zenrows.com/browser/sessions/*` (Bearer; same backend as `@zenrows/mcp` `browser_*`); CDP via `zenrows browser connect`. Escalation-only (prefer fetch/extract); on by default, opt out via `policy.allow_browser`; bills by bandwidth + session time (15-min max) | available | diff --git a/registry/capabilities.json b/registry/capabilities.json index a60ddb5..a6fe807 100644 --- a/registry/capabilities.json +++ b/registry/capabilities.json @@ -17,7 +17,7 @@ "command": "zenrows extract", "backend": "Extract — GET https://api.zenrows.com/v1/ (extract=auto, css_extractor, outputs, response_type; autoparse deprecated)", "requiresAuth": true, - "notes": "Default is extract=auto, which works on any prepared domain with any key. A REQS007 (legacy AUTH010) is reported, never retried, with the next step for its reason: on a domain not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED), prepare it with `zenrows extract prepare --url [--wait]` (POST /v1/extract/prepared-domains: 1 to 5 example URLs; one works, at least 3 of the same page type work better because Extract compares them to check and fill in fields; a few minutes, no credits) and check it with `zenrows extract prepare status `; on a page Extract did not recognise on a prepared domain (EXTRACT_PAGE_NOT_RECOGNISED), try a product, article or listing page, retry it once in a few minutes, or use --css, because preparing again does not change it. CSS / outputs / markdown work on any domain; autoparse is deprecated and runs only with --autoparse." + "notes": "Default is extract=auto, which works on any prepared domain with any key. A REQS007 (legacy AUTH010) is reported, never retried, with the next step for its reason: on a domain not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED), prepare it with `zenrows extract prepare --url [--wait]` (POST /v1/extract/prepared-domains: 1 to 5 example URLs; one works, we recommend 3 or more of the same page type because Extract compares pages of one type to check the fields it found and fill in fields one page is missing; a few minutes, no credits) and check it with `zenrows extract prepare status `; on a page Extract did not recognise on a prepared domain (EXTRACT_PAGE_NOT_RECOGNISED), try a product, article or listing page, retry it once in a few minutes, or use --css, because preparing again does not change it. CSS / outputs / markdown work on any domain; autoparse is deprecated and runs only with --autoparse." }, "batch": { "key": "batch", diff --git a/skills/extract/SKILL.md b/skills/extract/SKILL.md index 481150b..6937e28 100644 --- a/skills/extract/SKILL.md +++ b/skills/extract/SKILL.md @@ -52,13 +52,12 @@ zenrows extract prepare example.com \ zenrows extract https://www.example.com/products/a # 3. extract again ``` -- `--url`: real pages you want to extract, 1 to 5. One works; pass at least 3 - example URLs of the same page type when you can, up to 5: several pages of - each page type you need parsed (several types can share the 5, e.g. 3 product - pages + 2 search-result pages). Extract compares the pages of one type against - each other, so it can check the fields it found and fill in fields one page is - missing; more pages of a type give more complete, more reliable results. With - fewer than 3 the command prints a tip on stderr; it is never an error. +- `--url`: 1 to 5 real pages you want to extract. One works; we recommend 3 or + more of the same page type (e.g. 3 product pages), because Extract compares + pages of one type to check the fields it found and fill in fields one page is + missing. Several page types can share the 5 (e.g. 3 product pages + 2 + search-result pages). With fewer than 3 the command prints a tip on stderr; + it is never an error. - Without `--wait` the command returns at once; then check about every 30 seconds with `zenrows extract prepare status example.com` until it says `ready`. `--wait` polls at the same pace. diff --git a/skills/zenrows/SKILL.md b/skills/zenrows/SKILL.md index f94814c..35c6403 100644 --- a/skills/zenrows/SKILL.md +++ b/skills/zenrows/SKILL.md @@ -83,8 +83,8 @@ Run `zenrows status` for the live capability matrix. As of this toolkit: - Extract: `extract=auto` works on any prepared domain. On one not prepared yet (`EXTRACT_DOMAIN_NOT_PREPARED`), run `zenrows extract prepare --url - --wait` (repeat `--url`: at least 3 pages of the same page type when - you can, up to 5), then extract again. `EXTRACT_PAGE_NOT_RECOGNISED` means the + --wait` (repeat `--url`, 1 to 5: one works; we recommend 3 or more of + the same page type), then extract again. `EXTRACT_PAGE_NOT_RECOGNISED` means the domain is prepared but not this page: do not prepare again; see [[extract]]. - Batch: the reliable path at scale, at Fetch's prices (create, list, status, wait, results, content, pause, resume, rerun, delete). Estimate specs locally with no key. - Crawl: collects the URLs behind one start page, optionally with each page's HTML. diff --git a/src/cli/commands/extract-prepare.ts b/src/cli/commands/extract-prepare.ts index 1d85cd8..c379786 100644 --- a/src/cli/commands/extract-prepare.ts +++ b/src/cli/commands/extract-prepare.ts @@ -5,8 +5,8 @@ * The way out of REQS007: prepare the domain so `extract=auto` works on it. * Submitting returns at once; `--wait` polls until the preparation is ready or * failed (a few minutes). Costs no credits; each new domain uses one slot of the - * account's monthly allowance. One --url works; fewer than 3 prints a hint to stderr - * (never an error) to add more pages of the same page type, up to 5. + * account's monthly allowance. One --url works; with fewer than 3 the command prints a + * tip to stderr (never an error): we recommend 3 or more of the same page type, up to 5. */ import { ensureApiKey } from "../../core/ensure-key.ts"; import { loadConfig } from "../../core/config.ts"; @@ -43,7 +43,7 @@ export const PREPARE_HELP = [ "again does not change that answer.", "", ` --url a real page you want to extract (repeat --url, 1 to ${MAX_EXAMPLE_URLS}). One works;`, - ` at least ${RECOMMENDED_EXAMPLE_URLS} pages of the same page type work better`, + ` we recommend ${RECOMMENDED_EXAMPLE_URLS} or more of the same page type (e.g. 3 product pages)`, " --wait poll every 30 seconds until the domain is ready or failed; off by default", ` --timeout give up waiting after this long (default ${DEFAULT_WAIT_TIMEOUT_MS / 1000})`, " status show progress and the account's monthly allowance", @@ -54,12 +54,10 @@ export const PREPARE_HELP = [ "uses none. The domain is the host (shop.example.com and example.com are separate); a", "leading www. is dropped.", "", - `One example URL works; up to ${MAX_EXAMPLE_URLS} related URLs work better: several pages of each`, - `page type you need parsed. Several types can share the ${MAX_EXAMPLE_URLS}, e.g. 3 product pages +`, - "2 search-result pages. Extract compares the pages of one type against each other, so it", - "can check the fields it found and fill in fields one page is missing: more pages of a type", - `give more complete, more reliable results for that type. Recommended: at least ${RECOMMENDED_EXAMPLE_URLS}`, - "pages of one type, e.g. three product pages:", + `1 to ${MAX_EXAMPLE_URLS} example URLs. One works; we recommend ${RECOMMENDED_EXAMPLE_URLS} or more of the same page type`, + "(e.g. 3 product pages), because Extract compares pages of one type to check the fields", + `it found and fill in fields one page is missing. Several page types can share the ${MAX_EXAMPLE_URLS}`, + "(e.g. 3 product pages + 2 search-result pages). Three product pages:", "", " zenrows extract prepare example.com --url https://www.example.com/products/a \\", " --url https://www.example.com/products/b --url https://www.example.com/products/c --wait", @@ -154,7 +152,7 @@ function validateUrls(raw: unknown, domain: string): string[] { code: "INVALID_USAGE", message: `Pass 1 to ${MAX_EXAMPLE_URLS} example page URLs with --url (got ${urls.length}).`, likely_cause: "Preparation learns the domain from real pages you want to extract.", - next_action: `Add 1 to ${MAX_EXAMPLE_URLS} --url pages; at least ${RECOMMENDED_EXAMPLE_URLS} pages of the same page type work best.`, + next_action: `Add 1 to ${MAX_EXAMPLE_URLS} --url pages. One works; ${RECOMMENDED_EXAMPLE_URLS} or more of the same page type work better.`, suggested_commands: [`zenrows extract prepare ${domain} --url https://${domain}/`], }); } diff --git a/src/core/http.ts b/src/core/http.ts index 39c1af6..3cfbf23 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -369,7 +369,7 @@ export function extractDomainNotPrepared(url: string, body: string): ToolkitErro code: "EXTRACT_DOMAIN_NOT_PREPARED", message: `Extract hasn't prepared ${domain} yet. Prepare it with \`${prepare}\` (a few example URLs, a few minutes, no credits), then retry; meanwhile use --css.`, likely_cause: detail ?? `Extract has not prepared ${domain} yet.`, - next_action: `Run \`${prepare} --wait\` (add --url pages: one works, at least 3 of the same page type work better, up to 5), then retry \`zenrows extract ${url}\`. Without --wait, check progress about every 30 seconds with \`zenrows extract prepare status ${domain}\`. Meanwhile extract with --css and explicit selectors.`, + next_action: `Run \`${prepare} --wait\` (1 to 5 --url pages: one works; we recommend 3 or more of the same page type), then retry \`zenrows extract ${url}\`. Without --wait, check progress about every 30 seconds with \`zenrows extract prepare status ${domain}\`. Meanwhile extract with --css and explicit selectors.`, suggested_commands: [`${prepare} --wait`, `zenrows extract prepare status ${domain}`, css], server_code: "REQS007", }); diff --git a/src/core/prepared-domains-api.ts b/src/core/prepared-domains-api.ts index 89504a8..384e253 100644 --- a/src/core/prepared-domains-api.ts +++ b/src/core/prepared-domains-api.ts @@ -4,8 +4,8 @@ * `extract=auto` answers REQS007 on a domain Extract has not prepared yet. Preparing * it is a call any key can make: `POST {apiBase}/extract/prepared-domains` with the * domain and 1 to 5 example URLs answers 202 at once; the preparation itself takes a - * few minutes and costs no credits. One example URL works; at least 3 of the same - * page type work better (see `RECOMMENDED_EXAMPLE_URLS`). `GET .../{domain}` reports progress and + * few minutes and costs no credits. One example URL works; we recommend 3 or more of + * the same page type (see `RECOMMENDED_EXAMPLE_URLS`). `GET .../{domain}` reports progress and * `GET .../allowance` the account's monthly allowance of new domains. Auth is the * `X-API-Key` header; errors are problem+json and we branch on `status` + `code`. * `fetchImpl` / `sleepImpl` / `now` are injectable for tests. @@ -17,9 +17,9 @@ import { agentClientHeader } from "./agent-client.ts"; export const MAX_EXAMPLE_URLS = 5; /** - * How many example URLs to recommend: 3 pages of one page type. Extract compares - * the pages of a type against each other, so it can check the fields it found and - * fill in fields one page is missing. Advice only: 1 and 2 URLs are still valid. + * How many example URLs to recommend: 3 or more of the same page type, because + * Extract compares pages of one type to check the fields it found and fill in fields + * one page is missing. Advice only: 1 and 2 URLs are still valid. */ export const RECOMMENDED_EXAMPLE_URLS = 3; @@ -30,9 +30,9 @@ export const RECOMMENDED_EXAMPLE_URLS = 3; export function exampleUrlsHint(count: number): string | undefined { if (count >= RECOMMENDED_EXAMPLE_URLS) return undefined; return ( - `Tip: ${count === 1 ? "one example URL works" : `${count} example URLs work`}, but more related URLs work better. ` + - `Add pages of the same page type (at least ${RECOMMENDED_EXAMPLE_URLS}, up to ${MAX_EXAMPLE_URLS} in total): ` + - "Extract compares them to check the fields it finds and fill in fields one page is missing." + `Tip: ${count === 1 ? "one example URL works" : `${count} example URLs work`}; we recommend ` + + `${RECOMMENDED_EXAMPLE_URLS} or more of the same page type (e.g. 3 product pages), up to ${MAX_EXAMPLE_URLS} in total, ` + + "because Extract compares pages of one type to check the fields it found and fill in fields one page is missing." ); } /** How often `--wait` polls, and for how long by default. */ @@ -312,7 +312,7 @@ export function failureNextStep(domain: string, failure: PreparedDomain["failure step = `Run the same zenrows extract prepare ${domain} command again.`; break; case "discovery_failed": - step = `Run zenrows extract prepare ${domain} again with 1 to ${MAX_EXAMPLE_URLS} --url page URLs.`; + step = `Run zenrows extract prepare ${domain} again with 1 to ${MAX_EXAMPLE_URLS} --url page URLs; we recommend ${RECOMMENDED_EXAMPLE_URLS} or more of the same page type (e.g. 3 product pages).`; break; case "unpreparable": step = `Run zenrows extract prepare ${domain} again with different --url pages from the same site; the same URLs fail again.`; diff --git a/tests/capabilities.test.ts b/tests/capabilities.test.ts index 49dde23..48e9e83 100644 --- a/tests/capabilities.test.ts +++ b/tests/capabilities.test.ts @@ -18,7 +18,7 @@ test("extract notes name the prepare step and no automatic autoparse fallback", assert.match(notes, /POST \/v1\/extract\/prepared-domains/); assert.match(notes, /zenrows extract prepare --url /); assert.match(notes, /zenrows extract prepare status /); - assert.match(notes, /1 to 5 example URLs; one works, at least 3 of the same page type work better/); + assert.match(notes, /1 to 5 example URLs; one works, we recommend 3 or more of the same page type/); assert.match(notes, /EXTRACT_PAGE_NOT_RECOGNISED/); assert.match(notes, /preparing again does not change it/); assert.match(notes, /autoparse is deprecated/); diff --git a/tests/extract-prepare.test.ts b/tests/extract-prepare.test.ts index 592d8e2..0f3a8be 100644 --- a/tests/extract-prepare.test.ts +++ b/tests/extract-prepare.test.ts @@ -231,6 +231,7 @@ test("failureNextStep: the step comes from failure.code, the slot sentence from assert.match(failureNextStep("x.com", { code: "interrupted" }), /same zenrows extract prepare/); assert.match(failureNextStep("x.com", { code: "preparation_exhausted" }), /same zenrows extract prepare/); assert.match(failureNextStep("x.com", { code: "discovery_failed" }), /--url page URLs/); + assert.match(failureNextStep("x.com", { code: "discovery_failed" }), /1 to 5 --url page URLs; we recommend 3 or more of the same page type/); assert.match(failureNextStep("x.com", { code: "unpreparable" }), /different --url pages/); assert.match(failureNextStep("x.com", { code: "preparation_failed" }), /other --url pages/); assert.match(failureNextStep("x.com", { code: "verification_failed" }), /support/); @@ -301,8 +302,9 @@ test("prepare with fewer than 3 URLs prints a tip on stderr, keeps the JSON and assert.equal(result!.out.ok, true); assert.equal(result!.out.hint, undefined, "the tip never enters the stdout JSON"); assert.deepEqual((calls[0]!.body as { example_urls: string[] }).example_urls, urls, "1 or 2 URLs are still sent as given"); - assert.match(err, /Tip: .*more related URLs work better/); - assert.match(err, /same page type \(at least 3, up to 5 in total\)/); + assert.match(err, /Tip: .*we recommend 3 or more of the same page type/); + assert.match(err, /up to 5 in total/); + assert.doesNotMatch(err, /at least 3/); } }); @@ -327,7 +329,8 @@ test("exampleUrlsHint recommends 3 and is silent from 3 on", () => { test("prepare help recommends several URLs of one page type, not one per page kind", () => { assert.doesNotMatch(PREPARE_HELP, /one per page kind/); assert.match(PREPARE_HELP, /One works;/); - assert.match(PREPARE_HELP, /at least 3 pages of the same page type work better/); - assert.match(PREPARE_HELP, /3 product pages \+\s+2 search-result pages/); + assert.match(PREPARE_HELP, /we recommend 3 or more of the same page type/); + assert.doesNotMatch(PREPARE_HELP, /at least 3/); + assert.match(PREPARE_HELP, /Several page types can share the 5\s+\(e\.g\. 3 product pages \+ 2 search-result pages\)/); assert.match(PREPARE_HELP, /products\/a[\s\S]*products\/b[\s\S]*products\/c/); });