| The scan endpoint rejects over-limit and mixed-host URL sets, refuses a caller who names a different user, and refuses the legacy client-supplied trigger switch. | DSH-130, DSH-131 | B | shopping-security.test.ts |
| A non-Pro caller is denied before any cache read, claim, meter or task trigger. | DSH-132 | B | shopping-security.test.ts |
| A cache hit costs no claim, no meter, no token and no task run, and the task returns the cached menu without scraping anything. | DSH-133, DSH-153 | B | shopping-security.test.ts, shopping-menu-scan.featuretests.test.ts |
| A second shopper arriving while an identical scan is in flight joins that run with a read-only token and starts no second run. | DSH-134, DSH-171 | B, C | shopping-security.test.ts, dispensaries.integration.test.ts |
| The task payload carries the server-verified subject and a host derived from the URL, never anything the caller supplied. | DSH-135 | B | shopping-security.test.ts |
| A contended claim waits for the winner and joins it, and one that never resolves returns a named pending error rather than hanging. | DSH-137, DSH-138 | B | shopping-routes.featuretests.test.ts |
| An unavailable claim store fails with 503 before metering, and a rate-limited request, a trigger failure or a failed source-run registration all release the claim — a failed attempt never wedges the shop for the next shopper. | DSH-136, DSH-139, DSH-140, DSH-141 | B | shopping-security.test.ts, shopping-routes.featuretests.test.ts |
| A finished run releases its lease and is never re-joined as if in progress: only that run can clear it, an unknown key reports absent, a lease with no run id yet returns a retry, a lease already owned by a successor is left alone, and an unauthenticated completion is refused. | DSH-143, DSH-144 | B | shopping-routes.featuretests.test.ts |
| The API and the scan task compute the same cache key from the same real code, and asking for a cached scan that does not exist answers 404 rather than a server error. | DSH-142, DSH-145 | B | shopping-routes.featuretests.test.ts |
| The task accepts the five-URL ceiling, rejects internal-network and other SSRF-shaped URLs, passes only unique normalized URLs onward, and re-filters its sitemap fallback through the same guard. | DSH-151, DSH-152, DSH-157 | B | shopping-menu-scan.security.test.ts, shopping-menu-scan.featuretests.test.ts |
| The task emits every stage in order with progress that only moves forward, and the client’s stage map covers every stage it emits. | DSH-101, DSH-102, DSH-155 | B | useShoppingAgent.featuretests.test.ts, shopping-menu-scan.featuretests.test.ts |
| A saved scan writes every expected column, the completion callback posts the cache key the API can clear, and a transient failure reporting completion is retried rather than treated as terminal. | DSH-154, DSH-158, DSH-191 | B | shopping-menu-scan.featuretests.test.ts |
| A page with no menu yields an empty menu, never an invented one — zero products is reported as “no products”, not as a failure and not as fabricated items. | DSH-156, DSH-172 | B, C | shopping-menu-scan.featuretests.test.ts, dispensaries.integration.test.ts |
| A missing scraper key fails the run loudly instead of reporting “no products found”, and a very long shop domain does not break the run’s tagging. | DSH-159, DSH-160 | B | shopping-menu-scan.featuretests.test.ts |
| A progress stream that closes without a terminal event is reported as a connection error rather than left spinning and never silently reconnects, and a run past fifteen minutes is aborted and reported as an abort, not an error. | DSH-128, DSH-129 | B | runProgressStream.featuretests.test.ts |