test(bdd): add direct test for _fast_init_or_upgrade early-return behavior #1156
No reviewers
Labels
No labels
auto/needs-reevaluation
controller-managed
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Blocks
#733 test(bdd): add direct test for _fast_init_or_upgrade early-return behavior
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core!1156
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "test/m3-fast-init-early-return"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds direct BDD coverage for the
_fast_init_or_upgradeclosure installed by the template-DB patch so the early-return behavior is explicitly verified instead of only being inferred from broader CLI scenarios. This protects the anti-hang behavior for parallel test execution by asserting when migration delegation must be skipped versus executed.Changes
features/fast_init_upgrade.featurewith five@mock_onlyscenarios that cover all relevant paths:features/steps/fast_init_upgrade_steps.pywith step definitions that set up each DB/url case, invokeMigrationRunner.init_or_upgrade, and assert both call behavior and file outcomes.features/mocks/fast_init_test_helpers.pywithpatch_original_init_or_upgrade()to patch the closure-captured_original_init_or_upgradereference for deterministic call tracking during BDD execution.Testing
_fast_init_or_upgradebehavior under the same patching mechanism used at runtime.Related Issues
Closes #733
Dependency Link
Forgejo dependency direction is set as required: this PR blocks #733, and issue #733 depends on this PR.
Implementation Notes
The helper uses closure-cell replacement of
_original_init_or_upgradeto avoid recreating or re-implementing the function under test, keeping the tests focused on real production control flow while remaining isolated and deterministic.Scoped Code Review Report (Issue #733)
I reviewed the latest local commit by Luis Mendes:
1b1dc3a1e52dbae56d76f812a75e811a0281918ftest/m3-fast-init-early-returnScope enforced
Per request, the review scope was limited to:
features/fast_init_upgrade.featurefeatures/steps/fast_init_upgrade_steps.pyfeatures/mocks/fast_init_test_helpers.pyfeatures/environment.py(_install_template_db_patch,_fast_init_or_upgrade)src/cleveragents/infrastructure/database/migration_runner.py(init_or_upgrade)docs/specification.mdtesting architecture sectionsIterative review cycles performed
I repeated full-category passes until no new issue types appeared.
Cycle 1 (correctness/coverage/security/perf):
Cycle 2 (execution/type/lint + parallel behavior):
nox -s unit_tests -- features/fast_init_upgrade.feature --processes 1nox -s unit_tests -- features/fast_init_upgrade.feature --processes 4nox -s typechecknox -s lintCycle 3 (targeted security pass):
Findings (ordered by severity, then category)
Medium
1) Security / Test Reliability — insecure temp path creation (TOCTOU)
features/steps/fast_init_upgrade_steps.py:103tempfile.mktemp(...)is used to generate a non-existent DB path.mktempis deprecated/insecure due race-window behavior. Another process can claim that path before use, causing flaky behavior in parallel CI and creating potential symlink/path-hijack risk on shared runners.mkstemp+ close/unlink before use, or allocate path inside aTemporaryDirectory.Low
2) Test Coverage Gap — missing explicit scenario for existing empty DB file path
features/environment.py:474@mock_onlyscenario asserting existing empty DB triggers template copy and does not call original migration.3) Type-Safety Policy Drift — inline type suppression in new helper
features/mocks/fast_init_test_helpers.py:90,features/mocks/fast_init_test_helpers.py:94# type: ignore[attr-defined]oncell.cell_contentsassignment.Anycast/helper abstraction and keep pyright strict without ignores.Overall assessment
Follow-up on the scoped review findings for #1156 / issue #733:
Applied all valid fixes on branch
test/m3-fast-init-early-return:Security/reliability fix
tempfile.mktemp) with a race-safe approach usingtempfile.mkdtemp+ deterministic filename in a private temp directory.Coverage fix
Type-safety cleanup
# type: ignoresuppression from closure-cell patch helper by using localAnynarrowing on the cell reference.Validation run (with
TEST_PROCESSES=9as required):nox -s unit_tests -- features/fast_init_upgrade.feature --processes 1✅nox -s unit_tests -- features/fast_init_upgrade.feature --processes 9✅nox -s lint✅nox -s typecheck✅No additional invalid/contradictory fixes were identified against issue #733 or
docs/specification.mdfor this scoped branch work.1b1dc3a1e5a52b54b4caCode Review Note
Unable to review — the branch
test/m3-fast-init-early-returnwas not found on the remote. Please verify the branch exists and has been pushed.Review: APPROVED
Thorough testing of
_fast_init_or_upgradeclosure mechanism. The closure cell manipulation is brittle but necessary and well-documented. 6 BDD scenarios covering non-empty DB skip, template copy, empty DB copy, non-matching prefix fallback, in-memory fallback, and non-SQLite fallback. Mocks correctly placed infeatures/mocks/.Independent Code Review Report -- PR #1156 / Issue #733
Reviewer: automated deep review (4 iterative cycles)
Scope: Code changes in branch
test/m3-fast-init-early-returnplus close connections to surrounding code (features/environment.pypatch logic,migration_runner.py)Cross-referenced: Issue #733 acceptance criteria,
docs/specification.md, prior review (comment #72933)Methodology
Performed 4 full review cycles across all categories (bugs, test coverage/flaws, security, performance, code quality/documentation). Each cycle re-examined all categories until no new findings emerged. Prior review findings (mktemp insecurity, missing empty-DB scenario, type suppression) were confirmed as already addressed in commit
a52b54b.Findings by Severity
LOW
L1: Test Coverage Gap -- Missing test for
sqlite://bare in-memory URL variantfeatures/fast_init_upgrade.feature(missing scenario)features/environment.py:459_fast_init_or_upgradeclosure checks two in-memory SQLite forms: Scenario 5 ("In-memory SQLite delegates") testssqlite:///:memory:but no scenario tests thesqlite://variant. This leaves one branch of the disjunction untested.sqlite://equality check is accidentally removed or altered, no test would catch the regression.Given a bare sqlite:// URL for fast-init testingthat setscontext.fast_init_db_url = "sqlite://"and asserts delegation to original.L2: Test Flaw -- Delegation scenarios don't verify
selfargument passed to mockfeatures/steps/fast_init_upgrade_steps.py:180-183mock_original.call_count == 1but never verify the mock was called with the correct arguments. Specifically, they don't check thatself(theMigrationRunnerinstance) was the first positional argument:selfis lost or substituted in the delegation call_original_init_or_upgrade(self, **kwargs)would pass silently.mock_original.call_argson context and add a Then step asserting the runner instance was passed.L3: Test Flaw -- No verification that
kwargsare forwarded in delegation pathsfeatures/steps/fast_init_upgrade_steps.py:181runner.init_or_upgrade()is always called without arguments. The originalMigrationRunner.init_or_upgradeacceptsrequire_confirmation: boolandprompt_for_migration: Callable. The_fast_init_or_upgradeclosure uses**kwargsto forward these, but no scenario exercises this forwarding:**kwargsforwarding breaks (e.g., closure signature changes to drop**kwargs), no test catches it.call_args.kwargs.L4: Documentation -- Commit message claims "5 BDD scenarios" but feature file contains 6
a52b54bmessage bodyL5: Documentation -- CHANGELOG entry misleading about "replacing"
mktempCHANGELOG.md, unreleased sectionmktempusage with private temp-directory allocation."However, this diff does not replace any existing
mktempcalls. The new code usesmkstemp/mkdtempfrom the start. The existingtempfile.mktempinenvironment.py:564(before_scenario) remains untouched. The word "replacing" implies a modification to existing code that didn't occur.mktempreplacement is tracked in the separate PR #736._fast_init_or_upgrade[...]. Uses race-safe temp-path allocation (mkstemp/mkdtemp) throughout."INFO
I1: Code Quality -- Redundant
hasattrchecks in cleanup helpersfeatures/steps/fast_init_upgrade_steps.py:31,44_register_path_cleanupand_register_directory_cleanupguard withif not hasattr(context, "_cleanup_handlers"). However,before_scenario(environment.py:509) always initializescontext._cleanup_handlers = []before any step runs. The guard is dead code.repl_coverage_steps.py:99,settings_steps.py:378). Not harmful but technically unreachable.I2: Thread Safety -- Class-level closure cell replacement without synchronization
features/mocks/fast_init_test_helpers.py:91patch_original_init_or_upgrade()mutates the_original_init_or_upgradeclosure cell on the class-levelMigrationRunner.init_or_upgradefunction. During thewithblock window, any concurrent thread hitting a fallback path in_fast_init_or_upgradewould invoke theMagicMockinstead of the real migration runner, silently skipping migration.@mock_onlyfeatures skip DB-touching setup. In the current execution model this is safe. However, if multi-threaded test execution is introduced, this would become a subtle concurrency bug.I3: Portability -- Closure cell manipulation is CPython-specific
features/mocks/fast_init_test_helpers.py:87-91cell_contentsread/write on closure cells is a CPython implementation detail documented in the C API but not guaranteed by the Python language specification. This technique would fail on PyPy or GraalPython.I4: Security Scanner -- Hardcoded credentials in test fixture
features/steps/fast_init_upgrade_steps.py:160"postgresql://user:pass@localhost/testdb"contains cleartext credentials. While obviously a test-only placeholder (no real database is contacted), automated secret scanners (e.g., Gitleaks, Semgrep secrets, GitHub Advanced Security) may flag this as a credential leak.postgresql://placeholder:placeholder@localhost/testdbor a clearly synthetic pattern that scanners are less likely to flag.Summary
Overall assessment: The implementation is solid and correctly addresses all 4 acceptance criteria from issue #733. The prior review findings (mktemp, empty-DB gap, type suppression) were properly resolved. The 6 scenarios cover all meaningful code paths in
_fast_init_or_upgrade. The closure-cell mocking approach is creative, well-documented, and appropriate for the testing need. The findings above are refinement-level observations -- none block merge.@ -0,0 +37,4 @@Scenario: In-memory SQLite delegates to original init_or_upgradeGiven an in-memory SQLite database URL for fast-init testingWhen I call init_or_upgrade on the fast-init databaseThen the original init_or_upgrade should have been invoked exactly onceL1: Missing scenario for the
sqlite://bare in-memory URL variant. The source code atenvironment.py:459checksdb_url == "sqlite://"as an alternative to":memory:" in db_url, but only the:memory:form is tested here. Consider adding a 7th scenario.@ -0,0 +88,4 @@cell_ref: Any = cellsaved: Any = cell_ref.cell_contentsmock = MagicMock(name="_original_init_or_upgrade")cell_ref.cell_contents = mockI2+I3: This closure cell mutation is (1) not thread-safe -- concurrent callers hitting fallback paths during the
withwindow would invoke the MagicMock -- and (2) CPython-specific (cell_contentsis a C-level implementation detail). Both are acceptable for the current Behave sequential execution model targeting CPython, but worth documenting as assumptions.@ -0,0 +28,4 @@def _register_path_cleanup(context: Context, path: str) -> None:"""Schedule *path* (and its SQLite sidecar files) for removal."""if not hasattr(context, "_cleanup_handlers"):I1: This
hasattrguard is technically unreachable --before_scenario(environment.py:509) always initializescontext._cleanup_handlers = []before any step runs. Consistent with other step files but dead code.@ -0,0 +157,4 @@def step_create_nonsqlite_url(context: Context) -> None:"""Set up a non-SQLite URL (PostgreSQL placeholder)."""context.fast_init_db_path = Nonecontext.fast_init_db_url = "postgresql://user:pass@localhost/testdb"I4: Hardcoded
user:passcredentials may trigger automated secret scanners. Consider using a clearly synthetic pattern likepostgresql://placeholder:placeholder@localhost/testdb.@ -0,0 +180,4 @@with patch_original_init_or_upgrade() as mock_original:runner.init_or_upgrade()context.fast_init_mock_called = mock_original.calledcontext.fast_init_mock_call_count = mock_original.call_countL2+L3: The mock's
call_argsare not captured or verified. Consider storingmock_original.call_argson context so Then steps can assert thatself(the runner instance) was passed correctly, and that keyword arguments (e.g.,require_confirmation=True) are forwarded when provided.Independent Code Review Report -- PR #1156 / Issue #733
Reviewer: Independent automated review (3 iterative cycles)
Scope: Code changes in branch
test/m3-fast-init-early-return(commit056ca8f) plus close connections to surrounding codeCross-referenced: Issue #733 acceptance criteria,
docs/specification.md, prior reviews (#72933, #2957, #3054)Methodology
Performed 3 full review cycles across all categories (bugs, test coverage, test flaws, performance, security, code quality, documentation). Each cycle re-examined every category globally until no new findings emerged. All changed files were read in full; surrounding code (
features/environment.py:418-603,migration_runner.py:198-254) was traced to verify behavioral correctness of each scenario.Acceptance Criteria Verification (Issue #733)
_fast_init_or_upgradewith a non-empty DB_original_init_or_upgradeis not calledFindings by Severity and Category
LOW -- Test Coverage (1 finding)
L1: Missing
sqlite://bare in-memory URL variant scenariofeatures/fast_init_upgrade.feature(missing scenario)features/environment.py:459_fast_init_or_upgradeclosure checks two in-memory SQLite forms: Scenario 5 ("In-memory SQLite delegates") testssqlite:///:memory:but no scenario tests thesqlite://bare URL variant. This leaves one branch of the disjunction untested.sqlite://equality check is accidentally removed or altered, no test would catch the regression.Given a bare sqlite:// URL for fast-init testingthat setscontext.fast_init_db_url = "sqlite://"and asserts delegation to original.LOW -- Test Flaws (2 findings)
L2: Delegation scenarios don't verify arguments passed to mock
features/steps/fast_init_upgrade_steps.py:180-183mock_original.call_count == 1but never verify the mock was called with the correctselfargument. The delegation call in_fast_init_or_upgradeis_original_init_or_upgrade(self, **kwargs)-- theself(theMigrationRunnerinstance) is never validated.selfis lost or substituted in the delegation call would pass silently.mock_original.call_argson context and add a Then step asserting the runner instance was passed as the first positional argument.L3: No verification that
**kwargsare forwarded in delegation pathsfeatures/steps/fast_init_upgrade_steps.py:181runner.init_or_upgrade()is always called without arguments. The originalMigrationRunner.init_or_upgradeacceptsrequire_confirmation: boolandprompt_for_migration: Callable. The_fast_init_or_upgradeclosure uses**kwargsto forward these, but no scenario exercises this forwarding.**kwargsforwarding breaks (e.g., closure signature changes to drop**kwargs), no test catches it.call_args.kwargs.LOW -- Documentation (2 findings)
L4: Commit message claims "5 BDD scenarios" but feature file contains 6
056ca8fmessage bodyL5: CHANGELOG entry misleading about "replacing" mktemp
CHANGELOG.md, unreleased sectionmktempusage with private temp-directory allocation." However, this diff does not replace any existingmktempcalls -- the new code usesmkstemp/mkdtempfrom the start. The existingtempfile.mktempinenvironment.py:564(before_scenario) remains untouched.mkstemp/mkdtemp) throughout new fast-init test steps."INFO -- Code Quality (1 finding)
I1: Redundant
hasattrchecks in cleanup helpersfeatures/steps/fast_init_upgrade_steps.py:31,44_register_path_cleanupand_register_directory_cleanupguard withif not hasattr(context, "_cleanup_handlers"). However,before_scenario(environment.py:509) always initializescontext._cleanup_handlers = []before any step runs. The guard is dead code in normal execution.plan_lifecycle_cli_steps.py:102,config_three_scope_steps.py:85), so it is at least consistent with project conventions.INFO -- Thread Safety (1 finding)
I2: Class-level closure cell replacement without synchronization
features/mocks/fast_init_test_helpers.py:91patch_original_init_or_upgrade()mutates the_original_init_or_upgradeclosure cell on the class-levelMigrationRunner.init_or_upgradefunction. During thewithblock window, any concurrent thread hitting a fallback path would invoke theMagicMockinstead of the real migration runner.@mock_onlyfeatures skip DB-touching setup. Safe in the current execution model.INFO -- Portability (1 finding)
I3: Closure cell manipulation is CPython-specific
features/mocks/fast_init_test_helpers.py:87-91cell_contentsread/write is a CPython implementation detail. Would fail on PyPy or GraalPython.INFO -- Security Scanner (1 finding)
I4: Hardcoded credentials in test fixture
features/steps/fast_init_upgrade_steps.py:160"postgresql://user:pass@localhost/testdb"may trigger automated secret scanners (Gitleaks, Semgrep secrets, etc.) despite being a test-only placeholder.postgresql://placeholder:placeholder@localhost/testdbor a clearly synthetic pattern.INFO -- Documentation (1 finding, NEW)
I5: Surrounding code docstring inconsistency
features/environment.py:447(surrounding code, not changed by this PR)_fast_init_or_upgradedocstring says "In-memory SQLite ->Base.metadata.create_all()+ alembic stamp" but the actual implementation delegates to_original_init_or_upgrade. The test correctly verifies the actual behavior (delegation), not the documented behavior.Summary
Overall Assessment
The implementation correctly addresses all 4 acceptance criteria from issue #733. The 6 BDD scenarios cover all meaningful code paths in
_fast_init_or_upgrade(early return, template copy, empty-DB copy, non-matching prefix fallback, in-memory fallback, non-SQLite fallback). The closure-cell mocking approach infast_init_test_helpers.pyis creative, well-documented, and appropriate for the testing need.Prior review findings (insecure mktemp, missing empty-DB scenario, type suppression) were properly resolved in the current commit. The remaining findings are refinement-level observations. None of the findings block merge.
This independent review confirms the findings from the prior self-review (#3054) and adds one new informational item (I5). The PR has already been APPROVED by @freemo (#2957).
a52b54b4cae3703f953bNew commits pushed, approval review dismissed automatically according to repository settings
e3703f953bb21e0fedea