Skip to content

1.9.79: both redirect links read a rule criterion's "all" as every visitor - #294

Merged
tracking202 merged 19 commits into
1.9.79from
1.9.79-rotator-all-spelling
Oct 28, 2026
Merged

tracking202 merged 19 commits into
1.9.79from
1.9.79-rotator-all-spelling

Conversation

@tracking202

@tracking202 tracking202 commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #265 (1.9.79-mcp-rotator-crud, merged in here with a merge commit). Merge #265 first; until then this PR's diff includes #265's.

Sibling of #290 (JF3K), which is also stacked on #265. The two merge cleanly in code. They conflict only where both append: changelogs.txt, 202-config/changelogs.json, and one block each in tests/live/setup-pages.sh after the same anchor. Keep both sides in each. I checked this on a local merge of the two branches:

Backlog: 3C1D

What changed and why

rtr.php read a criterion value of ALL or all as every visitor; offrtr.php read only ALL. So a rule stored as all sent a visitor to the rule from a redirector link and to the default from a landing page's offer link. Two writers stored all:

  • the API before 1.9.79, which stored values as sent;
  • Setup › Redirectors' save, which stores what is typed.

#265 makes the API store ALL, but rows already stored keep all.

Measured before the fix on a live instance: one redirector per stored spelling, one rule to /rule, default /default, and a Chrome click through each entry point.

stored rtr.php offrtr.php
is ALL rule rule
is all rule default
is All default default
is " all" default default
is "United States(US), all" default default
is_not all rule rule

Where the other spellings came from. The history here goes back to 1.9.29, where rtr.php gained || in_array('all', $values). The first v3 rule write (fc26a2a7) stored (string) ($c['value'] ?? ''): any spelling and any spacing, and '' for a criterion sent without a value. p202 rotator test (go-cli/cmd/verify.go) predicted EqualFold(TrimSpace(value), "ALL"), which is a third reading: it called All every visitor, which neither script did, and missed United States(US), all, which rtr.php would have read had the space not been there.

One reading. Prosper202\Rotator\Criterion reads a stored value as follows:

  • split on commas, each value trimmed, the empty ones dropped;
  • ALL in any case, alone or among other values, matches every visitor, whatever the statement. is not ALL already matched everyone on both scripts, and still does.

Its readers and writers:

Who Change
rtr.php, offrtr.php read the value through Criterion::values() and matchesEveryone(). Nothing else in the click path changed: same reads, same in_array comparisons per type.
API (RotatorsController::criterion()) stores Criterion::normalize(). All and ALL now become ALL too, not only all.
Setup › Redirectors' save (ajax/rotator.php) stores Criterion::normalize(); it stored all as typed.
Setup › Redirectors' list, Overview › Rotator Breakdown (sort_rotator.php) list such a criterion as "any visitor", through Criterion::describe(). The Breakdown's value is no longer in bold.
p202 rotator test (verify.go) predicts what the links do, through the Go twins in go-cli/cmd/rotator_criterion.go.
p202 diff (diff.go) no longer reports all against ALL, or a list with and without spaces, as a difference between two installs.
GET still answers values as stored.

