Skip to content

fix(engine): create the database's parent directory before connecting - #11

Merged
adbarc92 merged 2 commits into
mainfrom
fix/create-db-parent-dir
Jul 25, 2026
Merged

fix(engine): create the database's parent directory before connecting#11
adbarc92 merged 2 commits into
mainfrom
fix/create-db-parent-dir

Conversation

@adbarc92

@adbarc92 adbarc92 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

main is currently red, and a fresh clone of AppForge cannot start the engine. This fixes both with a one-line change plus the regression test that should have caught it.

The bug

Store.connect() called aiosqlite.connect(db_path) directly. sqlite does not create missing intermediate directories, and both default paths live under data/:

  • backend/engine/run.py:50data/engine.db
  • backend/main.py:99data/web.db

data/ is gitignored (.gitignore:273) and untracked, so it does not exist on a fresh clone or a CI runner. The engine dies with OperationalError: unable to open database file.

Evidence it is real

Symptom Where
test_start_project_emits_project_created fails — 1 failed, 151 passed CI backend (3.11) on main @ ae59b29
Web server throws OperationalError: unable to open repeatedly CI e2e on main @ 667e21f

A/B reproduction locally, with data/ moved aside:

WITHOUT fix, data/ absent:  1 failed, 2 passed
                            FAILED …::test_start_project_emits_project_created
                            AssertionError: expected project_created event
WITH fix,    data/ absent:  3 passed

Why the suite did not catch it

Two things compounded:

  1. Every store test builds its path under pytest's tmp_path — which pytest has already created. The missing-parent case was never exercised.
  2. Any developer who has run the engine once already has data/. Local runs pass for a reason that does not hold on a fresh checkout.

So the green local suite was accurate and still missed this. Only CI, running on a clean runner, saw it. The new test closes that gap by constructing the fresh-clone condition explicitly.

The fix

In Store.connect() — the single point every caller passes through — rather than patching the two db_path defaults independently:

parent = Path(self.db_path).parent
if str(parent) not in ("", "."):
    parent.mkdir(parents=True, exist_ok=True)

The guard matters: a bare filename like run.db yields a parent of ., and calling mkdir on it is pointless noise.

Tests

tests/engine/test_store_db_path.py covers three cases, and the file documents why the gap existed so it is not reintroduced:

  • missing parent directory (the exact data/ fresh-clone condition)
  • nested missing parents (a/b/c/run.db)
  • bare filename with no directory component (must not trip the mkdir)

Verified these fail without the fix — both directory cases raise the exact sqlite3.OperationalError: unable to open database file.

Verification

Full suite run with data/ removed, i.e. true CI conditions:

Check Result
uv run pytest tests/ (no data/) 155 passed (152 + 3 new)
coverage 86.96% (gate 70%)
uv run ruff check backend/ tests/ clean
uv run black --check backend/ tests/ clean, 67 files

README badge and docs/STATUS.md counts updated 152 → 155.

Scope note — corrected after CI ran

I originally expected e2e to stay red because its Playwright specs predate the engine rewrite. CI shows that was wrong, and the correction is worth stating plainly.

This fix did clear the unable to open database file error from the e2e web server. What it revealed underneath is a different failure:

RuntimeError: claim_next_task failed: Error executing tool claim_next_task: database is locked

So e2e remains red, but for a reason this PR neither causes nor fixes: the web bridge starts start_run(workers=4) against data/web.db, and under CI those four worker processes hit lock contention on claim_next_task.

I have not diagnosed the root cause and am not guessing at one here. Worth recording: a missing busy_timeout pragma is not the explanation, since Python's sqlite3.connect() already applies a 5s busy timeout that aiosqlite inherits. This warrants proper investigation as its own piece of work.

Effect of this PR on CI: backend (3.11), backend (3.12), frontend, and validate-config all pass — backend (3.11) being the job that was red on main. e2e was already red before this PR and stays red.

adbarc92 added 2 commits July 25, 2026 11:53
Store.connect() called aiosqlite.connect(db_path) directly. sqlite does
not create missing intermediate directories, and the default paths
(data/engine.db in run.py, data/web.db in main.py) live under data/,
which is gitignored and so absent on a fresh clone or CI runner. The
engine died with "unable to open database file".

This was red on main: CI backend (3.11) failed
test_start_project_emits_project_created, and the e2e job's web server
threw the same OperationalError on a loop.

It survived local verification for two compounding reasons: every store
test builds its path under pytest's tmp_path, which already exists, and
any developer who has run the engine once already has a data/ directory.
Confirmed by A/B with data/ removed -- the CI failure reproduces exactly
without this change and passes with it.

Fixed in Store.connect() so every caller benefits, rather than at the two
db_path defaults. tests/engine/test_store_db_path.py covers the missing
parent, nested parents, and a bare filename with no directory component
(which must not trip the mkdir).

Backend suite 152 -> 155; full suite verified with data/ absent.
… pointer

CI on this PR showed the e2e job now fails with
'claim_next_task failed: database is locked' rather than the
missing-directory error -- a second, undiagnosed defect that the first
one was masking. Logged as a known gap and promoted to the top next
step, with the busy_timeout explanation explicitly ruled out.

Also: the state summary still pointed at publication-prep, a branch that
no longer exists; black file count 66 -> 67.
@adbarc92
adbarc92 merged commit 74cadcf into main Jul 25, 2026
4 of 5 checks passed
@adbarc92
adbarc92 deleted the fix/create-db-parent-dir branch July 25, 2026 18:01
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