setBaseUrl() and setSource() used to fire refresh() inline as soon as
both `baseUrl` and `source` were populated — but setToken() never
triggered a refresh. QML evaluates literal property assignments before
bindings to other objects' properties, so a model declared with
literal `source` plus bindings to `BackendConnection.url` /
`BackendConnection.token` (the exact shape of make:bridge:window's
output) could fire its GET *before* the `token` binding had landed.
The unauthenticated request hit Symfony's SessionAuthenticator, came
back 401, and the model parked at `ready === false` with an empty
list. Mercure subscribed anonymously (the model explicitly clears the
SSE client's bearer), so subsequent server-side mutations propagated
fine — masking the initial-fetch failure as "list is empty until
something changes". Hit by the second window in examples/todo.
Both classes now implement QQmlParserStatus and trigger the initial
refresh from componentComplete(), where every binding (literal *and*
singleton-derived) is guaranteed to have landed. After completion,
individual setter changes still trigger refresh inline — so token
rotation / URL reassignment after first load behave unchanged.
Regression test under framework/qml/tests/tst_reactive_list_model.qml
using the v0.2.0 qmltestrunner harness. Adds a TestHttpServer helper
that mimics SessionAuthenticator's 401-on-no-bearer behaviour so the
regression is observable; verified the test fails against the unfixed
production code (`Actual: ""` vs `Expected: "Bearer testtoken"` on
the captured Authorization header).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the testing-strategy row of PLAN.md §13 v0.2.0 and parks the
two remaining items with rationales.
Shipped:
- framework/qml/tests/{CMakeLists.txt, main.cpp, tst_smoke.qml}
Qt Quick Test scaffold: QUICK_TEST_MAIN bootstrap + one smoke test
proving the harness loads. New tests land as tst_<feature>.qml in
the same dir; qmltestrunner auto-discovers them. Built only when
-DBUILD_TESTING=ON (production AppImages stay clean).
- skeleton + example/todo Makefiles: `make qmltest` target invokes
the configure → build → ctest dance. `make quality` now depends
on qmltest.
- .gitea/workflows/ci.yml: `QML unit tests` step after qmllint in
the Quality job. Out-of-tree build dir (build-tests) so the
CTest run doesn't pollute the cached release build.
Verified locally: configure + build + ctest pass, both smoke
assertions pass, runs in 0.5s.
Closed in PLAN.md §13 v0.2.0 with rationale (no code change):
- Build-time Symfony cache warmup → moved to v0.3.0. The obvious
approach (cache:warmup at build, copy at first launch) doesn't
save any time because Symfony bakes absolute kernel.project_dir
into the compiled cache, and the AppImage's FUSE mount path
changes every launch — every cached path is stale on launch N+1.
Doing it properly requires virtualising getProjectDir(), symlink
fix-up, multi-app namespacing — its own minor's worth of design.
- ReactiveObject cursor pagination → closed N/A. ReactiveObject
already has pending / invoke() / Idempotency-Key correlation /
version-gap detection at parity with ReactiveListModel; the only
feature it lacks is *pagination*, which is meaningless for a
single-entity model.
That fully closes the v0.2.0 plan as documented. Remaining v0.2.0
items in PLAN.md §13 are the audit-ends already shipped earlier in
the cycle (interfaces / BridgeOp / BridgeBundleInfo / Maker DRY /
--with-dto / port negotiation / pre-migration backup / bridge:export
/ periodic auto-update / native-dialogs doc / event maker /
read-model maker / qmltestrunner) plus the two parked items
documented above. Ready to tag when the user gives the word.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>