jobs.spec.ts:169 — "the reconcile job flags drift when the installed version leaves the pin" — fails on CI and only on CI. The product code is correct; this issue records the evidence so nobody re-derives it.
The drift path works
Reproduced locally against a faithful reconstruction of CI's setup — the fixture forge (tests/e2e/fixtures/forge/server.mjs on :9099, artifacts built with build-artifacts.sh), versioniq's own installer, identical pin state:
occ versioniq:install fixtureapp 1.0.1 --source=codeberg:fixtureowner/fixtureapp
occ config:app:set versioniq pin.fixtureapp --value '{"version":"1.0.0",...}'
occ background-job:execute <live job id> --force-execute
Result:
{"version":"1.0.0",...,"driftedTo":"1.0.1","driftedAt":"2026-08-28T05:10:50+00:00"}
PinDriftHandler, PinStore::markDrift() and PinReconcileJob are all correct. Verified across three scenarios:
| scenario |
result |
| registered app (versioniq 1.4.1, pinned 1.0.0) |
driftedTo: "1.4.1" ✅ |
| unknown app (never installed) |
driftedTo: "0" ✅ (guard is === '', and getAppVersion() returns '0', not '') |
| fixture app via versioniq's installer — CI's exact case |
driftedTo: "1.0.1" ✅ |
All three version sources agree after an install: oc_appconfig.installed_version, the on-disk info.xml, and IAppManager::getAppVersion($id, false) all report 1.0.1.
Hypotheses eliminated
- Orphaned job row — real, and fixed in this PR, but not CI's cause: the anchored
runJob does not throw on CI, so a live OCA\Versioniq\… row exists there and is what executes.
getAppVersion() returning '' for an unknown app — it returns '0', so the === '' guard never fires and that path does record drift.
- App-id mismatch —
Application::APP_ID, info.xml <id>, and the value the test writes to are all versioniq.
- Config read path —
appConfigValue() reads app versioniq, the same store PinStore::all() enumerates.
What CI's signature means
On CI, pin.driftedTo is absent entirely — not "0", not a stale value. Given the preconditions now asserted in the test all pass there (pin readable as 1.0.0, app installed, versions differ), the only remaining explanation is that PinStore::all() does not return fixtureapp on that instance, so handle() is never called for it. That is instance state, not code.
Next step for whoever picks this up
Needs a CI run with the job's own view dumped — specifically what PinStore::all() returns on the runner. That cannot be determined from outside the instance, which is why this is excluded rather than guessed at.
Related: this investigation produced two real fixes in the same PR — the anchored runJob, and RemoveRetiredCronJobs now clearing the five OCA\AppVersions\* rows the app-id rename orphaned (verified 5 → 0 on a live instance).
jobs.spec.ts:169— "the reconcile job flags drift when the installed version leaves the pin" — fails on CI and only on CI. The product code is correct; this issue records the evidence so nobody re-derives it.The drift path works
Reproduced locally against a faithful reconstruction of CI's setup — the fixture forge (
tests/e2e/fixtures/forge/server.mjson :9099, artifacts built withbuild-artifacts.sh), versioniq's own installer, identical pin state:Result:
{"version":"1.0.0",...,"driftedTo":"1.0.1","driftedAt":"2026-08-28T05:10:50+00:00"}PinDriftHandler,PinStore::markDrift()andPinReconcileJobare all correct. Verified across three scenarios:driftedTo: "1.4.1"✅driftedTo: "0"✅ (guard is=== '', andgetAppVersion()returns'0', not'')driftedTo: "1.0.1"✅All three version sources agree after an install:
oc_appconfig.installed_version, the on-diskinfo.xml, andIAppManager::getAppVersion($id, false)all report1.0.1.Hypotheses eliminated
runJobdoes not throw on CI, so a liveOCA\Versioniq\…row exists there and is what executes.getAppVersion()returning''for an unknown app — it returns'0', so the=== ''guard never fires and that path does record drift.Application::APP_ID,info.xml<id>, and the value the test writes to are allversioniq.appConfigValue()reads appversioniq, the same storePinStore::all()enumerates.What CI's signature means
On CI,
pin.driftedTois absent entirely — not"0", not a stale value. Given the preconditions now asserted in the test all pass there (pin readable as1.0.0, app installed, versions differ), the only remaining explanation is thatPinStore::all()does not returnfixtureappon that instance, sohandle()is never called for it. That is instance state, not code.Next step for whoever picks this up
Needs a CI run with the job's own view dumped — specifically what
PinStore::all()returns on the runner. That cannot be determined from outside the instance, which is why this is excluded rather than guessed at.Related: this investigation produced two real fixes in the same PR — the anchored
runJob, andRemoveRetiredCronJobsnow clearing the fiveOCA\AppVersions\*rows the app-id rename orphaned (verified 5 → 0 on a live instance).