Skip to content

Add orchestration property tests (#72) - #97

Merged
leynos merged 19 commits into
mainfrom
issue-72-hypothesis-property-based-tests-for-episodic-orchestration
May 25, 2026
Merged

Add orchestration property tests (#72)#97
leynos merged 19 commits into
mainfrom
issue-72-hypothesis-property-based-tests-for-episodic-orchestration

Conversation

@leynos

@leynos leynos commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

This branch adds Hypothesis coverage for the structured generation orchestration contracts raised in issue #72. It exercises enum normalisation across mixed ActionKind inputs, rejects every non-execution ModelTier, broadens planner format-error preservation across malformed JSON and invalid plan objects, and checks LangGraph ordering and token aggregation invariants.

Review follow-up changes expand the module documentation, make graph event recording an explicit injected dependency, bound generated malformed payloads, and pin representative PlanningResponseFormatError messages with Syrupy snapshots.

Closes #72.

Review walkthrough

Validation

  • make check-fmt: passed
  • make lint: passed
  • make test PYTEST_XDIST_WORKERS=1: passed, 481 passed, 3 skipped
  • make typecheck: passed

Notes

hypothesis was already present in the dev dependency group, so no dependency or lockfile change was required. One full-suite run hit a transient py-pglite fixture setup timeout in an unrelated reference-document pagination case; the isolated case passed immediately, and the subsequent full-suite retry passed.

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 30664a53-3dab-410c-9a74-aab3a7c81aa6

📥 Commits

Reviewing files that changed from the base of the PR and between 4c05c61 and 47f4687.

📒 Files selected for processing (4)
  • docs/adr/adr-005-structured-planning-and-tool-execution.md
  • docs/developers-guide.md
  • episodic/orchestration/_checkpoint_payload.py
  • episodic/orchestration/_checkpoint_resume.py

Hypothesis Property-Based Tests for Episodic Orchestration

Adds comprehensive Hypothesis-based property tests, shared test scaffolding, and supporting refactors for the episodic/orchestration slice to address issue #72. Documentation and execplan notes were updated; follow-up architectural work (decoupling the finish callback from LangGraph internals) is tracked in issue #108.

Test coverage added

  • New property-test modules and suites:

    • tests/test_orchestration_config_model_tier_properties.py — verifies GenerationOrchestrationConfig normalises mixed ActionKind enums/strings and raises on unknown action-kind strings; verifies ShowNotesToolExecutor rejects every ModelTier except ModelTier.EXECUTION and accepts EXECUTION.
    • tests/test_orchestration_planner_format_properties.py — Hypothesis-driven tests asserting StructuredGenerationPlanner raises PlanningResponseFormatError for arbitrary non-object JSON and structurally invalid/malformed plan payloads (malformed payload generation bounded).
    • tests/test_orchestration_graph_invariant_properties.py — LangGraph invariants: token-aggregation non-negativity, plan → execute → finish ordering, finish_callback invoked on direct-execute and not invoked on suspend (checkpoint) path, finish_callback exceptions do not replace the computed orchestration result, and concurrent direct-path finish_callback behaviour is recorded and asserted.
    • tests/test_orchestration_properties.py and tests/test_orchestration_langgraph_properties.py — migrated to shared property-test support strategies/types.
    • tests/test_orchestration_planner_format_properties.py plus Syrupy snapshots assert and pin representative planner format-error messages.
  • Snapshot testing:

    • tests/snapshots/test_generation_orchestration_snapshots.ambr and tests/test_generation_orchestration_snapshots.py — pinned representative PlanningResponseFormatError messages.
  • Shared test support:

    • tests/_orchestration_property_support.py — centralized Hypothesis strategies, deterministic fakes and helpers (GraphEventRecorder, PropGraphPlanner, PropGraphToolExecutor, PropShowNotesGenerator), malformed-plan payload generators (bounded), regex for planner-format errors, and token/step input strategies reused across property tests.

Implementation and refactoring

  • LangGraph:

    • build_generation_orchestration_graph(...) gains an optional synchronous finish_callback parameter (TYPE_CHECKING-typed Callable[[dto.GenerationOrchestrationResult], None] | None).
    • On the direct-execute path the finish node aggregates the orchestration result and, if provided, invokes finish_callback. Callback exceptions are caught and logged (events added: generation_graph.finish_node.callback.finish and generation_graph.finish_node.callback.error with correlation_id) and do not alter the computed result.
    • Execute-node selection extracted into a helper to choose direct or checkpoint/suspend-before-execute paths.
  • Module factorisation:

    • Extracted checkpoint payload, resume and graph-state responsibilities into:
      • episodic/orchestration/_graph_state.py — new GenerationGraphState dataclass (request, planner_result, action_results, orchestration_result, suspended_result).
      • episodic/orchestration/_checkpoint_payload.py — JSON-compatible checkpoint serialisation/deserialisation with strict validation and enum coercion.
      • episodic/orchestration/_checkpoint_resume.py — suspend/resume logic, persistence interactions and resume_generation_orchestration implementation.
    • episodic/orchestration/langgraph.py refactored to use the new helpers and to accept an injected finish_callback.
    • episodic/orchestration/init.py updated to re-export resume_generation_orchestration and GenerationGraphState from the new modules.
  • DTOs:

    • episodic/orchestration/_dto.py — uuid imported at runtime to support runtime annotation inspection.

Documentation and execplan

  • docs/developers-guide.md — documents finish_callback behaviour (fires only on direct plan → execute → finish path; not invoked on suspend path; exceptions logged and do not replace result), notes property-test coverage (issue #72), and documents GenerationGraphState exposure and callback synchronisation responsibilities.
  • docs/execplans/2-4-1-structured-output-planning-and-tool-calling-execution.md — adds a completion-stage note referencing #72 and the property-based coverage (execplan note updated).
  • docs/roadmap.md — marks issue #72 work complete under Phase 2.4.1.
  • docs/adr/adr-005-structured-planning-and-tool-execution.md — ADR updated to document the finish_callback hook semantics.

Review feedback, fixes and test additions

  • Replaced nondeterministic Hypothesis sampled_from usage with pytest.mark.parametrize where required to deterministically exercise every non-EXECUTION ModelTier; remaining probabilistic instances were addressed in review.

  • Hardened finish_callback invocation: wrapped in try/except, added success/error logging events (with correlation_id), and ensured callback errors do not overwrite the graph result.

  • Added tests asserting:

    • finish_callback invoked exactly once on direct-execute path and receives the final orchestration result.
    • finish_callback not invoked on suspend/checkpoint path and a SuspendedWorkflowResult is produced.
    • finish_callback exceptions do not replace the orchestration result.
    • concurrent direct invocations record one non-None callback result per invocation.
  • Bounded generated malformed planner payloads and pinned representative PlanningResponseFormatError messages with Syrupy snapshots.

  • Architectural coupling: the finish_callback currently accepts GenerationGraphState (LangGraph framework type); decoupling it to accept a domain-level GenerationOrchestrationResult is tracked in issue #108.

Test & CI status

  • make check-fmt passed, make lint passed, make typecheck passed.
  • Test suite (make test PYTEST_XDIST_WORKERS=1) reported 481 passed, 3 skipped. A transient unrelated py-pglite fixture timeout was observed once and retried successfully.

Files of interest

  • Tests & support: tests/_orchestration_property_support.py, tests/test_orchestration_config_model_tier_properties.py, tests/test_orchestration_planner_format_properties.py, tests/test_orchestration_graph_invariant_properties.py, tests/test_orchestration_properties.py, tests/test_orchestration_langgraph_properties.py, tests/test_generation_orchestration_snapshots.py, tests/snapshots/test_generation_orchestration_snapshots.ambr
  • Orchestration internals: episodic/orchestration/langgraph.py, episodic/orchestration/_graph_state.py, episodic/orchestration/_checkpoint_payload.py, episodic/orchestration/_checkpoint_resume.py, episodic/orchestration/_dto.py, episodic/orchestration/init.py
  • Documentation and execplan: docs/developers-guide.md, docs/execplans/2-4-1-structured-output-planning-and-tool-calling-execution.md, docs/roadmap.md, docs/adr/adr-005-structured-planning-and-tool-execution.md

Closing

Closes issue #72. Follow-up architectural/refactor work to decouple the finish_callback contract from LangGraph internals is tracked in issue #108.

Walkthrough

Centralise Hypothesis strategies/fakes and invalid-plan generators; add planner-format snapshots and property tests; implement checkpoint payload serialisation, suspend/resume and GenerationGraphState; add optional synchronous finish_callback to LangGraph; update tests to use shared support.

Changes

Orchestration Property-Based Test Coverage

Layer / File(s) Summary
Shared Hypothesis strategies and fakes
tests/_orchestration_property_support.py
Add PropGraphPlanner, PropGraphToolExecutor, PropShowNotesGenerator, PropTokenInputs/PropStepKeyInputs, Hypothesis strategies for tokens/plans/actions, invalid-plan payload builders, PLANNER_FORMAT_ERROR_PATTERN, and invalid_plan_payloads.
Invalid planner payload helpers & snapshots
tests/test_generation_orchestration_snapshots.py, tests/__snapshots__/test_generation_orchestration_snapshots.ambr
Add canonical valid plan object, JSON-corruption helpers, utilities to capture StructuredGenerationPlanner parse failures as strings, and a Syrupy snapshot mapping malformed-plan scenarios to captured format-error strings.
StructuredGenerationPlanner format validation properties
tests/test_orchestration_planner_format_properties.py
Add Hypothesis-driven async tests asserting StructuredGenerationPlanner raises PlanningResponseFormatError for arbitrary non-object JSON and for generated invalid plan payloads matching PLANNER_FORMAT_ERROR_PATTERN.
Config normalisation & model-tier gating
tests/test_orchestration_config_model_tier_properties.py
Add Hypothesis test ensuring mixed ActionKind enums/strings normalise to ActionKind, test that unknown strings raise ValueError, and async tests that ShowNotesToolExecutor rejects non-EXECUTION tiers and accepts EXECUTION.
Test migrations to shared support
tests/test_orchestration_properties.py, tests/test_orchestration_langgraph_properties.py
Replace local underscore-prefixed strategies/types with shared imports and update Hypothesis @given uses and parameter annotations to use Prop* helpers and *_strategy objects.
LangGraph invariant and finish_callback tests
tests/test_orchestration_graph_invariant_properties.py
Add property/async tests asserting total tokens are non-negative and equal planner+tool totals, lifecycle order is ["plan","execute","finish"], finish_callback behaviour on direct vs suspend paths, callback-exception robustness, and concurrent direct invocation recording.
Checkpoint payloads
episodic/orchestration/_checkpoint_payload.py
Implement JSON-compatible checkpoint serialisation/deserialisation for LLMUsage, ExecutionPlan, PlannedAction, PlannerResult and ActionExecutionResult, with runtime type checks and enum parsing.
Checkpoint suspend & resume
episodic/orchestration/_checkpoint_resume.py
Add suspend helpers to compute deterministic execute-step idempotency keys, persist/reuse checkpoints, validate single-step preconditions, and implement resume_generation_orchestration that deserialises planner_result, resumes via resume_port, and marks checkpoints resumed.
GenerationGraphState and LangGraph finish callback
episodic/orchestration/_graph_state.py, episodic/orchestration/langgraph.py, episodic/orchestration/_dto.py, episodic/orchestration/__init__.py
Add GenerationGraphState dataclass, move state and checkpoint helpers into dedicated modules, add ExecuteNodeResult/ExecuteNodeFn types, extend build_generation_orchestration_graph with optional synchronous finish_callback, wrap finish node to invoke the callback on the direct path and log exceptions, rewire execute-node selection for suspend vs direct execute, and make uuid import runtime-visible in DTOs.
Docs, ADR, roadmap and snapshots
docs/developers-guide.md, docs/execplans/..., docs/roadmap.md, docs/adr/adr-005-structured-planning-and-tool-execution.md, tests/__snapshots__/*
Document finish_callback semantics and property-test coverage reference #72; add execplan/roadmap notes; update ADR with callback behaviour; add planner-format error message snapshot.

Sequence Diagram

sequenceDiagram
  participant Request as GenerationOrchestrationRequest
  participant Planner as StructuredGenerationPlanner
  participant Graph as LangGraph
  participant Checkpoint as CheckpointPort
  participant Executor as ToolExecutor
  participant FinishCB as finish_callback
  Request->>Planner: plan(request)
  Planner->>Graph: provide PlannerResult
  alt checkpoint_port provided (suspend)
    Graph->>Checkpoint: save_or_reuse(checkpoint_payload)
    Checkpoint-->>Graph: return SuspendedWorkflowResult
  else direct execute path
    Graph->>Executor: execute(planned_action)
    Executor-->>Graph: return ActionExecutionResult
    Graph->>FinishCB: invoke(orchestration_result)
    FinishCB-->>Graph: returns or raises (exception logged)
  end
  Graph-->>Request: return GenerationOrchestrationResult or SuspendedWorkflowResult
Loading

Possibly related PRs

  • leynos/episodic#77 — Related refactor moving LangGraph suspend/finish control flow and checkpoint handling; overlaps with checkpoint/resume and state changes in this PR.
Prompt for AI agent (wrap future test-run instructions here)

"Plan then execute then finish in order, true;
Fuzzed planners cough up errors pinned with care.
Checkpoints pause and later hand back cue,
Tokens counted, state held, callbacks called — all fair. ✨"

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #72

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-72-hypothesis-property-based-tests-for-episodic-orchestration

@sourcery-ai

sourcery-ai Bot commented May 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds property-based tests (Hypothesis) to validate orchestration behaviour across planning, execution, and LangGraph flows, including enum normalisation, model-tier boundaries, malformed planner payload handling, and event ordering/token aggregation invariants.

File-Level Changes

Change Details Files
Extend orchestration property tests with richer planners/executors and LangGraph event tracking.
  • Add event recording to the test planner and tool executor to trace orchestration phases.
  • Introduce a test-only show-notes generator that returns a minimal structured result with deterministic token usage.
  • Bundle token-related inputs for LangGraph property tests into a dedicated dataclass.
tests/test_orchestration_properties.py
Introduce Hypothesis strategies for invalid planner payloads and enum/required-input variations to assert format error preservation.
  • Define helpers to construct valid and systematically invalid plan JSON objects (missing/extra/top-level and step fields).
  • Add composite Hypothesis strategies that generate unknown action kinds, model tiers, and malformed required_inputs shapes.
  • Combine these into a unified invalid-plan-payload strategy used in a new property test that asserts PlanningResponseFormatError with specific field names.
tests/test_orchestration_properties.py
Broaden property coverage for configuration enum normalisation and model-tier acceptance/rejection boundaries.
  • Extend config normalisation property test to assert the normalised enabled_action_kinds sequence matches ActionKind(str(kind)) for all mixed inputs.
  • Add an async test that verifies execution-tier PlannedAction inputs are accepted by ShowNotesToolExecutor and preserve the execution model tier in the result.
  • Keep existing non-execution ModelTier rejection property test and validate it against the new boundaries.
tests/test_orchestration_properties.py
Add LangGraph property tests to ensure non-negative token aggregation and strict plan/execute/finish ordering.
  • Tighten assertions in the total-token aggregation property test to require non-negative input/output tokens as well as total_tokens, and equality with summed planner+tool usage.
  • Introduce a new property test that builds a GenerationOrchestrationRequest via Hypothesis, runs the orchestration graph with test planner/executor, and asserts event order is exactly plan -> execute -> finish.
  • Verify that planner_result, action_results, and orchestration_result are all present after graph execution.
tests/test_orchestration_properties.py

Assessment against linked issues

Issue Objective Addressed Explanation
#72 Add Hypothesis property-based tests to verify GenerationOrchestrationConfig correctly normalises arbitrary mixes of ActionKind enum values and their string equivalents across the full input space.
#72 Add Hypothesis property-based tests to verify ShowNotesToolExecutor.execute() rejects every ModelTier value other than ModelTier.EXECUTION (and correctly accepts ModelTier.EXECUTION at the boundary).
#72 Add Hypothesis property-based tests to verify StructuredGenerationPlanner raises PlanningResponseFormatError for all malformed/invalid plan payloads and that the LangGraph state-machine enforces plan → execute → finish ordering with non-negative aggregated token counts for arbitrary valid GenerationOrchestrationRequest inputs.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review May 13, 2026 23:23
sourcery-ai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot added the Issue label May 13, 2026
coderabbitai[bot]

This comment was marked as resolved.

@leynos
leynos marked this pull request as draft May 14, 2026 00:03
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@lodyai
lodyai Bot force-pushed the issue-72-hypothesis-property-based-tests-for-episodic-orchestration branch from 6a63526 to af454cc Compare May 15, 2026 10:46
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review May 15, 2026 14:16
sourcery-ai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@leynos

leynos commented May 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following now been resolved?

tests/test_orchestration_properties.py (1)

422-424: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win
Replace probabilistic tier sampling with deterministic exhaustive coverage.
Hypothesis st.sampled_from() with @settings(max_examples=len(ModelTier)) does not guarantee every non-execution tier is tested—values are drawn with replacement, and duplicates can skip enum members entirely. The test name claims to reject "all non-execution tiers", yet the implementation provides probabilistic rather than exhaustive coverage. Use pytest.mark.parametrize to guarantee all tiers are exercised.

Proposed fix
-@given(model_tier=st.sampled_from([t for t in ModelTier if t != ModelTier.EXECUTION]))
-@settings(max_examples=len(ModelTier))
-@pytest.mark.asyncio
-async def test_planned_action_model_tier_rejection_for_all_non_execution_tiers(
-    model_tier: ModelTier,
-) -> None:
+@pytest.mark.parametrize(
+    ("model_tier",),
+    [(tier,) for tier in ModelTier if tier is not ModelTier.EXECUTION],
+)
+@pytest.mark.asyncio
+async def test_planned_action_model_tier_rejection_for_all_non_execution_tiers(
+    model_tier: ModelTier,
+) -> None:

Coding guidelines (**/test_*.py) require "parametrize broadly" for proper test idioms.

🤖 Detailed instructions

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @tests/test_orchestration_properties.py around lines 422 - 424, Replace the
probabilistic Hypothesis-based sampling with deterministic parametric coverage:
remove the @given(...) and @settings(...) decorators and instead add a
pytest.mark.parametrize for the
test_planned_action_model_tier_rejection_for_all_non_execution_tiers test that
iterates every ModelTier member except ModelTier.EXECUTION (e.g., parametrize
("model_tier",) with [(tier,) for tier in ModelTier if tier is not
ModelTier.EXECUTION]); keep @pytest.mark.asyncio and the async test signature
unchanged so the test deterministically exercises all non-execution tiers.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "test_planned_action_model_tier_rejection_for_all_non_execution_tiers|sampled_from|max_examples=len(ModelTier)|parametrize" tests/test_orchestration_properties.py -A 3 -B 3

Length of output: 1571


@leynos: Yes, this has been resolved. Lines 453–459 of tests/test_orchestration_properties.py now read:

`@pytest.mark.parametrize`(
    "model_tier",
    [tier for tier in ModelTier if tier is not ModelTier.EXECUTION],
)
`@pytest.mark.asyncio`
async def test_planned_action_model_tier_rejection_for_all_non_execution_tiers(
    model_tier: ModelTier,
) -> None:

The @given and @settings(max_examples=len(ModelTier)) decorators have been removed and replaced with @pytest.mark.parametrize, guaranteeing deterministic, exhaustive coverage of every non-execution ModelTier member.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/orchestration/langgraph.py`:
- Around line 639-641: The finish-callback invocation (finish_callback(state))
must be isolated so exceptions raised by the callback do not prevent returning
the already-computed result; wrap the finish_callback call in a try/except that
catches exceptions, logs or handles the error, and then continue to return
result unconditionally. Locate the finish_callback(state) call in the function
where result and state are computed and replace it with a guarded call that
preserves returning result even if finish_callback fails.

In `@tests/test_orchestration_properties.py`:
- Around line 12-15: The current oversized property-test module uses a
file-level suppression "pylint: disable=too-many-lines"; split it into smaller
focused test modules (e.g., config_model_tier_tests, planner_format_error_tests,
graph_invariants_tests), move shared domain-specific strategies/fakes/fixtures
into a shared helper (or conftest) so each new test module can reuse them,
ensure every new test file stays below the 400 logical-line limit, remove the
file-level "pylint: disable=too-many-lines" suppression, and update imports/test
discovery to reference the new module names so all tests still run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dfe3323f-7c6b-4550-ac57-9b907c0cc535

📥 Commits

Reviewing files that changed from the base of the PR and between af454cc and 291671c.

📒 Files selected for processing (2)
  • episodic/orchestration/langgraph.py
  • tests/test_orchestration_properties.py

Comment thread episodic/orchestration/langgraph.py
Comment thread tests/test_orchestration_properties.py Outdated
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_orchestration_properties.py`:
- Around line 471-477: The test uses Hypothesis sampling (st.sampled_from +
`@settings`(max_examples=10)) but filtering ModelTier to non-EXECUTION yields a
single value and produces duplicate runs; replace the Hypothesis decorators with
pytest.parametrization: remove `@given` and `@settings` and add
`@pytest.mark.parametrize`(("model_tier",), [(tier,) for tier in ModelTier if tier
is not ModelTier.EXECUTION]) above the test function
test_planned_action_model_tier_rejection_for_all_non_execution_tiers so the test
deterministically runs once per non-EXECUTION ModelTier value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ce0ac05a-cc5b-4df4-bc5e-3849f2ffde02

📥 Commits

Reviewing files that changed from the base of the PR and between 291671c and 21690cf.

📒 Files selected for processing (2)
  • tests/test_generation_orchestration_snapshots.py
  • tests/test_orchestration_properties.py

Comment thread tests/test_orchestration_properties.py Outdated
@leynos

leynos commented May 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already.

❌ Failed checks (2 errors, 5 warnings)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Test test_planned_action_model_tier_rejection uses @given with sampled_from filtering to single value with max_examples=10, generating identical test cases. Review requested parametrised coverage. Replace @given/@settings with @pytest.mark.parametrize covering ModelTier excluding EXECUTION, per review comments and PR objectives.
Unit Architecture ❌ Error The finish_callback parameter accepts unvalidated code without explicit error handling. Fallibility is hidden from the API with no documented exception contract, allowing uncaught errors. Document finish_callback fallibility contract explicitly. Surface exceptions or mark infallible in type/docs. Handle errors in _run_finish_node. Decouple event recording from test fakes; use test wrappers instead.
User-Facing Documentation ⚠️ Warning PR introduces a new optional finish_callback parameter to public API function build_generation_orchestration_graph(), but no documentation was updated or added. Document the new finish_callback parameter in the function's docstring and in docs/developers-guide.md orchestration section, explaining its purpose, type signature, and usage context.
Developer Documentation ⚠️ Warning PR adds new public API parameter finish_callback and property tests for issue #72, but fails to document in docs/developers-guide.md. No design documents, ADRs, or roadmap updates. Document finish_callback parameter and issue #72 tests in orchestration section of docs/developers-guide.md. Update roadmap to mark issue #72 complete. Consider adding an ADR if warranted.
Testing (Unit And Behavioural) ⚠️ Warning Tier rejection test uses @given/@settings instead of parametrize as requested. finish_callback untested with checkpoint_port. Use @pytest.mark.parametrize for exhaustive ModelTier coverage. Test finish_callback in both direct-execute and suspend-before-execute graph paths.
Domain Architecture ⚠️ Warning finish_callback parameter accepts GenerationGraphState, a LangGraph framework detail. Domain architecture requires callbacks to accept domain-shaped events, not framework structures. Redefine finish_callback to accept domain-level objects rather than LangGraph's GenerationGraphState implementation detail.
Observability ⚠️ Warning The finish_callback invokes user code without exception handling or logging, creating an unobservable failure point in a critical path. Wrap finish_callback invocation in try/except with _log_event calls for success/failure. Document the callback contract and exception behaviour in the function docstring.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented May 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai please create a new GitHub issue for the following:

⚠️ Warning 6 — Domain Architecture

Status: Not resolved. finish_callback exposes GenerationGraphState — a LangGraph framework type — in the public API of build_generation_orchestration_graph. This couples callers to the framework.

This is a design decision that likely warrants its own issue rather than being addressed inline. A GitHub issue does not currently exist for it.

Shall I open a GitHub issue to track replacing finish_callback: Callable[[GenerationGraphState], None] with a callback that accepts a domain-level result object (e.g. GenerationOrchestrationResult)? That would be the correct resolution per the Domain Architecture warning without blocking this PR.

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo leynos/episodic \
  --title "Decouple finish_callback from LangGraph internals: accept domain-level result instead of GenerationGraphState" \
  --assignee leynos \
  --body "## Summary

The \`finish_callback\` parameter added to \`build_generation_orchestration_graph()\` in PR `#97` accepts a \`Callable[[GenerationGraphState], None]\`. \`GenerationGraphState\` is a LangGraph framework detail, which means callers of this public API are now coupled to the orchestration framework's internal state representation.

## Problem

Exposing \`GenerationGraphState\` in the public function signature violates domain-architecture boundaries: it forces any code that registers a finish callback to depend on a LangGraph implementation type rather than a domain-level object. If the orchestration framework changes or is replaced, all callback implementations must change too.

## Proposed Resolution

Redefine \`finish_callback\` to accept a domain-level object rather than \`GenerationGraphState\`. The most natural candidate is \`GenerationOrchestrationResult\`, which is already the domain-level result type produced by the finish node. The revised signature would be:

\`\`\`python
finish_callback: cabc.Callable[[GenerationOrchestrationResult], None] | None = None
\`\`\`

The implementation in \`_run_finish_node\` should extract the \`GenerationOrchestrationResult\` from state and pass it — rather than the raw state — to the callback.

## Acceptance Criteria

- [ ] \`finish_callback\` signature updated to \`Callable[[GenerationOrchestrationResult], None] | None\`
- [ ] \`_run_finish_node\` passes the domain result object, not the LangGraph state, to the callback
- [ ] Existing tests updated to reflect the new callback contract
- [ ] \`build_generation_orchestration_graph\` docstring updated accordingly

## Context

Raised as a Domain Architecture warning during review of PR `#97`. Deferred from that PR as it is a design decision warranting independent tracking.

Requested by `@leynos`."

Length of output: 46


@leynos: Issue #108 has been opened at #108"Decouple finish_callback from LangGraph internals: accept domain-level result instead of GenerationGraphState" — assigned to you.

It documents the problem, the proposed resolution (change the callback signature to Callable[[GenerationOrchestrationResult], None] and pass the domain result from _run_finish_node), and the acceptance criteria, with a backlink to PR #97.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/orchestration/langgraph.py`:
- Around line 633-637: Update the docstring for the finish_callback parameter in
langgraph.py to precisely state its runtime contract: the callback is invoked as
finish_callback(state) where state is a copied GenerationGraphState whose
orchestration_result field has been populated (not the original mutable state);
keep that callback exceptions are logged and propagated to the caller. Ensure
the text references the exact symbol names finish_callback,
GenerationGraphState, and orchestration_result so readers know which object is
copied and which field is set.

In `@tests/test_orchestration_graph_invariant_properties.py`:
- Around line 180-213: The test duplicates planner/tool setup; replace the
inline Construction of PropGraphPlanner and PropGraphToolExecutor with calls to
the existing helper functions _planner_result() and _tool_result(): locate where
planner is built using PropGraphPlanner(...) and where tool_executor is built
using PropGraphToolExecutor(...) and change them to reuse the returned
PlannerResult and ActionExecutionResult helpers by passing _planner_result()
into PropGraphPlanner (or directly assigning the helper result if it already
returns the planner) and _tool_result() into PropGraphToolExecutor (or assign
the helper result), removing the repeated literal fields so the test uses the
shared helpers instead of rebuilding equivalent objects.

In `@tests/test_orchestration_planner_format_properties.py`:
- Around line 37-39: Two module-level tests (including
test_planning_response_format_error_for_arbitrary_non_object_json and the other
test at lines 68-70) should be moved into a test class (e.g.,
TestStructuredGenerationPlannerFormatProperties) so related property tests are
grouped; wrap both functions as methods of that class, keep their names
unchanged and still prefixed with test_, and ensure any fixtures (like noise)
remain as method parameters and imports/async markers are preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0c823e2c-ebf3-4de8-972f-72be60a209de

📥 Commits

Reviewing files that changed from the base of the PR and between 21690cf and 9d0e4ce.

📒 Files selected for processing (10)
  • docs/developers-guide.md
  • docs/execplans/2-4-1-structured-output-planning-and-tool-calling-execution.md
  • docs/roadmap.md
  • episodic/orchestration/langgraph.py
  • tests/_orchestration_property_support.py
  • tests/test_orchestration_config_model_tier_properties.py
  • tests/test_orchestration_graph_invariant_properties.py
  • tests/test_orchestration_langgraph_properties.py
  • tests/test_orchestration_planner_format_properties.py
  • tests/test_orchestration_properties.py

Comment thread episodic/orchestration/langgraph.py Outdated
Comment thread tests/test_orchestration_graph_invariant_properties.py
Comment thread tests/test_orchestration_planner_format_properties.py Outdated
Wrap the structured generation planner format property tests in a pytest
collection class while preserving their decorators and test bodies.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Share graph construction and callback recording between the direct and
suspend finish-callback invariant tests without changing their assertions.
@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Adjust the extracted finish-callback helper to accept the already-derived
correlation id instead of the full graph state.

Keep the graph builder below the CodeScene function-size threshold while
preserving the result-only callback contract.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/orchestration/langgraph.py`:
- Around line 635-639: The current return type tuple[typ.Any, str] for
_build_execute_node is too broad—define a precise callable type: add a union
type ExecuteNodeResult = dict[str, tuple[dto.ActionExecutionResult, ...]] |
dict[str, dto.SuspendedWorkflowResult] and an ExecuteNodeFn =
typ.Callable[[GenerationGraphState], typ.Awaitable[ExecuteNodeResult]] then
change _build_execute_node signature to return tuple[ExecuteNodeFn, str]; update
any internal references and imports (typ, GenerationGraphState, dto) so the
inner async node callables conform to ExecuteNodeFn and the function return
annotation uses the new types.

In `@tests/_orchestration_property_support.py`:
- Around line 174-179: The current usage_strategy constructs LLMUsage with
independent input_tokens, output_tokens, and total_tokens which can be
inconsistent; change usage_strategy to build total_tokens from the sampled
component counts so total_tokens = input_tokens + output_tokens (i.e., use
st.builds with a combining factory that accepts sampled input_tokens and
output_tokens and returns LLMUsage(input_tokens=..., output_tokens=...,
total_tokens=input+output)). Update the usage_strategy reference (LLMUsage and
usage_strategy) so generated examples are internally consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd0f4d79-e948-4f62-a419-557d3a5ccc6a

📥 Commits

Reviewing files that changed from the base of the PR and between ba37458 and 21ad6c8.

📒 Files selected for processing (13)
  • docs/developers-guide.md
  • docs/execplans/2-4-1-structured-output-planning-and-tool-calling-execution.md
  • docs/roadmap.md
  • episodic/orchestration/_dto.py
  • episodic/orchestration/langgraph.py
  • tests/__snapshots__/test_generation_orchestration_snapshots.ambr
  • tests/_orchestration_property_support.py
  • tests/test_generation_orchestration_snapshots.py
  • tests/test_orchestration_config_model_tier_properties.py
  • tests/test_orchestration_graph_invariant_properties.py
  • tests/test_orchestration_langgraph_properties.py
  • tests/test_orchestration_planner_format_properties.py
  • tests/test_orchestration_properties.py

Comment thread episodic/orchestration/langgraph.py
Comment thread tests/_orchestration_property_support.py
Move graph state, checkpoint payload serialization, and suspend/resume logic
out of `langgraph.py` into private orchestration modules.

Keep the existing `langgraph` private helper names importable for tests and
other callers while reducing the module below the CodeScene file-size limit.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@episodic/orchestration/_checkpoint_payload.py`:
- Around line 150-180: The current construction coerces malformed step entries
and required_inputs by casting steps and doing str(item); instead validate each
step is a mapping/object and enforce required_inputs as list[str] or raise
TypeError with context: inside the generator that builds dto.PlannedAction for
each element of steps, first assert the step is a dict/object (reject non-object
steps), use _required_string/_required_enum/_require_field as before but ensure
_require_field(..., "required_inputs") returns a list and then validate every
item is a str (do not call str(item)); if any check fails raise TypeError
including the field name and "plan step" context so callers can see which step
and field is invalid (update references: PlannedAction, _required_string,
_required_enum, _require_field, and the steps generator).

In `@episodic/orchestration/_graph_state.py`:
- Around line 14-15: Add a full NumPy-style docstring for the public dataclass
GenerationGraphState that opens with a one-line summary, followed by extended
description and structured sections: Parameters (listing and typing each
dataclass field and its semantics), Attributes (if different from Parameters),
Raises (if any invariants), Notes (behavioral/contract details for graph-node
authors), and Examples (minimal usage). Make sure to name the class
GenerationGraphState in the docstring, document each public field exactly as
declared in the dataclass with types and expected values/constraints, and
explain the orchestration semantics (initialize → plan → execute → finish) and
how nodes should read/modify the state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dbdb694a-fa4a-47f4-b232-0a86c511f23b

📥 Commits

Reviewing files that changed from the base of the PR and between 21ad6c8 and 84ce665.

📒 Files selected for processing (5)
  • episodic/orchestration/__init__.py
  • episodic/orchestration/_checkpoint_payload.py
  • episodic/orchestration/_checkpoint_resume.py
  • episodic/orchestration/_graph_state.py
  • episodic/orchestration/langgraph.py

Comment thread episodic/orchestration/_checkpoint_payload.py Outdated
Comment thread episodic/orchestration/_graph_state.py Outdated
Add a precise execute-node protocol, generate internally consistent
LLM usage values in property tests, and reject malformed checkpoint plan
step payloads instead of coercing them.

Expand `GenerationGraphState` documentation with the graph traversal
contract for node authors.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added Roadmap and removed Issue labels May 22, 2026
Correct the developer guide to describe `finish_callback` as a domain-result
hook and document the public `GenerationGraphState` export separately.

Add focused concurrent direct-path coverage so shared callback behaviour stays
explicit, and document that shared mutable callbacks own their synchronisation.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added Issue and removed Roadmap labels May 24, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/developers-guide.md`:
- Around line 842-844: Update the callback-concurrency paragraph by replacing
British spellings "serialise" and "synchronisation" with the standardized -ize
forms "serialize" and "synchronization" to conform to the repo's en-GB-oxendict
(-ize) guideline; ensure the sentence reads "...The graph does not serialize
concurrent invocations of a shared callback; callbacks that mutate shared state
must provide their own synchronization."

In `@episodic/orchestration/langgraph.py`:
- Around line 226-228: Docstrings in episodic/orchestration/langgraph.py use
en-GB oxendict spellings like "serialise"/"synchronise"/"synchronisation" in the
callback contract; update those to the project's en-GB-oxendict-approved forms
"serialize"/"synchronize"/"synchronization" instead. Locate the callback
contract text in the docstring around the graph execution callback (mentions
"already-computed graph result" and "invoked synchronously in the graph
execution context") and perform the string substitutions there and in the other
occurrences noted (also around lines 311-314) so all instances of
serialise/synchronise/synchronisation are replaced with
serialize/synchronize/synchronization. Ensure only docstrings/comments are
changed and run tests/lint afterwards.

In `@tests/test_orchestration_graph_invariant_properties.py`:
- Around line 317-319: The test currently asserts positional equality between
observed_results and the list comprehension [state["orchestration_result"] for
state in states], which flakes under concurrent runs; change this to
order-insensitive comparison by asserting multiset equivalence (e.g., compare
collections.Counter(observed_results) ==
collections.Counter([state["orchestration_result"] for state in states])) while
keeping the existing length check (len(observed_results) ==
expected_invocations) and the non-None assertion (all(result is not None for
result in observed_results)); update the assertion that references
observed_results and states to use Counter (or another multiset comparison)
instead of direct list equality.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f742faae-2c72-4785-bf93-695e9f43a832

📥 Commits

Reviewing files that changed from the base of the PR and between 653fc84 and 8feb5ad.

📒 Files selected for processing (3)
  • docs/developers-guide.md
  • episodic/orchestration/langgraph.py
  • tests/test_orchestration_graph_invariant_properties.py

Comment thread docs/developers-guide.md Outdated
Comment thread episodic/orchestration/langgraph.py Outdated
Comment thread tests/test_orchestration_graph_invariant_properties.py Outdated
Normalize the finish callback concurrency prose to Oxford `-ize` forms and
make the concurrent callback assertion order-insensitive so scheduling order
cannot affect the invariant test.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Expand the private checkpoint module docstrings so their role in the
LangGraph suspend and resume flow is clear.

Document `resume_generation_orchestration` in the developer guide and record
the `finish_callback` observation contract in ADR-005.
@leynos
leynos merged commit 5b6a984 into main May 25, 2026
4 checks passed
@leynos
leynos deleted the issue-72-hypothesis-property-based-tests-for-episodic-orchestration branch May 25, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Hypothesis property-based tests for episodic/orchestration/

1 participant