Rows already stored (#25). The reader takes every spelling a writer stored, and nothing in the database is rewritten. A rule is rewritten in canonical form only when it is next saved, by the page or the API.

What this changes for stored rows.

"None". I found no writer that stored a "none" criterion value. The nearest is '', an empty value from the old API. It is now an empty list, so is matches no visitor and is_not every visitor, as before, with one exception: an ISP criterion is "" matched every visitor while MaxMind ISP lookups were off (in_array(null, ['']) is true). It now matches none.

ClickPathReads (#27). The criteria read in both scripts is unchanged. It is a read by rule id, from the redirector's own active rules (RuleOrder::activeRules), and no read or join was added.

Docs. documentation/api/12-rotators.md (the criterion value row), docs/openapi.yaml (the criterion value, and setRotatorRules), and both changelogs.

Tests

  • tests/Rotator/CriterionTest (unit). Every spelling in tests/fixtures/rotator-criterion-values.json is checked for its values, its every-visitor reading and its normalized form, and normalizing twice changes nothing. That fixture lists the 13 spellings writers stored, each with its writer.
    • It also checks describe().
    • Structural: every served file that names 202_rotator_rules_criteria is listed, so a new reader fails until it is listed. Each reader must call Criterion:: and must contain no string literal ALL in any case; offrtr.php's own 'ALL' is how the two scripts came to differ.
  • Go:
    • TestCriterionValuesReadAsThePHPTreeReadsThem reads the same fixture through repoPath(); the fixture is added to go-cli.yml's paths, both lists.
    • TestRotatorDestinationReadsEverySpellingOfAll.
    • TestDiffComparesCriteriaAsTheRedirectsReadThem.
    • TestOnlyTheCriterionReaderSpellsAll, an AST scan that refuses a literal ALL/All outside rotator_criterion.go. Lower-case "all" names the --all flags and is not looked for.
  • tests/live/rotator-criterion-spellings.sh (new). One redirector per stored spelling, written into the row as an older writer left it. A click goes through rtr.php and one through offrtr.php for each, and every spelling sends the visitor to the rule; a value that only resembles ALL (Mall) does not.
  • tests/live/setup-pages.sh: the page saves a criterion typed all as ALL and lists the rule as "If any visitor".
  • Updated:
    • RotatorRuleSetIntegrationTest::testValuesAreStoredAsThePageStoresThem now also covers All and Chrome, aLL;
    • IpCriterionTest, which pins the page's write statement, now pins the new one.

Plants: 11, each caught by a named test. Backups were mktemp copies checked with cmp, the tree was restored, and the run was repeated on the final tree.

  1. rtr.php with its own ALL/all check again. Caught by CriterionTest and the spellings pass (All, all, the list).
  2. offrtr.php with its own ALL check again. Same tests, all included.
  3. Criterion case-sensitive.
  4. Criterion not trimming.
  5. The API storing only lower-case all as ALL. Caught by the integration test and CriterionTest.
  6. The page storing as typed (setup-pages.sh).
  7. The page's list as stored (setup-pages.sh and CriterionTest).
  8. The Breakdown as stored.
  9. rotator test back to its whole-value EqualFold.
  10. diff comparing raw values.
  11. The Go reader not trimming.

Live pass

Instance. A local MariaDB 10.11 / PHP 8.3.6 instance on this branch, port 8291. It was installed by running install.php's own steps without the licence form; the licence stub is down.

The licence check was made unreachable. For the signed-in pages, the server's proxy points at a closed port, which AUTH::is_valid_api_key() treats as a network failure and lets through.

What could not run here. The Go CLI answers 402, so p202 rotator test and p202 diff ran only in Go unit tests.

Results.

  • tests/live/rotator-criterion-spellings.sh: 21 passed, 0 failed. Every stored spelling sends the visitor to the rule through both rtr.php and offrtr.php; Mall goes to the default on both.
  • Overview › Rotator Breakdown, read as the page draws it: the six spellings' rules are listed "If any visitor", Mall as "If Country is Mall", and the browser rules as before.
  • tests/live/setup-pages.sh: 331 passed, 0 failed (1.9.79: redirector create, update, delete and whole-rule-set writes in the MCP connector #265 reported 328).
  • tests/live/offer-rotator-routing.sh: 30/0.
  • tests/live/rotator-breakdown.sh: 10/0.
    • One run failed first, with a "Deleted rule" counting 2 old clicks. setup-pages.sh TRUNCATEs the rotator tables and leaves 202_clicks_rotator, so a reused instance hands new redirectors ids that old clicks name.
    • It passed once AUTO_INCREMENT was moved past them (see "Found outside").

Verification

.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed, run on the final tree before the commit (everything staged), PHP 8.3.6 with the full vendor/, schema tier on a scratch MariaDB 10.11 database. The unit tier ran 6119 tests, 4 skipped (the documented "User class requires DB singleton" ones). The live instance's 202-config.php was moved out of the checkout for the run.

Scope report
------------
  actionlint SKIP  actionlint not installed (brew install actionlint, or the download script CI uses)
  go         PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.24)
  golangci   PASS  ran under CI's go1.25.14 via GOTOOLCHAIN (local go is 1.24)
  openapi    PASS
  patterns   PASS
  phpcs      PASS
  phpstan    PASS
  schema     PASS
  syntax     PASS
  unit       PASS

Not covered by this script:
  live      stand up an instance, drive the path a user would take
  agent     add and run a case under tests/fixtures/agent-eval/cases/
  ci        a workflow still in_progress has not passed; read the run

Report the lines above verbatim. Do not write "tests pass".

Then actionlint v1.7.7 was built into a scratch directory (go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7), and verify.sh --tier actionlint was run with it on PATH: actionlint PASS.

CI status: not yet observed.

What could not run here

  • The Go CLI against the instance. It gets 402 without the licence stub. rotator test and diff ran only in Go unit tests.
  • CI's MySQL 8.0. Not run here: this sandbox has MariaDB.
  • Agent evals. None was added; the change is in the click path and how two CLI commands read a value.

For the reviewer to check

  1. ALL in any case, and values trimmed. This moves traffic for stored rows that were not ALL/all. A rule stored as All, all or United States(US), all matched no visitor on either link and now matches every visitor on both. A stored United States(US), Canada(CA) now matches Canada.
    • Why: in each case the stored value says what the rule was meant to do; p202 rotator test already told users All meant every visitor; and the writers now store these spellings as ALL, so reading them as anything else would make a saved-again rule change meaning.
    • If only all should change, make Criterion::isEveryone() compare ALL and all exactly, and drop the trimming in values(). The fixture's cases say which rows move.
  2. is not ALL stays "every visitor". That is what both scripts did, and the docs now say it.
  3. The page now normalizes on save (all → ALL), so the editor shows ALL after a save. It already trimmed in its JavaScript.
  4. The Rotator Breakdown's criterion line is now plain text through Criterion::describe(), where the value was in <strong>.

Review follow-up: ALL on an IP criterion (9I0R, 3de05fa)

This answers Codex's P2 on RotatorsController.php ("Allow ALL in IP criteria"). It is the defect this PR first listed under "Found outside".

Both links read a criterion holding ALL as every visitor, whatever its type. The writers, though, checked an IP value with IpCriterion::refusal() first, which refused ALL as "not an IP address". So the following could not be written, nor could a stored IP=ALL rule be sent back as GET answered it:

  • the set PUT;
  • the single-rule POST and PUT, which share planRule() → ruleParts() → criterion();
  • the Redirectors page.

The changes:

  • API. criterion() returns a value holding ALL before any type's own check is made. It reads the value with Criterion::values() and matchesEveryone(), the same reading the redirects use, so all, All, 203.0.113.7, ALL and US, ALL are taken on any type and stored with ALL as ALL. A country or device list beside ALL is no longer refused for its form either.
  • Page. Its check and write go through the new Criterion::refusal() and Criterion::stored().
  • Test. RotatorRuleSetIntegrationTest::testAValueHoldingAllIsTakenWhateverTheType stores an IP=ALL rule, reads it with GET and sends it back through the set PUT (answered unchanged, rows untouched) and the single-rule PUT. Before the fix it failed with Invalid rules.0.criteria.0.value. CriterionTest holds refusal() and stored() to the same spellings.
  • Plants: 3, each caught by a named test.
    • API short-circuit removed: caught by the integration test.
    • Criterion::refusal() ALL handling removed: caught by CriterionTest and the page pass.
    • Criterion::stored() ALL handling removed: caught by CriterionTest.
  • Live.
    • setup-pages.sh 333/0; it posts an IP criterion of all and checks that ip|ALL is stored.
    • Spellings pass 24/0, including an IP rule stored as All on both links.
    • offer-rotator-routing.sh 30/0 and rotator-breakdown.sh 10/0.
    • Over HTTP, a stored IP All rule went through GET and then PUT twice, answering 200 both times.
  • Ladder on the final tree: every tier PASS except actionlint SKIP (no workflow changed). The unit tier ran 6127 tests, 4 skipped.
  • 1.9.79: a redirector rule written on its own needs a criterion, as the page and the set ask #290. It needs no commit of its own: its single-rule writes reach criterion() through planRule(), and a trial merge of the two branches has the fix on both paths. The conflicts are still only the two changelogs and setup-pages.sh.

Found outside this item (one line each)

  • rtr.php compares an ISP criterion with the first comma-part of getIspData(), offrtr.php with the whole answer, so an ISP rule can match on one entry point and not the other.
  • p202 rotator test reads a bare country code (US) as that country, while the redirects never match one (countryCodeFromValue).
  • tests/live/setup-pages.sh TRUNCATEs the rotator tables but not 202_clicks_rotator, so on a reused instance new redirectors get ids old clicks name, and tests/live/rotator-breakdown.sh then counts those clicks.

Generated by Claude Code

claude added 16 commits October 23, 2026 19:54
…or tools

PUT /rotators/{id}/rules replaces a redirector's rule set the way Setup ›
Redirector saves it, in one transaction: every rule the redirector is to
have, in the order the redirects try them. A rule or entry sent with its id
keeps it (its clicks and report rows stay its own), one without is added,
one left out is removed (remove_rotator_rule, as the page asks). What the
page refuses is refused (a rule with no criterion or destination, two
destinations without a split test, a split test without weights), every
problem named by its place in one 422 with nothing written; another
account's campaign or another redirector's rule is refused by name.
?dry_run=1 answers the set as it would be stored and each rule's change;
the route is stageable; PATCH is a 405. A GET body sent back changes
nothing.

The rules had no order but their ids, so a rule could only be added after
the others: 202_rotator_rules gains position (the 1.9.79 rung), read by
rtr.php and offrtr.php through Prosper202\Rotator\RuleOrder (in id order on
a database the upgrade has not reached, rather than failing the click), the
Setup page, the API and both rotator reports; the page's save and POST
.../rules write it. The API's rule writers lock the redirector's row.

A criterion's values are stored as the page stores them (split, trimmed,
`all` as ALL), and a device in capitals, which never matches, is refused.

p202 rotator set-rules <id> (--rules or --file, --dry-run) sends a set; the
MCP connector gains rotator_create, rotator_update, rotator_delete and
rotator_set_rules, whose rules argument is the set as JSON with a schema
(a tool argument can now be a JSON document). Hosted AI keys may write
redirectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
With the rotator tools the list is 84,564 characters; with #260's text-ad
tools beside them, about 87,900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… them

The merged tree's unit tier read three of the new statements as hidden
from it: the criteria and destinations delete named its table through a
variable, the click path's rule read was built in a class the cached-read
scan does not follow, and two rule reads put a method call after their
WHERE. The delete names each table literally, RuleOrder makes the click
path's cached read itself, and the ORDER BY goes through a variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… back

PUT /rotators/{id}/rules?dry_run=1 built each rule's criteria and
redirects in the order the body listed them, while the write updates kept
entries in place and GET reads them back by id, so a body that reordered a
rule's entries (keeping their ids) previewed one order and wrote another.
The nested order decides nothing on the click path: every criterion has to
match, and a destination is drawn by weight. So the plan now holds each
rule's entries as the write will read them back (the kept ones by id, the
new ones after them as sent), and the preview and the write answer the
same data. The rules keep the order sent. The API reference and the
OpenAPI description say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…rule

