Skip to content

Commit ec3d57a

Browse files
[cross-repo from workflow#395] server + workflow + sdk-python: make replay verification a first-class platform contract (#18)
1 parent 2e98e07 commit ec3d57a

5 files changed

Lines changed: 56 additions & 1 deletion

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,28 @@ infers the workflow type and input from that event; otherwise pass
203203
contains the commands the workflow would emit next, including determinism
204204
failures surfaced as workflow failure commands.
205205

206+
For CI and operator replay gates, the package also installs offline
207+
verification commands:
208+
209+
```bash
210+
durable-workflow-replay-verify tests/fixtures/golden_history \
211+
--workflows my_app.workflows:all_workflows \
212+
--output replay-report.json
213+
214+
durable-workflow-replay-verify exported-history-bundles \
215+
--simulate-bundles \
216+
--output replay-simulation.json
217+
218+
durable-workflow-history-bundle-verify exported-history-bundles/run-001.json \
219+
--output integrity-report.json
220+
```
221+
222+
`durable-workflow-replay-verify` emits the same verdict and
223+
`promotion_decision` vocabulary as the platform replay contract. Golden-history
224+
mode replays cross-runtime fixtures against registered workflow classes;
225+
`--simulate-bundles` integrity-checks every exported history bundle in a
226+
directory and reports missing bundle evidence as a blocking result.
227+
206228
## External payload storage
207229

208230
Large payload offload is opt-in. `serializer.external_storage_envelope(...)`

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Documentation = "https://durable-workflow.github.io/docs/2.0/polyglot/python"
5656
Repository = "https://github.com/durable-workflow/sdk-python"
5757
Issues = "https://github.com/durable-workflow/sdk-python/issues"
5858

59+
[project.scripts]
60+
durable-workflow-replay-verify = "durable_workflow.replay_verify:main"
61+
durable-workflow-history-bundle-verify = "durable_workflow.history_bundle_verify:main"
62+
5963
[tool.setuptools.packages.find]
6064
where = ["src"]
6165

src/durable_workflow/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@
138138
from .replay_verify import (
139139
CaseReport as ReplayCaseReport,
140140
GoldenHistoryReport,
141+
SimulationReport,
142+
aggregate_verdicts,
143+
promotion_decision_for,
144+
simulate_bundles,
141145
verify_golden_history,
142146
verify_replay,
143147
)
@@ -287,6 +291,10 @@
287291
"replay",
288292
"GoldenHistoryReport",
289293
"ReplayCaseReport",
294+
"SimulationReport",
295+
"aggregate_verdicts",
296+
"promotion_decision_for",
297+
"simulate_bundles",
290298
"verify_golden_history",
291299
"verify_replay",
292300
"HISTORY_BUNDLE_SCHEMA",

src/durable_workflow/replay_verify.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ def simulate_bundles(
661661
VERDICT_DRIFTED: 0,
662662
VERDICT_FAILED: 0,
663663
},
664+
missing_bundles=[str(directory)],
664665
error=f"Bundle directory [{directory}] does not exist.",
665666
)
666667

@@ -674,6 +675,15 @@ def simulate_bundles(
674675
VERDICT_FAILED: 0,
675676
}
676677

678+
if not paths:
679+
return SimulationReport(
680+
verdict=VERDICT_FAILED,
681+
promotion_decision=PROMOTION_BLOCK_AND_INVESTIGATE,
682+
summary=summary,
683+
missing_bundles=[str(directory / "*.json")],
684+
error=f"No history-export bundle JSON files were found in [{directory}].",
685+
)
686+
677687
bundles: list[BundleEntry] = []
678688
verdicts: list[str] = []
679689

@@ -709,7 +719,7 @@ def simulate_bundles(
709719
summary["total"] += 1
710720
summary[verdict] += 1
711721

712-
overall = aggregate_verdicts(verdicts) if verdicts else VERDICT_FAILED
722+
overall = aggregate_verdicts(verdicts)
713723

714724
return SimulationReport(
715725
verdict=overall,

tests/test_replay_verify.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,17 @@ def test_simulate_bundles_returns_failed_for_missing_directory(tmp_path: Path) -
428428

429429
assert report.verdict == VERDICT_FAILED
430430
assert report.promotion_decision == PROMOTION_BLOCK_AND_INVESTIGATE
431+
assert report.missing_bundles == [str(tmp_path / "no-such-dir")]
432+
assert report.error is not None
433+
434+
435+
def test_simulate_bundles_returns_failed_when_directory_has_no_bundles(tmp_path: Path) -> None:
436+
report = simulate_bundles(tmp_path)
437+
438+
assert report.verdict == VERDICT_FAILED
439+
assert report.promotion_decision == PROMOTION_BLOCK_AND_INVESTIGATE
440+
assert report.summary["total"] == 0
441+
assert report.missing_bundles == [str(tmp_path / "*.json")]
431442
assert report.error is not None
432443

433444

0 commit comments

Comments
 (0)