From df8cbfb8e4a5024f5175b72de2feefefe1f757b4 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:41:12 +0000 Subject: [PATCH] Freeze producer legacy command-key qualification fixture --- .github/workflows/phpunit-feature.yml | 3 + .github/workflows/server-parity.yml | 1 + .../command-key-legacy-v1-vectors.json | 172 ++++++++++++++++++ scripts/ci/test_command_key_legacy_fixture.py | 66 +++++++ tests/Unit/ServerParityAggregateTest.mjs | 1 + 5 files changed, 243 insertions(+) create mode 100644 resources/platform-protocol-specs/command-key-legacy-v1-vectors.json create mode 100644 scripts/ci/test_command_key_legacy_fixture.py diff --git a/.github/workflows/phpunit-feature.yml b/.github/workflows/phpunit-feature.yml index c071739d1..8b1e7181b 100644 --- a/.github/workflows/phpunit-feature.yml +++ b/.github/workflows/phpunit-feature.yml @@ -186,6 +186,9 @@ jobs: python3 -m unittest tests/Unit/Support/php_apt_test.py python3 scripts/ci/verify-php-apt-source.py + - name: Check the shared legacy command-key seed and authority scopes + run: python3 scripts/ci/test_command_key_legacy_fixture.py + - name: Check published failover image identity and recovery result gates run: python3 -m unittest discover -s tests/Unit/Support -p 'single_region_failover*test.py' diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 5e7a75b71..02378785e 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -28,6 +28,7 @@ on: - config/dw-contract.php - resources/platform-protocol-specs/carried-poll-v1-vectors.json - resources/platform-protocol-specs/workflow-task-completion-limits-v1-vectors.json + - resources/platform-protocol-specs/command-key-legacy-v1-vectors.json - tests/Fixtures/ActivityMetadataProjection/** - tests/Fixtures/CodecRegression/** - tests/Fixtures/CommandIntent/** diff --git a/resources/platform-protocol-specs/command-key-legacy-v1-vectors.json b/resources/platform-protocol-specs/command-key-legacy-v1-vectors.json new file mode 100644 index 000000000..8a3619209 --- /dev/null +++ b/resources/platform-protocol-specs/command-key-legacy-v1-vectors.json @@ -0,0 +1,172 @@ +{ + "schema": "durable-workflow.command-key-legacy-fixture.v1", + "reference_producer": { + "tag": "3.9.1", + "source_commit": "7d7ad9d6c9aee2e4592d74389ba9efd36e741f44", + "tests": "rust/tests/execution/command_keys.rs", + "scope": "Source control, not an installed-client receipt or a reusable database dump." + }, + "synthetic_legacy_shape": { + "backend": "sqlite", + "source_helper": "legacy_key", + "scope": "Transform a stopped, disposable copy of real current-producer API admissions. This is not a physical pre-upgrade rehearsal.", + "preconditions": [ + "Database belongs to the test cohort; no production input.", + "Server is stopped; use a disposable copy.", + "Selected command_id, command_type, workflow_instance_id and workflow_run_id equal the original API receipt.", + "Stored request_id equals the original caller key token and exactly one dw_command_intents row belongs to command_id." + ], + "transaction": "BEGIN IMMEDIATE; verify preconditions before mutations; require one affected row from each statement; COMMIT only if all succeed, otherwise ROLLBACK.", + "statements": [ + "DELETE FROM dw_command_intents WHERE command_id=:command_id", + "UPDATE workflow_commands SET request_id=:legacy_request_id WHERE id=:command_id" + ], + "preserve": "Every other command column, related update/signal, task, history and payload-registry row remains byte-for-byte unchanged." + }, + "keys": { + "original": "token-key", + "second": "second-key", + "case_exact": "CaseSensitive", + "case_other": "casesensitive", + "tokens": { + "signal": "signal-v1:7fd98d71570942fe2b98b2888d3d879c32e8130a904115032291500e856db4ba", + "update": "update-v1:7fd98d71570942fe2b98b2888d3d879c32e8130a904115032291500e856db4ba" + } + }, + "legacy_readback": { + "types": [ + "signal", + "update" + ], + "states": [ + "open_after_restart", + "completed_after_restart", + "continued_after_restart" + ], + "original_input": 1, + "retry_input": 999, + "concurrency": 4, + "expected": { + "retained": true, + "intent_verified": false, + "same_fields": [ + "command_id", + "run_id" + ], + "update_same_fields": [ + "update_id", + "accepted_at" + ], + "writes": 0, + "success_statuses": { + "signal": [ + 202 + ], + "update": [ + 200, + 202 + ] + } + }, + "update_close_setup": [ + "complete_update(original_update_id, null)", + "complete_workflow(null) or continue_as_new" + ], + "closed_update_expected": { + "update_status": "completed", + "result": null + }, + "explicit_original_run": "Read back the original receipt, including after continuation; do not claim the successor was targeted.", + "changed_name_or_explicit_run": { + "status": 409, + "reason": "request_id_conflict", + "original_id_field": "retained_command_id", + "writes": 0 + } + }, + "ambiguity": [ + { + "shape": "two-rows", + "setup": "Admit original under token-key. Admit a second command under second-key; transform only that second command to raw token-key.", + "retry_key": "original" + }, + { + "shape": "legacy-token", + "setup": "Admit under token-key; transform that command to raw keys.tokens[type], removing its intent.", + "retry_key": "original" + }, + { + "shape": "modern-raw", + "setup": "Keep the original modern admission under token-key unchanged.", + "retry_key": "tokens[type]" + } + ], + "ambiguity_expected": { + "status": 409, + "reason": "request_id_ambiguous", + "retained": true, + "intent_verified": false, + "retryable": false, + "writes": 0, + "must_not_select_receipt": true + }, + "case_only": { + "setup": "Admit under CaseSensitive, then transform that command to raw CaseSensitive. Exact-byte retry reads back unverified.", + "retry_key": "casesensitive", + "mysql_mariadb_default_ci": { + "status": 409, + "reason": "request_id_ambiguous", + "retryable": false, + "writes": 0 + }, + "sqlite_postgresql_binary": { + "status": 202, + "identity": "new command; distinct key", + "zero_write_claim": false + } + }, + "physical_upgrade": { + "scope": "Separate actual old-image admission and schema-upgrade cell. Do not substitute the synthetic transform or minimal orphan command-row schema tests.", + "baseline": { + "tag": "3.8.0", + "source_commit": "28f084513235f6647e740c4b3d9e3aace7a323b7", + "index_digest": "sha256:7988a23525d9e50e997a9f6f02e801300ed4f626de3cf6977033657ae7e9f58e" + }, + "schemas": { + "sqlite": [ + 12, + 13 + ], + "postgresql": [ + 13, + 14 + ], + "mariadb": [ + 13, + 14 + ], + "mysql": [ + 14, + 15 + ] + }, + "procedure": [ + "Admit real signal/update receipts through baseline public APIs; record exact keys, command/run/update identities and rows/history. Use the baseline advertised worker protocol; seed generation is not a current-SDK acceptance claim.", + "Stop all baseline Servers, keep the database and backup, and record the baseline image/source and database fingerprints.", + "Run the exact candidate image schema-upgrade once; require the declared schema transition and an empty dw_command_intents table.", + "Prove original raw request_id values and command/history rows were preserved, then start the exact candidate image.", + "Perform legacy readbacks and name/target conflicts; compare full rows/history and original identities. Keep synthetic ambiguity cells separate unless their seed was genuinely created through the baseline API." + ], + "no_inference": "The 3.9.1 source tests prove all-backend row preservation separately. They do not prove this installed upgrade cell for an SDK." + }, + "postcommit_response_loss": { + "scope": "Separate real admission cell. A proxy must establish that the Server committed before dropping the response; then retry the original key. Synthetic legacy setup does not establish response-loss evidence." + }, + "receipt_requirements": [ + "Exact fixture Git commit/blob and digest", + "Actual SDK/CLI package and Server source/index/platform identities", + "Synthetic-shape versus physical-upgrade versus postcommit-loss cell label", + "Original and returned command/run/update identities and complete zero-write snapshots where required", + "Nonpass and cleanup outcomes; no inferred PASS from source lineage" + ] +} diff --git a/scripts/ci/test_command_key_legacy_fixture.py b/scripts/ci/test_command_key_legacy_fixture.py new file mode 100644 index 000000000..b6726f4e9 --- /dev/null +++ b/scripts/ci/test_command_key_legacy_fixture.py @@ -0,0 +1,66 @@ +"""Check the shared producer seed, not native catalog/upgrade acceptance.""" +import hashlib +import json +from pathlib import Path +import sqlite3 +import unittest + +ROOT = Path(__file__).resolve().parents[2] +FIXTURE = json.loads((ROOT / "resources/platform-protocol-specs/command-key-legacy-v1-vectors.json").read_text()) + + +class LegacyFixtureTest(unittest.TestCase): + def test_tokens_are_the_reference_producer_utf8_sha256_tokens(self): + for kind in ("signal", "update"): + self.assertEqual(FIXTURE["keys"]["tokens"][kind], + kind + "-v1:" + hashlib.sha256(FIXTURE["keys"]["original"].encode()).hexdigest()) + + def test_seed_mutations_preserve_every_other_row_and_column(self): + for kind in ("signal", "update"): + with self.subTest(kind=kind), sqlite3.connect(":memory:") as db: + # Only the fixture's two statement effects are under test. + # This minimal table is not a native database seed/catalog. + db.executescript(""" + CREATE TABLE workflow_commands(id TEXT PRIMARY KEY, request_id TEXT UNIQUE, payload BLOB, workflow_run_id TEXT); + CREATE TABLE dw_command_intents(command_id TEXT PRIMARY KEY, intent_digest TEXT); + CREATE TABLE workflow_history_events(id TEXT PRIMARY KEY, payload BLOB); + """) + db.execute("INSERT INTO workflow_commands VALUES (?,?,?,?)", + ("selected", FIXTURE["keys"]["tokens"][kind], b"original", "original-run")) + db.execute("INSERT INTO workflow_commands VALUES ('other','other-key',X'00ff','other-run')") + db.executemany("INSERT INTO dw_command_intents VALUES (?,?)", [("selected", "digest"), ("other", "kept")]) + db.execute("INSERT INTO workflow_history_events VALUES ('history',X'ff00')") + db.commit() + before_other = db.execute("SELECT * FROM workflow_commands WHERE id='other'").fetchall() + before_history = db.execute("SELECT * FROM workflow_history_events").fetchall() + with db: + for statement in FIXTURE["synthetic_legacy_shape"]["statements"]: + self.assertEqual(db.execute(statement, {"command_id": "selected", "legacy_request_id": "legacy-key"}).rowcount, 1) + self.assertEqual(db.execute("SELECT * FROM workflow_commands WHERE id='selected'").fetchone(), + ("selected", "legacy-key", b"original", "original-run")) + self.assertEqual(db.execute("SELECT * FROM dw_command_intents").fetchall(), [("other", "kept")]) + self.assertEqual(db.execute("SELECT * FROM workflow_commands WHERE id='other'").fetchall(), before_other) + self.assertEqual(db.execute("SELECT * FROM workflow_history_events").fetchall(), before_history) + # A failed second statement must roll back the intent deletion. + db.execute("INSERT INTO dw_command_intents VALUES ('selected','restored')") + db.commit() + with self.assertRaises(sqlite3.IntegrityError): + with db: + for statement in FIXTURE["synthetic_legacy_shape"]["statements"]: + db.execute(statement, {"command_id": "selected", "legacy_request_id": "other-key"}) + self.assertEqual(db.execute("SELECT intent_digest FROM dw_command_intents WHERE command_id='selected'").fetchone(), ("restored",)) + self.assertEqual(db.execute("SELECT request_id FROM workflow_commands WHERE id='selected'").fetchone(), ("legacy-key",)) + + def test_reference_cases_keep_distinct_authority(self): + self.assertEqual(FIXTURE["schema"], "durable-workflow.command-key-legacy-fixture.v1") + self.assertEqual({c["shape"] for c in FIXTURE["ambiguity"]}, {"two-rows", "legacy-token", "modern-raw"}) + self.assertEqual(FIXTURE["ambiguity_expected"]["retryable"], False) + self.assertEqual(FIXTURE["legacy_readback"]["expected"]["intent_verified"], False) + self.assertEqual(FIXTURE["physical_upgrade"]["schemas"], + {"sqlite": [12, 13], "postgresql": [13, 14], "mariadb": [13, 14], "mysql": [14, 15]}) + self.assertEqual(FIXTURE["case_only"]["mysql_mariadb_default_ci"]["status"], 409) + self.assertEqual(FIXTURE["case_only"]["sqlite_postgresql_binary"]["zero_write_claim"], False) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/Unit/ServerParityAggregateTest.mjs b/tests/Unit/ServerParityAggregateTest.mjs index 5fe64a8b1..ff89f450f 100644 --- a/tests/Unit/ServerParityAggregateTest.mjs +++ b/tests/Unit/ServerParityAggregateTest.mjs @@ -52,6 +52,7 @@ test('pull requests run without a path filter and push keeps the one path list', for (const input of [ 'tests/Fixtures/ActivityResultInspection/v1.json', 'resources/platform-protocol-specs/workflow-task-completion-limits-v1-vectors.json', + 'resources/platform-protocol-specs/command-key-legacy-v1-vectors.json', 'tests/Fixtures/ServerParityReaders/sdk-signals.mjs', 'tests/Conformance/ServerParityDeadlinePollTest.mjs', 'benchmarks/capacity/v1/bindings/php/capacity_adapter.php',