From ec5817ceb19d88af8fc2d2b9b1783e65a07cc1e7 Mon Sep 17 00:00:00 2001 From: Jamie Holding Date: Mon, 28 Sep 2026 19:17:33 +0100 Subject: [PATCH] fix(backfill): an anonymous run says so at the END, where it is read Every documented example omitted the API key, and the docs are the smaller half of it: without a key the command SUCCEEDS. It reads the 7 days anonymous access allows, writes 433 rows of Magic Kingdom instead of about 94,000, and exits 0. The existing notice prints before a run that takes minutes, so it scrolls away, and the last thing on screen is `done: 433 rows`. For a customer who has just paid for 400 days that is indistinguishable from success: a file, no error, and a number that means nothing unless you already know what it should have been. A keyless example is worse than a broken one, because a broken one gets reported. The notice is repeated at the end of the run, naming what the key would have bought, and the README and --help export THEMEPARKS_API_KEY before the example that needs it -- while still saying --list does not, since finding a park before you have paid is the point of that flag. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 6 +++- src/backfill.ts | 21 +++++++++++++ test/unit/backfill.test.ts | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e2968ab..faf91d8 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,11 @@ job as the example below, resumable, and it is what to reach for if what you want is the file rather than the code: ```bash -npx themeparks-backfill --list disney # find your park. No key needed. +# How far back it reaches is your plan, so set the key first: without one you get +# the 7 days anonymous access allows, and the run still succeeds, quietly. +export THEMEPARKS_API_KEY=tpw_your_key + +npx themeparks-backfill --list disney # find your park. This part needs no key. npx themeparks-backfill "magic kingdom" # NDJSON, into the current directory npx themeparks-backfill "Walt Disney World Resort" --format csv --out ./data ``` diff --git a/src/backfill.ts b/src/backfill.ts index a2cfc70..38bce24 100644 --- a/src/backfill.ts +++ b/src/backfill.ts @@ -866,6 +866,10 @@ options: --version print the package version examples: + export THEMEPARKS_API_KEY=tpw_your_key + how far back this reaches is your plan, so without a key you get the 7 days + anonymous access allows — and the run still succeeds, quietly. + themeparks-backfill --list disney find an id, or check a spelling. Destinations with their parks indented underneath. Works before you have a key. @@ -1070,13 +1074,30 @@ export async function main( if (status === EX_TEMPFAIL) return status; if (status !== 0) failed.push(park.name); } + // SAID AGAIN AT THE END, and this is the point of it. The notice above is + // printed before a run that takes minutes, so it scrolls away, and the last + // thing on screen is `done: 433 rows` -- which for a customer who thought they + // were downloading five years is indistinguishable from success. They paid for + // 400 days, got seven, and the command exited 0. + const anonymousNotice = (): void => { + if (apiKey != null) return; + process.stderr.write( + `\nthat was ANONYMOUS ACCESS: the last 7 days only.\n` + + ` a free key reads 30 days, Pro 400, Business the whole archive\n` + + ` set THEMEPARKS_API_KEY and run the same command again\n` + + ` keys: https://www.themeparks.wiki/profile\n`, + ); + }; + if (failed.length > 0) { process.stderr.write( `\n${String(failed.length)} of ${String(targets.length)} did not finish: ${failed.join(', ')}\n` + ` the rest are written. Run the same command again to retry just these.\n`, ); + anonymousNotice(); return 1; } + anonymousNotice(); return 0; } diff --git a/test/unit/backfill.test.ts b/test/unit/backfill.test.ts index c0c284e..32b71bc 100644 --- a/test/unit/backfill.test.ts +++ b/test/unit/backfill.test.ts @@ -1264,3 +1264,65 @@ describe('argument handling', () => { expect(err.join('')).toContain('no park or destination matching'); }); }); + +describe('an anonymous run says so when it finishes', () => { + // The notice at the START scrolls away under a run that takes minutes, and the + // last thing on screen is `done: 433 rows` — which for a customer who thought + // they were downloading five years is indistinguishable from success. Running + // the documented example without a key gives 433 rows of Magic Kingdom instead + // of ~94,000, and exits 0. + let dir: string; + let err: string[]; + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'bf-anon-')); + err = []; + vi.spyOn(process.stderr, 'write').mockImplementation((c) => { + err.push(String(c)); + return true; + }); + vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + }); + afterEach(() => { + vi.restoreAllMocks(); + rmSync(dir, { recursive: true, force: true }); + }); + + const server = async () => { + const page = await loadFixture('mk_park_daily_page2.json'); + const coverage = await loadFixture('mk_history_coverage.json'); + return vi.fn((input: unknown) => { + const url = String(input); + const body = url.includes('/history/coverage') + ? coverage + : url.includes('/history/daily') + ? page + : DESTINATIONS; + return Promise.resolve( + new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' } }), + ); + }); + }; + + it('says it at the end as well as the start', async () => { + const previous = process.env.THEMEPARKS_API_KEY; + delete process.env.THEMEPARKS_API_KEY; + try { + expect(await main([MK, '--out', dir], { fetch: await server() })).toBe(0); + } finally { + if (previous !== undefined) process.env.THEMEPARKS_API_KEY = previous; + } + const text = err.join(''); + expect(text).toContain('ANONYMOUS ACCESS'); + // Both ends: before, so it can be acted on, and after, so it is read. + expect(text.split('7 days').length - 1).toBeGreaterThanOrEqual(2); + expect(text.trimEnd().endsWith('keys: https://www.themeparks.wiki/profile')).toBe(true); + }); + + it('says nothing of the sort when a key was given', async () => { + expect(await main([MK, '--out', dir, '--api-key', 'tpw_test'], { fetch: await server() })).toBe( + 0, + ); + expect(err.join('')).not.toContain('ANONYMOUS'); + expect(err.join('')).not.toContain('no API key'); + }); +});