Repository navigation
1.9.79: Customer LTV's pages read the record a button names exactly - #308
Merged
Merged
Conversation
LTV webhooks were queued by every /ltv write and sent by nothing a
default install runs: 202-cronjobs/ltv_webhooks.php was the only
dispatcher, and every default scheduler (the installer's cron line,
install.sh, both Docker cron services, AutoCron) fetches index.php
alone. EveryCronJobIsScheduledByDefaultTest listed it as NOT_SCHEDULED
("opt-in, predates the measurement rewrite": history, not a reason). The
Landing Page Optimizer's pairing webhook waited with the rest.
The dispatch moves into Prosper202\Ltv\WebhookDispatcher, and two entry
points run it: the minutely tier of index.php (last, with a 15-second
budget) and ltv_webhooks.php on its own (--budget=N, default 50), which
the Coolify worker service now runs too. Both take one MySQL named
lock, as the attribution worker and the Android intake do, and each
delivery is still claimed by compare-and-set before it is sent, so runs
the lock does not see cannot send it twice either.
- The deadline is asked before each delivery, at least one a run, so a
run ends at most one delivery past it.
- A webhook whose delivery took 2 s or more to fail is not sent to again
in that run; its other deliveries stay due, untried, and a cursor on
(next_attempt_at, delivery_id) steps past them. A receiver that times
out costs one timeout a run instead of the budget every webhook on the
install shares. A failure that comes back at once is retried at its own
pace, as before.
- Retries and backoff are unchanged (recordAttempt(): 2^n minutes, failed
after 6, the webhook marked dead). A refusal, a failed lookup or a send
that throws fails its own delivery and the run goes on (#24).
- A delivery still unsent 7 days after it was queued is failed with
"expired: ..." as its last response, never sent, its webhook left
active: an upgraded install sends the last week of its backlog, oldest
first, rather than months of stale events. Expired 1,000 a statement;
a run whose budget ends first sends nothing, since what is left to
expire is the oldest due.
- The response body is stored as UTF-8 cut at a character boundary: a
body that is not UTF-8 was refused under a strict sql_mode, the outcome
went unrecorded and the delivery would have been sent again.
- post() builds every option through requestOptions(), the guard's set
winning over the POST's, and refuses to send when curl refuses one.
Tests: WebhookDispatchIntegrationTest (a real server, a php -S receiver,
two child processes for the overlapping runs, under the lock and
without it), WebhookDispatcherTest, LtvWebhooksAreSentEveryMinuteTest
(the minutely tier, last, with its budget; the Coolify worker), and the
scheduling, guard-parity and LTV server-failure tests updated. Each
removal planted was caught by a named test. tests/live/ltv-webhooks.sh
is the pass end to end against an https receiver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
1.9.79 gained #262. The only conflict was 202-config/changelogs.json, where both sides added 1.9.79 lines: 1.9.79's lines are kept and this branch's follow them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in what 1.9.79 gained since this branch last merged it. The conflicts were only in the changelogs, where both sides added 1.9.79 lines: 1.9.79's lines are kept as it has them and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… in the last 7 days A webhook is marked dead when one delivery fails 6 times, about an hour of retries now that #273 sends them, and nothing could make it active again: LTV Settings said to register it again, which for the Landing Page Optimizer, whose pairing keeps the webhook's id and secret, meant pairing again. A dead webhook was also queued nothing, and the deliveries it had pending spent their remaining attempts on "endpoint not active", so the events of an outage were lost for good. - MysqlWebhookRepository::revive() makes a webhook that is not active active again with its id, URL and secret, resets its pending deliveries' attempts to 0, and (requeue, the default) makes every delivery of it not delivered and queued within the dispatcher's 7 days pending again, due now: the one that killed it and the ones held while it was dead. A delivered one is never touched. It locks the webhook row first, and an active webhook is left as it is. - While dead, enqueue() still queues its events, and the dispatcher holds each (holdDelivery(): failed, attempts as they were, "held: …"), never trying it. The hold asks the webhook's status again in its own UPDATE, so a delivery read while the webhook was dead and revived since is sent. - PUT /ltv/webhooks/{id} {"status": "active", "requeue": bool}, LTV Settings' Revive button on a dead row (its confirm names how many it missed), p202 ltv webhooks revive <id> [--requeue=false] and bin/p202 ltv:webhook:revive <id> [--no-requeue]. requeue is the default: the receiver was down, the events it missed are what keeps a subscriber (the optimizer's conversions, a CRM's revenue) consistent, the 7 days are the window the dispatcher already applies, and every one goes with its own X-P202-Delivery, so a receiver that did take one de-duplicates it. requeue false is there for a receiver that was replaced and wants new events only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…webhook-id This branch is stacked on #289: its Revive action reads the webhook id strictly, and the fix here reads Delete's id the same way. No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
LTV Settings' Delete read webhook_id with (int), so an id sent as 12abc deleted webhook 12: one of the account's own, and not the one named (CLAUDE.md #4). Measured live against the page before this change: webhook_id=5abc answered "Webhook deleted." and webhook 5 was gone. Delete now reads the id as #289's Revive does, through one helper both use (Prosper202\Http\PostedId::read(): digits only, no sign, no leading zero, round-tripped so a value past PHP_INT_MAX is no id), and refuses anything else with "Pick the webhook to delete from the list.". The page's other casts are swept the same way: the field's and the integration's Delete, the delivery log's webhook id, the customer c-param (1abc saved c1), the Required box (!empty() read "false" as ticked), the webhook events (a string, or no box ticked, subscribed the webhook to every event), and every text field, which (string) read as "Array" when sent as a list (an integration's provider was stored as "array"). ForbidNumericCastOfRequestParamRule now reads the request superglobals outside api/ as well, naming PostedId::read() there; the 28 pages that still cast (66 sites) are listed by count in phpstan-baseline.neon, so the counts only shrink. CLAUDE.md #4 says so, and what the rule does not see. Tests: PostedIdTest; LtvSettingsPostedValuesIntegrationTest runs the page as the account (page-runner) and posts each spelling; the rule's fixture now plants superglobal casts outside api/. tests/live/ ltv-settings-posted-ids.sh is the live pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… and waits for a run that is sending revive() with requeue false only reset the pending deliveries' attempts and made the webhook active, so the next run sent every delivery queued while it was dead that no run had held yet, and every one waiting out a retry's backoff: "new events only" sent old ones (Codex, on #296). Without requeue, every delivery of the webhook still pending is now held in the revival's transaction, as a dead webhook's are: failed, its attempts as they were, "held: its webhook was revived for new events only, …", never sent. The answer says how many (held). With requeue, the pending ones are sent: their attempts go back to 0 and each keeps its due time, so one waiting out a backoff waits at most 32 minutes. A claim (claimDelivery()) reads the same as a backoff, so a due time is never shortened. The failed ones it sends again are due at their queueing rather than at the revival, so they go oldest first with the pending ones no run has held yet. A delivery a run has claimed and not yet recorded must not be held or made due under it: a hold would be overwritten by the run's record (a held delivery sent anyway), and a requeued one could be claimed again while the first send is in flight. revive() now takes the dispatcher's lock (WebhookDispatcher::betweenRuns()), waiting up to 20 seconds for a run in flight; a run still sending then is an LtvConflictException, a 409 through the API with nothing changed, and the Go CLI hints to run it again in a minute rather than "update it instead". Tests: WebhookReviveIntegrationTest gains a pending row at revival time both ways and a revival from a second session while a run has claimed a delivery; each failed against a78e59a. The live pass revives with a delivery still pending, both ways. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…v-delete-webhook-id Brings in #289's fix for a revival without requeue, which takes the dispatcher's lock and can answer a conflict. The conflicts were only in changelogs.txt and tests/live/README.md: #289's revised revive lines are kept as it has them, and this branch's lines follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
#289's revive() now waits up to 20 seconds for a dispatcher run still sending and then throws LtvConflictException with nothing changed. The page showed the repository's API-worded sentence; it now says, in its own words, that webhook #N was not revived because deliveries are being sent right now, nothing was changed, and to revive it again in a minute. LtvSettingsPostedValuesIntegrationTest holds the dispatcher's lock while the page revives a dead webhook: the sentence, and the webhook still dead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Companies' Merge read company_id and source_company_id with (int), so an id sent as 12abc merged company 12: one of the account's own, and not the one named (CLAUDE.md #4). The same cast read Companies' Edit and Delete, Products' Save and Delete, the customer page's customer_id (so Erase, Merge, Link Identity, Unlink and Save Changes acted on customer 12, as did a link with ?customer_id=12abc), its source_customer_id and alias_id, the merge search's exclude, and every list's offset and Edit row. Measured live against origin/1.9.79: source_company_id=2abc answered "Companies merged." and company 2 was gone; customer_id=1abc erased customer 1; a Delete posted with offset=50abc deleted the company. Each id is read through PostedId (#296) by tracking202/ajax/ltv_helpers.php's p202_ltv_posted_id(), and anything else (12abc, 0, -1, empty, absent) is refused with the page's sentence naming what to pick ("Pick the company to merge away from the list.", "Pick the customer from the Customer LTV list.", "Pick the identity to unlink from the list.") before anything is changed. The merge search answers a refusal as a 422 with the sentence, which its dialog now shows instead of "Search failed". The pages' other values are read the same way, as #296 did for LTV Settings: a page the pager did not write ("Pick a page with Previous or Next.", and a request carrying one runs no action), a grouping, segment or status the list does not offer, and an unknown action are said, where they were read as the first page, the default or nothing; a value sent as a list is refused, where (string) stored a company name or an identity as "Array"; and a customer's Save Changes is refused when crm[] comes as one value, a field is left out or one is unknown, where each field so sent was cleared. The LTV pages' 18 sites leave phpstan-baseline.neon (28 pages and 66 sites to 22 and 48), so ForbidNumericCastOfRequestParamRule holds them; LtvPagesReadPostedValuesExactlyTest holds them (and LTV Settings) to no (string), (bool), empty(), strval() or boolval() of a request value: 36 sites on origin/1.9.79, 0 now. Tests: LtvPostedValueReadersTest (the readers); LtvPagesPostedIdsIntegrationTest runs each page as the account (page-runner) and posts every spelling on each merge, delete, edit and alias action; each of 17 planted casts and reads fails it. tests/live/ltv-pages-posted-ids.sh is the live pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Contributor
Contributor
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, changelogs.txt, tests/live/README.md, where both sides added lines: the incoming lines are kept as they are and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in ltv-webhooks-scheduled (this branch's stacked base), and through it 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, where both sides added lines: the incoming lines are kept as they are and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in ltv-webhook-revive (this branch's stacked base), and through it 1.9.79's new commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in ltv-delete-webhook-id (this branch's stacked base), and through it 1.9.79's new commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202
added a commit
that referenced
this pull request
Oct 29, 2026
…and every rows command returns its encoding error (#321) * 1.9.79: report losers and winners fail on a payout they cannot read, and every rows command returns its encoding error p202 report losers and report winners (and the MCP tools report_losers and report_winners) read the payout of --aff-campaign-id with `payout, _ = fetchCampaignPayout(...)`. A 5xx, a key or role the server refused, a campaign that does not exist or an unreachable server priced every click at a payout of 0: losers listed only the rows that never converted, winners listed rows whose CPC is past their break-even, and both exited 0 (CLAUDE.md #1). The read's error is the command's now, wrapped with %w so it keeps the API error's category, exit code and class-wide hint, and a 404 names `p202 campaign list`. Nothing is printed and the breakdown is not read. fetchCampaignPayout also read an answer with no aff_campaign_payout as 0, for every caller (report breakeven and campaign optimize too), and returned its JSON parse error uncategorized (exit 1, the generic --help hint). Both are a server error now (exit 4) naming `p202 campaign get <id> --json`. The column is DECIMAL NOT NULL: a payout of 0 is still 0, and a number (1.9.77 on) and a numeric string (before) read the same. rowsToJSON printed its json.Marshal error and returned nil, so a row JSON cannot carry left stdout empty and the command exited 0. It returns the error (server, exit 4, with a hint), and each of its ten call sites passes it up, through renderRows where the rows are the last thing printed; losers and winners encode before printing their notes, so a failure leaves stderr the error alone. The test fakes answered the payout as the old string ("0", "160") or not at all (masked_test); they answer it as the API does now, a number. TestEncodingErrorsReachTheCaller is the floor for the encoding shape: errcheck does not report a blank assignment (golangci-lint ran clean with `out, _ := rowsToJSON(...)` planted), so the test walks the CLI's source for an encoding call's error assigned to _ and for a function that makes one and returns no error, listing the 18 that remain with what each does on a failure. The fetchCampaignPayout entry leaves requestErrorDrops. Stacked on #310. Backlog: WHC2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr * 1.9.79: read the three request values 1.9.79's own merges left cast 1.9.79's PHPStan job has been red since the merges at 15:46Z. #308 extended ForbidNumericCastOfRequestParamRule to the pages and baselined the casts already there. Three PRs merged beside it each added a cast that its baseline does not count, and each PR passed on its own: - Personal settings (#283) cast the same-IP fallback after checking it is a key of its choices. It now stores 1 for '1' and 0 for '0', the two keys the check admits. - Setup > Offline Conversions (#293) cast a feed id after a regex test. It now reads it with PostedId::read(). - Setup > Traffic Sources (#271) cast the edited account's id to read its stored postbacks. The page's own ownership check compares the raw value in SQL, where MySQL reads '12abc' as 12. So a save from ?edit_ppc_account_id=12abc changed account 12 (one of the account's own, not the one named) and held that account's postbacks. Measured on a live instance: the old page saved the tampered edit onto account 1. The id is now read exactly, and anything else is refused in the page's words before anything is read or written. The baseline counts are unchanged: each file now holds only the casts they counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr --------- Co-authored-by: Claude <noreply@anthropic.com>
tracking202
added a commit
that referenced
this pull request
Oct 29, 2026
… split them, not by splittest (#327) * 1.9.79: a rule's destinations are checked by weight, as the redirects split them, not by splittest The rule check (RotatorsController::planRule(), used by PUT /rotators/{id}/rules, the MCP tool rotator_set_rules, and POST and PUT of one rule) refused a rule with two or more destinations and splittest 0, saying "splittest is 0, so the rule sends every visitor it matches to one destination". Neither redirect reads splittest: rtr.php and offrtr.php send a rule's one destination every visitor it matches, and split them between more by weight with getSplitTestValue(), the first taking them all when every weight is 0. Measured on a live instance, 24 clicks each through rtr.php and through offrtr.php: splittest 0 at 50/50 split 13/11 and 11/13, as splittest 1 did; 100/0 and 0/0 sent every click to the first destination under either value. So the refusal named behaviour that does not happen, a GET body holding such a rule could not be sent back to the set, such a rule could not be renamed through PUT of one rule, and a POST the 1.9.78 API took was refused. The check now asks every rule with more than one destination for a weight on each and at least one above 0, whatever splittest says, and says why in its message; a rule with one destination is taken whatever its weight, since the redirects send it every visitor. splittest is stored as sent: it is Setup › Redirectors' Split test switch. Setup › Redirectors showed a rule with more than one destination and splittest 0 with the switch off, which shows and posts only the first destination, so saving the redirector removed the others with nothing said (measured in a browser, on 1.9.79 and before). Such a rule now opens as a split test, each destination with its weight, and saves as one; the list names it a split test and shows the weights. RotatorRuleSplitIntegrationTest holds the check to the click path: it runs the real getSplitTestValue() from connect2.php over each shape of weights, expects a rule taken exactly when the redirects send its visitors where its weights say, the same with splittest 0 and 1 and through all three writes, and fails if rtr.php, offrtr.php, connect2.php or RuleOrder.php comes to read splittest or to choose a destination another way. RedirectorPageShowsSplitsIntegrationTest renders the page for such a rule. SyncRotatorRulesIntegrationTest syncs one, as a set and one rule at a time; the check made that sync fail. RotatorRuleSetInstanceTest clicks through rtr.php for a rule sent with splittest 0. Follows up #265, which merged without this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr * 1.9.79: read the three request values 1.9.79's own merges left cast 1.9.79's PHPStan job has been red since the merges at 15:46Z. #308 extended ForbidNumericCastOfRequestParamRule to the pages and baselined the casts already there. Three PRs merged beside it each added a cast that its baseline does not count, and each PR passed on its own: - Personal settings (#283) cast the same-IP fallback after checking it is a key of its choices. It now stores 1 for '1' and 0 for '0', the two keys the check admits. - Setup > Offline Conversions (#293) cast a feed id after a regex test. It now reads it with PostedId::read(). - Setup > Traffic Sources (#271) cast the edited account's id to read its stored postbacks. The page's own ownership check compares the raw value in SQL, where MySQL reads '12abc' as 12. So a save from ?edit_ppc_account_id=12abc changed account 12 (one of the account's own, not the one named) and held that account's postbacks. Measured on a live instance: the old page saved the tampered edit onto account 1. The id is now read exactly, and anything else is refused in the page's words before anything is read or written. The baseline counts are unchanged: each file now holds only the casts they counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tracking202 <306382+tracking202@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backlog: EZJ3
Stacked on #296 (
1.9.79-ltv-delete-webhook-id), which is stacked on #289. It uses #296'sProsper202\Http\PostedIdand its extension ofForbidNumericCastOfRequestParamRuleto the request superglobals. Merge it after both. The diff against1.9.79includes theirs until they land. This PR's own change is commit 89f4952 (one commit on top of #296's headba572614).What was wrong
Companies' Merge read
company_idandsource_company_idwith(int), so an id sent as12abcmerged company 12. That is one of the account's own companies, and not the one named (CLAUDE.md #4). The other Customer LTV pages read their ids the same way:customer_id, so Erase, Merge, Link Identity, Unlink and Save Changes all acted on customer 12. A link with?customer_id=12abcopened customer 12 too.source_customer_idandalias_id.exclude.offsetandedit.Measured live against
origin/1.9.79withtests/live/ltv-pages-posted-ids.sh(7 passed, 33 failed):source_company_id=2abcanswered "Companies merged." and company 2 was gone.source_customer_id=2abcmerged customer 2.customer_id=1abcwith Erase erased customer 1.product_id=1abcdeleted product 1.offset=50abcdeleted the company.What changed
Ids. Every id the LTV pages read from
$_POST/$_GETnow goes throughPostedId::read(), via new readers intracking202/ajax/ltv_helpers.php(p202_ltv_posted_id()and the others below).12abc,0,-1, empty, absent, a leading zero, a sign,.0, and a list.{"results": [], "error": "Pick the record to merge into from the list."}. Before, a bad request read as "no matches". The dialog now shows the server's sentence instead of "Search failed — try again". This was checked in Chromium (Playwright) by rewriting the dialog's own POST in flight.The pages' other values, read the way #296 read LTV Settings':
name[]=) is refused. Before,(string)stored a company name or an identity as "Array". This applies to:company;mode;action.crmcomes as one value, when one of its ten fields is left out, or when it carries an unknown key. The same goes foremailleft out andcfsent as one value. Before, the page read each of those as emptied and cleared the stored value. The form drawn again shows what was typed, and the stored value where nothing usable was.Machine checks.
Baseline. The LTV entries leave
phpstan-baseline.neon, so the rule now holds those pages:ltv_companiesltv_productsltv_customerltv_merge_searchltv_subscriptionssort_ltvThe baseline goes from 28 pages / 66 sites to 22 / 48, exactly the 18 fixed. PHPStan reported each cast when it was planted back (14 of 14
(int)plants).New:
LtvPagesReadPostedValuesExactlyTest. It holds every LTV page, LTV Settings included, to no(string),(bool),empty(),strval()orboolval()of a request value. It finds 36 such sites onorigin/1.9.79and 0 on this branch. The rest of the tree is not held (see findings).Tests
LtvPostedValueReadersTest(unit) covers the readers.LtvPagesPostedIdsIntegrationTest(@group integration, in 1.9.79: LTV Settings reads the row a button names exactly #296's style) runs each page as the account throughtests/Setup/fixtures/page-runner.php. For each id below it posts12abc-style,0,-1, empty, absent,N,N,N.0,0N,+Nand[N], and checks the sentence and that nothing changed. It then checks that the exact id still works.customer_idon erase, add_alias, delete_alias and save;alias_id;exclude.It also covers:
Plants used a fresh
mktempbackup, checked withcmpbefore planting and after restoring. Each plant was counted only when a named assertion failed:(string)of a list, theis_arrayfold ofcrm, and a left-out field read as emptied.tests/live/ltv-pages-posted-ids.shis the live pass, listed intests/live/README.md.Verification
verify.sh --since origin/1.9.79 --changed, on the committed branch (this includes #289's and #296's files):The unit run had 6143 tests and 5 skipped:
CronEntryPointsFailLoudlyTest, which skips when the checkout has a202-config.php, as this one did for the integration and live runs.Run by hand against MariaDB 10.11 on PHP 8.3.6:
LtvPagesPostedIdsIntegrationTestLtvSettingsPostedValuesIntegrationTest, re-run because LTV Settings loads the helpers fileorigin/1.9.79(915a367e)form-body.py), one field changedcrmsent as one valueWhat could not run, and how the live pass ran without the licence
install-instance.shcannot pass its licence step. Each instance was installed withinstall.php's own steps without the licence form:INSTALL::install_databases(),InstallAccount::create(), the202_api_keysrow and the partitions, as 1.9.79: LTV Settings reads the row a button names exactly #296's agent did. AutoCron and the daily email registration were left out.php -S(through the eval router) ran with every proxy variable pointed at the closed port127.0.0.1:9, soAUTH::is_valid_api_key()failed open. Each server log shows 25 licence checks, every oneFailed to connect to 127.0.0.1 port 9. Nothing reached the licence service.202-login.php's own form and seeds its companies, customers and product in the database directly. The REST API and Go CLI were not used.tests/live/upgrade-equals-install.sh(no schema change in this PR).For the reviewer
crm[...]field oremailis missing, orcrmhas an unknown key. The edit form always posts all ten fields andemail, so only a hand-built request is refused. Before, any such request cleared the fields it left out.offsetandedit, an empty value means none, asltv.jsdrops0and''from bookmarks and 1.9.79: LTV Settings reads the row a button names exactly #296'sshow_deliveriesdoes. An action's id that is empty is refused.FieldErrorPathsAreDottedTest.ltv_customer.phpis listed inALLOWEDfor its twocrm[<column>]sentences. These name the form's own field names (a form post, not an API body), asOverviewChart.phpalready is.ltv_settings.php(1.9.79: LTV Settings reads the row a button names exactly #296),MysqlCompanyRepository(4J3V), and role permissions (E29Z).Findings outside this item
(string)of a request superglobal is read at about 360 sites in 48 files outside the LTV pages. A tree-wide sibling ofForbidNumericCastOfRequestParamRule, with a by-count baseline, would hold them.ltv_settings.phpkeeps its ownltv_settings_posted()/ltv_settings_posted_id(). They could use the sharedp202_ltv_posted_*()once both PRs merge.tracking202/static/p13n_event.phpstays in the baseline:(float)of anis_numeric()value, not an id.Generated by Claude Code