Skip to content

test(schema): replay the updater from every version a server can hold - #1342

Merged
mastacontrola merged 1 commit into
working-1.6from
test-upgrade-replay-gate
Aug 24, 2026
Merged

test(schema): replay the updater from every version a server can hold#1342
mastacontrola merged 1 commit into
working-1.6from
test-upgrade-replay-gate

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

What this closes

CI has only ever exercised a fresh install of the schema, and that is not a gap anyone chose — it is what the code makes easy. SchemaUpdaterPage::update() slices its step array from self::$mySchema, which is 0 on a new database, so a fresh install really does run every step from the beginning. tests/schema-executes.test.php therefore sets $mySchema = 0 and FOG_SCHEMA = PHP_INT_MAX, which neutralises the two expressions an upgrade turns on:

$hasIndexed = count($this->schema) > self::$mySchema;
$items      = $hasIndexed ? array_slice($this->schema, self::$mySchema, null, true) : [];
...
$newSchema->set('version', $version + 1);

So the SQL side of an upgrade was already covered — a server at version N runs steps [0,N) then [N,end), which is the same statements in the same order a fresh install runs, and schema-executes runs exactly that against three database engines. What was never covered is the arithmetic and the version it leaves behind.

That is what #1338 broke. Seven statements appended inside the previous step's array left the element count unmoved while FOG_SCHEMA went 360 → 367. Every existing server was then pinned on ?node=schema — the updater had nothing to apply, never advanced the stored version, and answered 204, which jQuery reports as statusText: "nocontent" and $.notifyFromAPI renders as a looping Generic Error toast. All seven CI checks were green, because a fresh install was correct throughout.

What is added

tests/schema-upgrade-replay.test.php builds the real step array and, for every version a server could be sitting on, runs the updater's own expressions over it and requires the stored version to land on FOG_SCHEMA. It also pins the two properties that make that arithmetic mean anything:

  • count($this->schema) === FOG_SCHEMA — the real element count, not a proxy
  • keys are a contiguous list from zero — the stored version is written as $version + 1 from the array key

No CI change is needed. run-all.sh globs tests/*.test.php, so it runs in the existing blocking tests (PHP 7.4) / tests (PHP 8.3) jobs. It needs no database on purpose: the upgrade-specific surface is arithmetic, so it blocks rather than riding the schema job where a failure would be one more tolerated leg.

Mutation-verified, three directions

Restored from copies rather than git checkout --.

Mutant Result
#1338 reproduced exactly — the seven steps merged back into step 360, FOG_SCHEMA left at 367 FAIL: FOG_SCHEMA is 367 but commons/schema.php holds 360 step(s), plus all 367 starting versions reported stuck
FOG_SCHEMA lowered below the count the stranded-step failure
$this->schema[400] = [...] index assignment the contiguity failure — which the count check alone does not see

Suite: 145 passed, 0 failed. Verified on PHP 8.3 locally and PHP 7.4 in a container (identical 367-step result), and schema-executes re-verified against MariaDB 11.8 with byte-identical output before and after the refactor.

Refactor

The shim that loads commons/schema.php outside the application moves to tests/lib/fog-schema-collector.php. It was carried by schema-executes alone; there are two callers now and there must stay one copy, because two hand-kept descriptions of the schema drifting apart is the failure both tests exist to catch.

schema-gate is kept, not replaced

The two fail on different things and neither subsumes the other. A real count is label-independent and catches shapes the text cannot see; the label checks cover hygiene a count cannot — an unlabelled append, or a label with no append. schema-gate's docblock recorded that a real count "was tried and rejected" because schema.php wants ~35 constants and a couple of core classes; the collector discovers both rather than listing them, so that objection is answered rather than overruled, and the docblock now says so.

Also repoints a reference in commons/schema.php that #1340 left dangling at a test file that was never committed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XqpPXAk7bEm8huH9WkiGk6

CI has only ever exercised a FRESH INSTALL of the schema. That is not a gap
anyone chose; it is what the code makes easy. SchemaUpdaterPage::update()
slices its step array from self::$mySchema, which is 0 on a new database, so
a fresh install really does run every step from the beginning -- and
tests/schema-executes.test.php sets $mySchema to 0 and FOG_SCHEMA to
PHP_INT_MAX, which neutralises the two expressions an upgrade turns on.

So the SQL side of an upgrade was already covered: a server at version N runs
steps [0,N) then [N,end), the same statements in the same order a fresh
install runs. What was not covered is the arithmetic and the version it
leaves behind, and that is what #1338 broke -- seven statements appended
inside the previous step's array left the element count unmoved while
FOG_SCHEMA went 360 -> 367, so every existing server sat on ?node=schema
permanently, looping a Generic Error toast, while all seven CI checks stayed
green because fresh installs were correct throughout.

tests/schema-upgrade-replay.test.php closes it. It builds the real step array
and, for every version a server could be sitting on, runs the updater's own
expressions over it and requires the stored version to land on FOG_SCHEMA. It
also pins the two properties that make that arithmetic mean anything:
count($this->schema) == FOG_SCHEMA, and keys that are a contiguous list from
zero -- the stored version is written as $version + 1 from the array KEY.

Needs no database, so it runs on the plain PHP matrix and BLOCKS, rather than
riding the schema job where a failure would be one more tolerated leg. No CI
change is needed at all: run-all.sh globs tests/*.test.php.

Mutation-verified in three directions, restoring from copies rather than
git checkout:

  - #1338 reproduced exactly (the seven steps merged back into step 360,
    FOG_SCHEMA left at 367) -> "FOG_SCHEMA is 367 but commons/schema.php
    holds 360 step(s)", plus all 367 starting versions reported stuck.
  - FOG_SCHEMA lowered below the count -> the stranded-step failure.
  - a $this->schema[400] = index assignment -> the contiguity failure,
    which the count check alone does not see.

Extracts the shim that loads commons/schema.php outside the application into
tests/lib/fog-schema-collector.php. It was carried by schema-executes alone;
there are two callers now and there must stay one copy, because two
hand-kept descriptions of the schema drifting apart is the failure both
tests exist to catch. schema-executes' behaviour is unchanged -- byte-
identical output before and after, verified against MariaDB 11.8.

schema-gate keeps counting `// N` labels and is not replaced. The two fail on
different things: a real count is label-independent and catches shapes the
text cannot see, while the label checks cover hygiene a count cannot -- an
unlabelled append, or a label with no append. Its docblock recorded that a
real count "was tried and rejected" because schema.php wants ~35 constants
and a couple of core classes; the collector discovers both rather than
listing them, so that objection is answered rather than overruled, and the
docblock now says so.

Also repoints a reference in commons/schema.php that #1340 left dangling at a
test file that was never committed.

Co-Authored-By: Claude <noreply@anthropic.com>
@mastacontrola
mastacontrola merged commit afaa0b9 into working-1.6 Aug 24, 2026
7 checks passed
@mastacontrola
mastacontrola deleted the test-upgrade-replay-gate branch August 24, 2026 10:53
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.

1 participant