PUT /rotators/{id}/rules/{ruleId} read and checked the rule, then took the
redirector's lock, so a rule set that removed the rule could commit in
between: the update then matched no row and its criteria and redirects
were inserted for a rule that no longer existed (these tables have no
foreign keys), answered as a success. DELETE /rotators/{id}/rules/{ruleId}
checked the rule the same way and answered a removed rule's delete as done.

Both now take the redirector's lock, then the rule's own (a locking read,
which also holds off Setup › Redirectors' save, which takes neither), before
the first read of the transaction, and read the rule under them. A rule gone
by then is a 404, with nothing written; an update that matches no row is
checked by a current read and refused the same way. The redirector delete
no longer reads the redirector before its lock.

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
…a rule at once

A rule write read its answer back after its transaction committed, when
the redirector's lock was already let go: a writer waiting on that lock
could replace the set in between, and PUT /rotators/{id}/rules answered the
second writer's rules beside its own changes (POST and PUT of one rule
answered the second writer's set too). The read-back now runs inside the
transaction, under the locks, and is what the write answers.

A rule set's 422 promised every problem at once, and gave one per rule:
the name, the flags and each criterion and redirect threw at the first
problem. Every check of a rule now runs and each problem is answered by
its place (rules.0.criteria.1.value), several in one entry included; the
single-rule writes report every entry's problems the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
The catalogue keeps #280's traffic source, account and category updates and
deletes beside the rotator writes, and rotator_list takes #279's hosted
omission of --all. Both changelogs keep the version branch's lines and add
this branch's. The tool list is 89,469 characters with both; its budget is
90,000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…or_get answers

#281 holds a hosted call's arguments to 1,000 JSON values, because each
item of a list argument becomes a flag of its own. rotator_set_rules takes
its rules as one JSON document, written to its command as one flag, and has
no per-rule tool to fall back on: at 1,000 values a hosted call could send
back about 30 rules of one criterion and two destinations, where rotator_get
answers about 100 under the result bound. A tool that takes a JSON document
is held to 5,000 values instead; at that bound the call allocated 5.5 MB, the
list arguments at theirs 5.1. Past it the refusal says what to leave out and
which command sends a larger set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
"set the api url" ranked p202 system integrations level with config
set-url once the rotator writes and #280's tasks were both in the index:
the two had scored within half a percent on the version branch, and the new
commands' words moved the weights enough to tie them. config set-url now
has a search task with the words the docs use for what it sets, the
install's base URL or instance URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…B limit

--file read the whole file with os.ReadFile before checking it against
the API's 1 MB body limit, so a path naming something huge, or endless
like /dev/zero, was read into memory before the size refusal could run.
A path is now read through the same rotatorMaxRuleSetBytes+1 limit
stdin already was, and anything past it is refused by the size check.

TestRotatorSetRulesReadsAFileOnlyUpToTheLimit sends --file /dev/zero and
expects the 1 MB refusal with no request; with os.ReadFile put back it
dies with "fatal error: out of memory" under a 3 GB address-space limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…or-all-spelling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…sitor

rtr.php read a criterion value of ALL or all as every visitor; offrtr.php
read only ALL. A rule stored as "all" (the API stored values as sent before
1.9.79, and Setup > Redirectors stores what is typed) sent a visitor to
the rule from a redirector link and to the default from a landing page's
offer link (measured live on both scripts). Neither read "All", " all " or
"United States(US), all" (the space kept on the value) as every visitor,
while p202 rotator test predicted "All" did.

Prosper202\Rotator\Criterion is now the one reading: the value split on
commas, each value trimmed, the empty ones dropped, and ALL in any case,
alone or among other values, matching every visitor whatever the
statement (is not ALL did on both scripts, and still does). Both scripts
call it; so do the API's writes and the Redirectors page's save, which
store ALL; the Redirectors page and Overview > Rotator Breakdown list such
a criterion as "any visitor". go-cli has the same reading for p202 rotator
test and p202 diff, and both trees hold to
tests/fixtures/rotator-criterion-values.json, the spellings writers stored.

CriterionTest holds every served file that names the criteria table to a
list, and each reader to calling Criterion and spelling ALL nowhere;
TestOnlyTheCriterionReaderSpellsAll does the same for the CLI.
tests/live/rotator-criterion-spellings.sh clicks both scripts for each
stored spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Copilot AI balanced review requested due to automatic review settings October 28, 2026 07:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T12:31:12.114376Z 4904aee New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Code review: FINDINGS

For 8dab59b: 0 issue(s). The review is encrypted for the maintainers in the review-findings artifact of this run.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8dab59b923

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/v3/Controllers/RotatorsController.php
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Adversarial review: NON-BLOCKING

For 4904aee: 4 finding(s). The report is encrypted for the maintainers in the review-findings artifact of this run.

Rewritten on every push; the check fails on BLOCKING.

Both redirect scripts read a criterion holding ALL as every visitor
whatever its type (Criterion::matchesEveryone()), and the writers held an
IP value to IpCriterion::refusal() first, which refused ALL as "not an IP
address". A redirector with an IP criterion of ALL could not have its
rule set sent back as GET answered it: the set PUT, and the single-rule
POST and PUT that share planRule(), answered 422 for a rule the links
read as valid; and the Redirectors page refused the same value.

RotatorsController::criterion() now returns a value holding ALL (read by
Criterion::values(), so " all ", "All" and "US, ALL" as the redirects read
them) before any type's own form is asked of it, so a country or device
list beside ALL is not refused for its form either. The page's check and
write go through Criterion::refusal() and Criterion::stored(), which hold
the same reading for the page.

RotatorRuleSetIntegrationTest stores an IP=ALL rule, reads it with GET
and sends it back through the set PUT (unchanged) and the single-rule PUT;
it failed before the fix with "Invalid rules.0.criteria.0.value".
CriterionTest holds refusal() and stored() to the same spellings, and the
live passes post an IP criterion of all to the page and click an IP rule
stored as " All ".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, 202-config/functions-upgrade.php, changelogs.txt, 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 1.9.79-mcp-rotator-crud (this branch's stacked base), and
through it 1.9.79's new commits. The Go workflow's path lists conflicted
where both sides added a file a Go test reads: both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
@tracking202
tracking202 merged commit e802d6e into 1.9.79 Oct 28, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants