browser tests run only by hand: the suites that launch Chrome / Chromium move to tests/browser/*.browser.ts (bun test, ci, ci:quick and the Forgejo workflows never pick them up; bun run test:browser runs them); the desktop boot check leaves bun run ci (it stays in desktop-release.ts mac at release time); CI=full is gone: Forgejo's ci runs the Rust steps when the push changed a Rust input (STOREBOUND_CI_BASE=HEAD~1), no Playwright install; CONTRIBUTING

This commit is contained in:
CRTkafa 2026-10-15 01:25:17 +08:00
parent 4c7ff89950
commit cd1e13c1e2
20 changed files with 116 additions and 133 deletions

View file

@ -17,7 +17,7 @@ fails without the fix.
| 63 | 2026-10 | STAR DASH: re-running `play products` / `prices import` to create 8 new IAPs also changed the live prices of existing products; `--only` was needed. In a terminal (no `--yes`) the one confirmation covered price changes too. | Price changes of existing products need `--allow-price-changes`, TTY or not; the terminal question covers creates and other updates only; with the flag a second question lists each old -> new (asked in a terminal even with `--yes`); dry runs show a "price changes on existing products (held)" section (`allowPriceChanges`, `heldSection` in `cli/lib/price-gate.ts`; `play products`, `prices import`, `play subscriptions`, `apple subscriptions`). `tests/price-gate.test.ts` (TTY and non-TTY). |
| 62 | 2026-10 | STAR DASH: `prices ppp` cut high-income markets (Germany EUR 3.59 instead of 5.99 for USD 4.99; UK, France, Japan cut too): it set base x PPP level x exchange rate, a net price, against VAT-inclusive store prices, and cut every region. | Par = the store's own conversion of the US price (Play convertRegionPrices, Apple equalizations; offline base x fx x (1 + VAT) rounded up); share = clamp(PPP level, 0.3, 1); defaults: only decreases, PPP only where the level is <= 0.5, World Bank high-income economies and CN at par, Play skips CN, top-10 revenue market drops > 20 % warned (from `.storebound/reports/` sales reports, else a static list); unchanged regions are left out of the sheet (`shareOf`, `offlinePar`, `planRows`, `topMarketDrops`, `topMarketsFromReports` in `cli/lib/ppp.ts`). `tests/ppp.test.ts`. |
| 61 | 2026-10 | STAR DASH: `prices ppp --json` printed log lines on stdout before the JSON, so parsing stdout failed. | `withJsonOutput` (`cli/lib/log.ts`) wraps every `--json` run in `cli/index.ts`: console.log / info go to stderr, `printJson` writes stdout, prompts use `promptOut()`; commands that printed JSON with console.log use `printJson`. `tests/json-stdout.test.ts` runs every command that reads `--json` against the fake stores and parses stdout. |
| 55 | 2026-10 | The Play feature graphic (1024 x 500) cut the phone off at the bottom: the game template's phone was 232 px wide at top 36 and rotated 7 deg, so its outline spanned about y 24..552 and the game's bottom bar was lost; the app template bled too, and `shots.feature` only took frame + palette. Nothing measured the device against the canvas. | `featureLayout` / `rotatedBox` (`cli/shots/lib.ts`) size the device so its rotated box fits the canvas with a 24 px margin (`shots.feature.fit` 'contain', the default; 'bleed' keeps the crop on purpose; `scale` 0.5-1.5), both templates use it. The templates record each device's measured layout box (`devices` in report.json) and `shots check` fails a feature graphic whose device leaves the canvas unless fit is 'bleed', and warns for a screenshot's front device off a side or the top (`deviceIssues`, `cli/shots/report.ts`). `tests/feature-fit.test.ts`. |
| 55 | 2026-10 | The Play feature graphic (1024 x 500) cut the phone off at the bottom: the game template's phone was 232 px wide at top 36 and rotated 7 deg, so its outline spanned about y 24..552 and the game's bottom bar was lost; the app template bled too, and `shots.feature` only took frame + palette. Nothing measured the device against the canvas. | `featureLayout` / `rotatedBox` (`cli/shots/lib.ts`) size the device so its rotated box fits the canvas with a 24 px margin (`shots.feature.fit` 'contain', the default; 'bleed' keeps the crop on purpose; `scale` 0.5-1.5), both templates use it. The templates record each device's measured layout box (`devices` in report.json) and `shots check` fails a feature graphic whose device leaves the canvas unless fit is 'bleed', and warns for a screenshot's front device off a side or the top (`deviceIssues`, `cli/shots/report.ts`). `tests/browser/feature-fit.browser.ts`. |
| 54 | 2026-10 | The second `apple build` of a Capacitor game archived fine, then `xcodebuild -exportArchive` failed with "exportArchive Copy failed" (exit 70). The IDEDistribution log had the real cause: `rsync error: syntax or usage error (code 1)` in IDEDistributionCreateIPAStep, because a Homebrew rsync 3.x, installed and linked that day by another session's release tool, came first on PATH. A tool changed a global binary on a shared machine as a side effect. | `apple build` runs xcodebuild with Xcode's and Apple's tool folders first on PATH (`xcodePath`) and prints a note when a non-Apple rsync is first (`foreignRsync`); an export failure shows the error lines of the newest IDEDistribution*.log (`distributionErrors`, `cli/lib/xcode.ts`, `cli/apple/build.ts`). `storebound doctor` flags a non-Apple rsync first on PATH. `tools/desktop-release.ts publish` calls rsync by absolute path (`/usr/bin/rsync` without `--chmod`, modes set on the server; else the Homebrew keg), never from PATH (`publishSteps`). Rule: tools never install or relink global binaries as a side effect. `tests/apple-build-export.test.ts`. |
| 53 | 2026-10 | After that failed export, `apple upload build/ios/export/App.ipa --yes` uploaded nothing: it read the stale IPA of build 1, found build 1 already VALID on App Store Connect and exited 0 ("build 1 is VALID"), so `build && upload` looked green. The export also wrote into the same folder every time. | `apple build` exports each build into its own folder `build/ios/export/<version>-<build>/` (removed first) and exits non-zero on any export failure or a missing IPA (`exportIpa`). `apple upload` fails with exit 1 when the IPA's build number is already on App Store Connect ("this ipa is build 1, which is already on App Store Connect (VALID): did the last build fail?"); only `--allow-existing` re-checks it. `--build N` picks / requires that build's IPA; the release and beta flows use `apple upload --no-wait --build ${BUILD} --allow-existing`, so a resumed flow still passes and a stale IPA never does (`existingBuild`, `exportedIpas`, `cli/apple/upload.ts`; `tests/apple-build-export.test.ts`). |
| 52 | 2026-10 | An iOS app that is live on the App Store but whose source is not on this machine could not appear in the desktop: the machine's app list (`~/.storebound/projects.json`) took only folders. | `storebound apps add --store apple\|play --app-id <id> --account <profile>` writes a store-only entry (`storeApps`, no path; `apps folders` shows "no local project", `apps remove apple:<id>`); the desktop adds it to the app list of the account with that profile (`registry::with_store_only`, `store_apps_cached` / `store_apps_refresh`), shown read-only as "Store only"; writing the folder list keeps the entries. `tests/onboard.test.ts`, `src-tauri/tests/machine.rs`, the boot check's `shared-profiles` case (zero uncaught errors). |
@ -31,10 +31,10 @@ fails without the fix.
| 44 | 2026-10 | "en-US: no APP_IPHONE_67 screenshots" blocked live apps that have the 6.5" set: App Store Connect requires the 6.9" set or, without it, the 6.5" set (and scales down from it). | `ACCEPTED_INSTEAD` in `cli/lib/validate-rules.ts`: the 6.5" set satisfies the 6.9" requirement (and either 13" iPad type the other); then a recommendation, not a blocker (`tests/wave1.test.ts`, a live-shape set list with only APP_IPHONE_65). |
| 43 | 2026-10 | The App Store keyword field is limited to 100 characters, not UTF-8 bytes (entry 4 was wrong): live versions READY_FOR_SALE read on 2026-10-13 hold Thai 95 characters / 265 bytes, Russian 91 / 169, Japanese 49 / 123, Turkish 100 / 105, Polish 99 / 105, and App Store Connect accepted them. | `check` (`cli/lib/copy.ts`), `check --aso` (`auditAppleKeywords`), `listing check` (`checkFields`), `listing translate` (`normalizeKeywords`) and `aso suggest` (`fitKeywordField`) count characters; `tests/listing-translate.test.ts`, `tests/wave1.test.ts`, `tests/aso-plus.test.ts` with Thai, Turkish and Polish fields over 100 bytes. |
| 42 | 2026-10 | `apple testflight distribute --build 1 --group Team --yes` failed with 400 on `GET /v1/betaGroups?filter[builds]=`: Apple's spec declares the filter, App Store Connect refuses it, and the spec has no GET of a build's groups. Before the groups existed it said `unknown group "Team" ()`. | Distribute reads each target group's builds (`GET /v1/betaGroups/{id}/relationships/builds`), reports internal all-builds groups as "already available" and never sends them the build; `unknownGroupMessage` points at `groups sync` for configured groups. `APPLE_LIVE_REJECTED` (`cli/api/client.ts`, also refused by `storebound api`); `tests/asc-query-guard.test.ts` checks every App Store Connect GET with a query in `cli/` against the spec's declared parameters and that list; the fake App Store Connect answers 400 to them (`tests/idempotency.test.ts`). |
| 41 | 2026-10 | Every Play App content card ended in an owner-only Save, so the owner came back once per card (privacy policy, ads, app access, advertising ID, government, financial, health, target audience, content rating, data safety). Saving an App content declaration is not public: it goes to Google only with the next "Send changes for review" in Publishing overview (verified 2026-10-13). | Card Saves are the agent's (`declaration-save`): `declarationSaveAllowed` (exactly Save / Kaydet, a button on an /app-content/<card> page) and `flowAllowed` for content rating and target audience, while `publishingControl` refuses "Send changes for review", "Submit N changes for review", Submit, Publish, "Restart review" everywhere; the plans' `buttons.final` is "Send changes for review"; `console fill` presses the declaration's Save; `console plan play-app-content --all` walks every card and ends with one owner list (`cli/lib/console-forms.ts`, `cli/lib/console-plan.ts`, `cli/lib/console-play-plans.ts`, `cli/console/fill.ts`; `tests/console-play-plans.test.ts`, `tests/console-flow.test.ts`). |
| 41 | 2026-10 | Every Play App content card ended in an owner-only Save, so the owner came back once per card (privacy policy, ads, app access, advertising ID, government, financial, health, target audience, content rating, data safety). Saving an App content declaration is not public: it goes to Google only with the next "Send changes for review" in Publishing overview (verified 2026-10-13). | Card Saves are the agent's (`declaration-save`): `declarationSaveAllowed` (exactly Save / Kaydet, a button on an /app-content/<card> page) and `flowAllowed` for content rating and target audience, while `publishingControl` refuses "Send changes for review", "Submit N changes for review", Submit, Publish, "Restart review" everywhere; the plans' `buttons.final` is "Send changes for review"; `console fill` presses the declaration's Save; `console plan play-app-content --all` walks every card and ends with one owner list (`cli/lib/console-forms.ts`, `cli/lib/console-plan.ts`, `cli/lib/console-play-plans.ts`, `cli/console/fill.ts`; `tests/console-play-plans.test.ts`, `tests/browser/console-flow.browser.ts`). |
| 40 | 2026-10 | Finding a new app's Play Console app id (`play.appId`) took several steps: the app list shows 10 apps per page and the new app was not on page 1. The id was in the URL Create app redirects to (/developers/<devId>/app/<appId>/app-dashboard) all along. | `console verify play-app-record --url <url>` reads the id from the redirect (or the capture JSON's url), shows the `play.appId` change as a dry diff and `--write` upserts it (`upsertNestedString`); `storebound play app-id` finds it by package name across captured app list pages (search box or pages) as an agent step (`cli/lib/play-app-id.ts`, `cli/play/app-id.ts`; `tests/play-app-id.test.ts`, a modeled 3-page list). |
| 39 | 2026-10 | Play IARC questionnaire: page 1's Next did nothing until the IARC Terms of Use box was ticked, an owner-only agreement, so the agent stopped mid-run and waited for the owner. | The box is in the plan's ONE owner batch before the run with its exact place (`ownerBatch`, printed first; `play-app-content` and `play-content-rating`); the step says not to press Next again and again; `console fill` waits for the tick ("agreement needed on …", `blocksNext`) instead of pressing a Next that does nothing (`tests/console-play-plans.test.ts`, `tests/console-flow.test.ts`). |
| 38 | 2026-10 | App Store Connect's New App dialog: a programmatic value set (a form_input-style set) on the Name and SKU textboxes left the dialog's React state empty (""), while clicking the field and typing worked. | The `apple-app-record` plan says "type, don't set value" (uiFacts and the fill step); `console fill` types every text field key by key and reads it back (`typeText` in `cli/console/fill.ts`); `tests/console-flow.test.ts` (a New App fixture whose state follows key presses only). |
| 39 | 2026-10 | Play IARC questionnaire: page 1's Next did nothing until the IARC Terms of Use box was ticked, an owner-only agreement, so the agent stopped mid-run and waited for the owner. | The box is in the plan's ONE owner batch before the run with its exact place (`ownerBatch`, printed first; `play-app-content` and `play-content-rating`); the step says not to press Next again and again; `console fill` waits for the tick ("agreement needed on …", `blocksNext`) instead of pressing a Next that does nothing (`tests/console-play-plans.test.ts`, `tests/browser/console-flow.browser.ts`). |
| 38 | 2026-10 | App Store Connect's New App dialog: a programmatic value set (a form_input-style set) on the Name and SKU textboxes left the dialog's React state empty (""), while clicking the field and typing worked. | The `apple-app-record` plan says "type, don't set value" (uiFacts and the fill step); `console fill` types every text field key by key and reads it back (`typeText` in `cli/console/fill.ts`); `tests/browser/console-flow.browser.ts` (a New App fixture whose state follows key presses only). |
| 37 | 2026-10 | A `scan.ignore` entry whose `match` text was in the evidence (a code line) instead of the finding title accepted nothing and said nothing. `init --profile game` wrote example-looking values into `.storebound/credentials.json` (`YOUR_KEY_ID`, `/absolute/path/to/upload.p12`, `sk_your_revenuecat_v2_secret_key`), which commands read as real credentials. | `scan.ignore[].evidence` matches an evidence line ("file:line text"), `match` stays the title; `scan` (report notes) and `check` (warning) name every entry that accepts no finding (`ignoreMatches` / `applyIgnore` / `unusedIgnores` in `cli/lib/scan/rules.ts`). `templates/credentials.example.json` holds empty strings only; `isPlaceholder` (`cli/lib/credentials.ts`) makes "your-…", `<…>`, "xxx", "/absolute/path/to/…", "example" and the zero Apple Ads client id missing in every credential read; `check` warns, `doctor` shows set / missing / placeholder per key. `tests/game-report.test.ts`. |
| 36 | 2026-10 | Privacy answers derived from the wrong source: `privacy-manifest --propose` wrote an EMPTY NSPrivacyCollectedDataTypes while `appPrivacy.types` listed 5 collected types; `console plan apple-app-privacy` added "Other Usage Data" from dataSafety (PSL_OTHER_APP_ACTIVITY) although `appPrivacy.types` was given; `console plan play-advertising-id` answered "uses advertising ID" Yes because dataSafety declared PSL_DEVICE_ID (a random install id, not the advertising ID). | `--propose` builds NSPrivacyCollectedDataTypes from `appPrivacy.types` (types, linked, tracking, purposes as Apple's manifest keys; NSPrivacyTracking still only with app-level domains, lesson 29) and `check` warns on an empty list against collected types. `appPrivacy.types` is authoritative in the App Privacy sheet (unlisted = not collected, certain); without it, tracking false and no ad / attribution SDK make Advertising Data, Customer Support, Environment Scanning, Hands and Head "not collected" unless a project signal suggests them (`cli/lib/project-signals.ts`). Advertising ID comes from ad / attribution SDKs, the AD_ID permission (source or merged manifest, or its `tools:node="remove"`), ads.appAdsTxt or `console.appContent.advertisingId`; none: No (certain), a conflict: ask (`advertisingIdUse`). `tests/game-report.test.ts`. |
| 35 | 2026-10 | `scan` on a Capacitor game: rule `review-prompt` (3.2.2(x), reward for rating) fired on `const rate = F.genRate(lv, bonus.gen(r));` (a bare "rate" next to "bonus"); every account finding cited `import './buildings/register';` as sign-up evidence; and 5.1.1(v) cleared as soon as `accounts.inApp` named a screen the code did not have. | The incentive signal needs a rate-us / leave-a-review phrase next to a reward, a reward "for rating / reviewing", or a reward granted within a few lines of a system review prompt call (`rewardNearReviewApi`). Sign-in / sign-up / deletion signals skip import, require and export-from lines (`codeLine`), and auth endpoints must be an absolute path, URL or `${base}/...`, never a module path; config-only accounts say "from config (accounts.enabled)". A config claim of in-app deletion without code evidence stays a finding (medium, "claimed by config, not found in code"). `tests/game-report.test.ts` with `tests/fixtures/scan/capacitor-game`. |
@ -52,21 +52,21 @@ fails without the fix.
| 23 | 2026-10 | Capacitor: native build ships old web assets. `flow run release --apply` went straight into `apple build` (xcodebuild archive) with `ios/App/App/public` and `android/app/src/main/assets/public` 3.5 hours older than the latest source commit: the native projects hold the copy `npx cap sync` made last, and nothing ran the web build + cap sync first, so the old game would have shipped. | New `build.prebuild` (per store `apple.prebuild` / `play.prebuild`): `apple build` / `play build` run it first (printed, streamed, nonzero exit stops; `--dry` only prints; `--no-prebuild`; once per `flow run` through `STOREBOUND_PREBUILD_DONE`). After it, a Capacitor project's copied `public/index.html` must be newer than the last commit outside ios/ android/ node_modules/ store/ .storebound/ and `<webDir>/index.html`, else the build stops ("web assets are older than the source ..."; `--allow-stale-web` overrides). `check` warns when stale without a prebuild; build output and dry plan show the bundle's sha256 + mtime. Monorepo (`apps/native` with its own capacitor.config.ts, android/, ios/; `play build` failed with "no Gradle project ... in android or ."): the one Capacitor project one or two levels down is found and named, several ask for `play.build.dir` / `apple.build.*` (`cli/lib/web-assets.ts`, `tests/web-assets.test.ts`). |
| 22 | 2026-10 | `play build` built a good AAB (Capacitor 8 / AGP), then failed its own check with "AAB manifest without package / versionCode". The aapt2 proto manifest was read with element field numbers one too high (name 4, attribute 5, child 6 instead of Resources.proto's 3, 4, 5). The test fixture came from the same wrong encoder, so the tests passed. | `aabInfo` / `encodeProtoManifest` in `cli/lib/gradle.ts` follow Resources.proto. `tests/build-sign.test.ts` builds the manifest bytes by tag, without the encoder, and the parser was checked against real AGP bundles. Lesson: a parser's fixture must not come from the same code's encoder. |
| 21 | 2026-10 | Play Data safety: conditional required rows. `play data-safety` for a game with username accounts (`accountCreation: ['PSL_ACM_USER_ID_OTHER_AUTH']`) got 400 "Invalid safety labels declaration: Response missing for PSL_ACCOUNT_DELETION_URL": once account creation is not `PSL_ACM_NONE`, Play requires the delete-account URL and the data-only deletion question (`PSL_SUPPORT_DATA_DELETION_BY_USER`, and `PSL_DATA_DELETION_URL` when it is yes), and refuses `PSL_HAS_OUTSIDE_APP_ACCOUNTS`; storebound wrote none of them. Later (2026-10-12) a game got 400 "Line 7: Response ID missing" for `PSL_SUPPORT_DATA_DELETION_BY_USER,,FALSE`: the current form asks it as a single choice (`DATA_DELETION_YES` / `DATA_DELETION_NO` / `DATA_DELETION_NO_AUTO_DELETED`) and no longer has `PSL_DATA_COLLECTION_USER_REQUEST_DELETE`; the CSV must use the current form's Response IDs. | `dataSafetyRows` writes them from `accounts.deletionUrl` and `dataSafety.partialDeletion` / `partialDeletionUrl`; `DATA_SAFETY_RULES` / `typeRules` / `dataSafetyIssues` in `cli/lib/data-safety.ts` check every conditional row in `check`, `project.declarations` and `play data-safety` (dry and real, before the POST); `tests/pure.test.ts` (conditional required rows), `tests/idempotency.test.ts`, `tests/app-privacy.test.ts`. The single choice is written one row per option (`partialDeletion: true | false | 'auto90'`); every (Question ID, Response ID) pair, `dataSafety.extra` included, is checked against Google's empty form exported from Play Console (`cli/data/play-data-safety-form.csv`, 2026-10-12; refresh: App content > Data safety > Export to CSV, nothing answered) by `formPairIssues`, and each Answer requirement comes from it; `tests/pure.test.ts` (pairs from Play's exported form). |
| 20 | 2026-10 | Hours of selector-fix cycles of the scripted console engine (`console fill`): the real console kept differing from the model (App Privacy "Set Up" as plain text, purpose checkboxes without an accessible name, two information steps in a type dialog, Play's material radios that ignore the native input). | Agent-first console mode: `console plan <form>` gives an AI agent the start URL, steps, answer sheet with the console's labels, never-press list and verified UI facts; the agent drives the page with its own browser tool and stops before the final button; `console verify <form>` checks the result (App Privacy summaries from the page text, Play Create app from input states, API for the App Store record and age rating). The scripted fill stays a fast path (and now handles the verified facts). MCP: `console_plan`, `console_verify` (read-only). `tests/console-plan.test.ts`, `tests/console-flow.test.ts`. |
| 20 | 2026-10 | Hours of selector-fix cycles of the scripted console engine (`console fill`): the real console kept differing from the model (App Privacy "Set Up" as plain text, purpose checkboxes without an accessible name, two information steps in a type dialog, Play's material radios that ignore the native input). | Agent-first console mode: `console plan <form>` gives an AI agent the start URL, steps, answer sheet with the console's labels, never-press list and verified UI facts; the agent drives the page with its own browser tool and stops before the final button; `console verify <form>` checks the result (App Privacy summaries from the page text, Play Create app from input states, API for the App Store record and age rating). The scripted fill stays a fast path (and now handles the verified facts). MCP: `console_plan`, `console_verify` (read-only). `tests/console-plan.test.ts`, `tests/browser/console-flow.browser.ts`. |
| 19 | 2026-10 | `play images --dry` aborted with "en-US: no sevenInchScreenshots ... (render first)" on a phone-only project: Play's 7" and 10" tablet screenshots are optional (required: 512 icon, 1024x500 feature graphic, at least 2 screenshots; Play Console Help answer 9866151), but every image type was treated as required. | `play images` uploads what exists (icon, feature graphic, phone screenshots, tablet / TV / Wear only when rendered), prints an info line for missing optional sets, never deletes what Play has for them without `--prune`; `play.tablets` (default: tablet renders exist) switches tablets for `shots render` / `shots check` / `shots gallery` / `validate` / `play images`, and `true` with missing renders is an error; then a 404 "Listing for language 'de-DE' not found." for languages still `status=draft`: `play images` takes the languages `play listing` pushes and skips one with no Play listing as info (`noListing`) (`cli/play/images.ts` `langImages`, `resolvedTablets` in `cli/shots/report.ts`, `tests/play-post-reads.test.ts`). |
| 18 | 2026-10 | `console fill play-app-record` with a copy of the owner's Chrome cookies reached the Play account chooser once, then Google answered `/v3/signin/rejected` and the chooser: Google session cookies are device-bound (Chrome's Device Bound Session Credentials) and Google signs out sessions whose cookies look stolen, which can also sign the owner out of their real Chrome. | Never copy Google cookies by default: storebound's own profile is the default (the owner signs in once by hand), started by storebound as a normal Chrome (no `--enable-automation`, `navigator.webdriver` false) and attached over CDP; `--use-chrome-session google\|apple` is an explicit opt-in that warns; `/signin/rejected` is said plainly (`cli/console/session.ts`, `tests/console-session.test.ts`). Confirmed 2026-10-12: the earlier default copy DID sign the owner out of Play Console in their real Chrome. |
| 17 | 2026-10 | After the data types were saved, the real App Privacy page shows each type's "Set Up <type>" as a plain `<p>` in a div with a click handler (no button role, no link) and puts the types in a box after the Data Types heading's box: role-based locators and the "nearest heading" check found nothing, so no type was set up. A "?" (More information) button sits next to each type. | `setUpAllowed` / `setUpTexts` (`cli/lib/console-forms.ts`: exact text "Set Up <type>" of a type storebound answers, in the Data Types section, outside dialogs) and `setUpClick` / `setUpType` / `flowWhere` (last heading before the control) in `cli/console/fill.ts`; the per-type dialog steps are `page` sections, so an unknown step dumps and pauses instead of ending the run; `tests/fixtures/console/apple-app-privacy-setup.html` (structure of the real page) and `tests/console-flow.test.ts`. |
| 16 | 2026-10 | `console fill play-app-record` with the copied Chrome cookies printed only "still waiting for the sign-in" for 5+ minutes on a signed-in Play Console: Google pages embed `accounts.google.com` frames (the account widget, cookie rotation), and the detector counted any frame on a sign-in host. | `signInReason`: a sign-in page only by its top-level URL, a visible password / email-entry form in the main frame, or a visible Apple sign-in frame (idmsa / appleid); other frames never count. Each detection logs "sign-in detected because: <rule> at <url>"; heartbeats name the page; a copied session that lands on a sign-in URL is said plainly ("did not carry over"); the watchdog dumps the sign-in wait once after `--step-timeout`, `--debug-dump` up to 3 times (`tests/console-fill.test.ts`, chooser / home with the widget frames, a top-level accounts.google.com page). |
| 15 | 2026-10 | The real App Privacy "Data Collection" dialog names each data type checkbox by its label run together with its help text ("NameIncluding first or last name"), and labels some types differently from Apple's help page ("Purchases", "Other Data"): exact-name matching found none. | `labelNames` (exact name, then the label as a prefix that ends where the help text starts) and `TYPE_ALIASES` in `cli/lib/console-forms.ts`; the flow fixture mirrors the real dialog's structure (`tests/console-flow.test.ts`). |
| 14 | 2026-10 | `console fill apple-app-privacy` clicked Next onto a real console page it did not recognise and then waited 8 hours without a single log line. | Every wait of the console engine is bounded: a step watchdog (`--step-timeout`, 30 s) writes a sanitized debug dump to `.storebound/console-debug/` and prints the page's buttons and the manual steps; a line a minute while waiting; no progress for 20 min stops the run with exit 1; `--debug-dump` dumps every step (`cli/console/debug.ts`, `tests/console-flow.test.ts`, which also scans the engine for waits without a deadline). |
| 18 | 2026-10 | `console fill play-app-record` with a copy of the owner's Chrome cookies reached the Play account chooser once, then Google answered `/v3/signin/rejected` and the chooser: Google session cookies are device-bound (Chrome's Device Bound Session Credentials) and Google signs out sessions whose cookies look stolen, which can also sign the owner out of their real Chrome. | Never copy Google cookies by default: storebound's own profile is the default (the owner signs in once by hand), started by storebound as a normal Chrome (no `--enable-automation`, `navigator.webdriver` false) and attached over CDP; `--use-chrome-session google\|apple` is an explicit opt-in that warns; `/signin/rejected` is said plainly (`cli/console/session.ts`, `tests/browser/console-session.browser.ts`). Confirmed 2026-10-12: the earlier default copy DID sign the owner out of Play Console in their real Chrome. |
| 17 | 2026-10 | After the data types were saved, the real App Privacy page shows each type's "Set Up <type>" as a plain `<p>` in a div with a click handler (no button role, no link) and puts the types in a box after the Data Types heading's box: role-based locators and the "nearest heading" check found nothing, so no type was set up. A "?" (More information) button sits next to each type. | `setUpAllowed` / `setUpTexts` (`cli/lib/console-forms.ts`: exact text "Set Up <type>" of a type storebound answers, in the Data Types section, outside dialogs) and `setUpClick` / `setUpType` / `flowWhere` (last heading before the control) in `cli/console/fill.ts`; the per-type dialog steps are `page` sections, so an unknown step dumps and pauses instead of ending the run; `tests/fixtures/console/apple-app-privacy-setup.html` (structure of the real page) and `tests/browser/console-flow.browser.ts`. |
| 16 | 2026-10 | `console fill play-app-record` with the copied Chrome cookies printed only "still waiting for the sign-in" for 5+ minutes on a signed-in Play Console: Google pages embed `accounts.google.com` frames (the account widget, cookie rotation), and the detector counted any frame on a sign-in host. | `signInReason`: a sign-in page only by its top-level URL, a visible password / email-entry form in the main frame, or a visible Apple sign-in frame (idmsa / appleid); other frames never count. Each detection logs "sign-in detected because: <rule> at <url>"; heartbeats name the page; a copied session that lands on a sign-in URL is said plainly ("did not carry over"); the watchdog dumps the sign-in wait once after `--step-timeout`, `--debug-dump` up to 3 times (`tests/browser/console-fill.browser.ts`, chooser / home with the widget frames, a top-level accounts.google.com page). |
| 15 | 2026-10 | The real App Privacy "Data Collection" dialog names each data type checkbox by its label run together with its help text ("NameIncluding first or last name"), and labels some types differently from Apple's help page ("Purchases", "Other Data"): exact-name matching found none. | `labelNames` (exact name, then the label as a prefix that ends where the help text starts) and `TYPE_ALIASES` in `cli/lib/console-forms.ts`; the flow fixture mirrors the real dialog's structure (`tests/browser/console-flow.browser.ts`). |
| 14 | 2026-10 | `console fill apple-app-privacy` clicked Next onto a real console page it did not recognise and then waited 8 hours without a single log line. | Every wait of the console engine is bounded: a step watchdog (`--step-timeout`, 30 s) writes a sanitized debug dump to `.storebound/console-debug/` and prints the page's buttons and the manual steps; a line a minute while waiting; no progress for 20 min stops the run with exit 1; `--debug-dump` dumps every step (`cli/console/debug.ts`, `tests/browser/console-flow.browser.ts`, which also scans the engine for waits without a deadline). |
| 13 | 2026-10 | App Privacy purposes were over-mapped: Play "advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse Location, Product Interaction, Performance Data). | `applePurpose` maps one Play purpose to one Apple purpose (advertising: third-party only); Developer's Advertising only from `appPrivacy.types`; `check` compares `appPrivacy.types` with Info.plist / PrivacyInfo.xcprivacy (`tests/app-privacy.test.ts`). |
| 12 | 2026-10 | A new Play app has no API: Play Console's "Create app" is the only way, its page-level button carries the same words as the form's final submit, and Free cannot become Paid later. | `console fill play-app-record`: `createAppAllowed` lets the opener through only on the app list, never in a form or dialog (EN + TR); declarations never ticked; Free / Paid outlined as an owner check (`tests/console-fill.test.ts`). Verified 2026-10-12: the form is at `/developers/<id>/create-new-app` and has an App package name field with a read-only "Check availability" (filled from `play.packageName`; the package name no longer comes from the first AAB upload); App or game / Free or paid are `<material-radio>`s whose model only updates on a click of the inner `[role=radio]` (the engine clicks it; `console verify play-app-record` checks aria-checked and the "Select …" hints). After Create app the owner adds the new app to the service account (app-level permissions do not cover later apps). |
| 12 | 2026-10 | A new Play app has no API: Play Console's "Create app" is the only way, its page-level button carries the same words as the form's final submit, and Free cannot become Paid later. | `console fill play-app-record`: `createAppAllowed` lets the opener through only on the app list, never in a form or dialog (EN + TR); declarations never ticked; Free / Paid outlined as an owner check (`tests/browser/console-fill.browser.ts`). Verified 2026-10-12: the form is at `/developers/<id>/create-new-app` and has an App package name field with a read-only "Check availability" (filled from `play.packageName`; the package name no longer comes from the first AAB upload); App or game / Free or paid are `<material-radio>`s whose model only updates on a click of the inner `[role=radio]` (the engine clicks it; `console verify play-app-record` checks aria-checked and the "Select …" hints). After Create app the owner adds the new app to the service account (app-level permissions do not cover later apps). |
| 11 | 2026-10 | Google Play has no public search API, so there is no Play keyword rank or Play competitor list to read without scraping. | `aso` reads App Store search only (iTunes Search API); `aso audit` covers Play through the listing text (short description, density); the aso-basics skill says so. |
| 10 | 2026-10 | The iTunes Search API order is close to, not equal to, App Store search (no personalisation, no search ads, no in-app event rows) and it has no subtitle field. | `aso rank` / `aso competitors` / `aso audit` label ranks as a trend and show no competitor subtitles; popularity only from Apple Ads, never estimated (`cli/lib/aso-intel.ts`). |
| 9 | 2026-10 | A retried or repeated write can duplicate on a live store: a POST create retried after a timeout or 5xx, a re-run during the list lag right after a create, two runs at once, a Play edit redone after an unclear commit, a crash in the middle of a screenshot set. | Every write is an upsert (`cli/lib/upsert.ts`: findOrCreate with a fresh re-check and a journal of recent creates, createOnce treating 409 as found, no blind retry of a create); API clients retry only requests that are safe twice; Play edits are all or nothing; asset sets keep the matching prefix; one lock per app and store (`cli/lib/lock.ts`, `--force-unlock`). Table: `docs/idempotency.md`; `tests/idempotency.test.ts` runs every write twice and asserts zero writes on the second run. |
| 8 | 2026-10 | Capacitor plugins in a monorepo live in `apps/native/node_modules`, so `scan` missed them. | `scan` / `privacy-manifest` resolve workspace packages (`cli/lib/scan/project.ts`, `cli/lib/node-packages.ts`). |
| 7 | 2026-10 | Console UIs: the App Store Connect sign-in runs in an `idmsa.apple.com` iframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies without `--restore-last-session`. | `console` session and fill (`cli/console/`), `tests/console-*.test.ts`. |
| 6 | 2026-10 | Screenshot fonts: only part of the glyphs a language needs were available, so Turkish / Polish / Vietnamese characters fell back to another font silently. | `shots fonts` / `shots check` check every language's characters against the fonts (`tests/glyphs.test.ts`). |
| 6 | 2026-10 | Screenshot fonts: only part of the glyphs a language needs were available, so Turkish / Polish / Vietnamese characters fell back to another font silently. | `shots fonts` / `shots check` check every language's characters against the fonts (`tests/browser/glyphs.browser.ts`). |
| 5 | 2026-10 | Account features do not imply email collection; Data safety said "email collected" without evidence. | `scan` marks email only with code evidence, else "ask owner" (`cli/lib/scan/declare.ts`). |
| 4 | 2026-10 | (Superseded by 43: the limit counts characters.) The App Store keyword limit (100) was taken to count UTF-8 bytes. | See 43. |
| 3 | 2026-10 | App Store app names are unique per storefront: "already being used" although the developer owns no such app. | `apple.name` (App Store name with a suffix the owner picks); `app.name` stays the brand. |