ci: turn Newman on — it has never run, and its seed could not fail - #156
ci: turn Newman on — it has never run, and its seed could not fail#156rubenvdlinde wants to merge 17 commits into
Conversation
`enable-newman: false` makes the job report `skipped`, which is
indistinguishable from a pass in the run summary. Eighteen collections in
tests/integration/ have never been executed by CI once.
The flag carried two named blockers. Neither was re-measured after
`database: pgsql` landed:
1. `React\\Async\\await()` undefined — `react/async ^4.3` is a require in
openregister@development's composer.json and is present in its
committed composer.lock, so the CI install resolves it.
2. SQLite lacking REGEXP — dead, the database input is pgsql.
Separately, the seed could not report failure at all. The shared workflow
interpolated newman-seed-command into an unquoted eval with no status
check, and this repo is the one caller passing an && chain
(`app:disable && app:enable`): the outer shell split it, eval ran only the
disable, and the && list returned 0 regardless. Fixed upstream in
ConductionNL/.github#212, so a verdict here now means something.
Refs openbuild#151
The honest Newman number, measuredRun 31316691991, job Both documented blockers are dead
And it passed through the fixed seed path — The collections: 80 failed assertions of 242, 14 of 18 collections red
Zero iterations failed anywhere — every collection ran to completion. These are assertion failures against a live server, not infrastructure failures. Causes visible in the server logThree distinct classes, none of them "Newman is broken":
What I am asking forThis PR turns the measurement on; it does not claim the measurement is green. The 80 are now visible, attributable and countable instead of hidden behind a Refs openbuild#151 |
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-09 14:27 UTC
Download the full PDF report from the workflow artifacts.
…ts as data
Two collection defects — both "wrong from the start", neither an app bug.
Together they are the root of most of the 78 failing assertions, because a
failed lookup leaves a collection variable empty and every downstream URL
then interpolates nothing.
1. `?filter[slug]=x` IS NOT A FILTER. OpenRegister reads `filter` as an
ordinary property name, looks for objects whose `filter` property equals
`{slug: x}`, and finds none. It says so in its own response:
"ignoredFilters": ["filter"],
"hint": "Query returned 0 results because filter was treated as a
property filter. Did you mean _filter? Control params require
underscore prefix."
Measured against a live instance (read-only GETs, status printed):
?filter[slug]=hello-world -> 200, total=0 <- what we were sending
?_filter[slug]=hello-world -> 200, total=30 <- unfiltered, ALSO wrong
?slug=hello-world -> 200, total=1 <- correct
Note the middle row: the underscore form the hint suggests returns the
WHOLE register. page-editor was using exactly that, so it was silently
reading someone else's object rather than failing.
2. ApplicationVersion HAS NO `applicationUuid` PROPERTY, and no `version`
one either. Read from the schema in lib/Settings/openbuild_register.json,
its properties are: name, slug, manifest, manifestDelta, baseRef, register,
semver, status, application, promotesTo, scope, owner, commitSha,
sourceRef. The relation is `application`; the version string is `semver`.
The collection filtered on `applicationUuid` and matched rows on
`r.version` — an API shape that has never existed.
Fixed in BOTH `raw` and `query[]`. Newman resolves the request from
`query[]`, so a raw-only fix would look done and change nothing.
Not asserted-away: no assertion was weakened, no test skipped. The requests
now address the API the app actually exposes.
Scope of this commit is deliberately just these two causes, so the next
Newman run measures their effect on its own.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:02 UTC
Download the full PDF report from the workflow artifacts.
The filter fix landed and the request is now correct — the log shows `?slug=hello-world` returning 200 with a body the size of a ONE-result response (~1.34kB in CI; measured locally: 1-result=1287B, 0-result=380B). The client-side re-check still finds nothing, so the row is present and the predicate is looking in the wrong place. Rather than guess the shape from outside, log it once when the re-check disagrees with the server-side filter. The assertion is unchanged.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:10 UTC
Download the full PDF report from the workflow artifacts.
…the app
The diagnostic answered it: in CI the lookup returns `rows=0`. My earlier
inference from response SIZE ("~1.34kB matches a 1-result body, so the row is
there") was wrong — worth saying plainly, because it is exactly the kind of
measurement that reads like evidence and is not. Logging the shape was what
settled it.
So `?slug=` works on my dev instance (200, total=1, verified against the live
API) and returns 0 in CI. The two run different OpenRegister builds — CI
installs `openregister@development` — and the property-filter dialect is not
the same in both. A lookup whose correctness depends on which OR build is
installed is not a lookup this suite should be making.
0.2 now fetches the register unfiltered (`_limit=100`) and finds hello-world
CLIENT-SIDE, which behaves identically on every OR build. The purpose of this
request is to locate the seeded Application, not to exercise OR's query
dialect — that belongs in OpenRegister's own suite, not here.
The assertion is unchanged; the diagnostic now prints the returned slugs and
`ignoredFilters` instead of a blob that the log truncated at 20 characters.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:16 UTC
Download the full PDF report from the workflow artifacts.
…cated rows=3, total=3: the unfiltered fetch works and CI has three Applications, none of which the predicate matches on slug. The previous diagnostic was cut off by the log before it printed them, so split it into one short line per row.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:22 UTC
Download the full PDF report from the workflow artifacts.
THE KEYSTONE DEFECT, and it is the shape this programme exists to find: a
seed step that is green for a true reason while leaving the instance unseeded.
`newman-seed-command` was:
php occ app:disable openbuild && php occ app:enable openbuild
Nextcloud runs repair steps on INSTALL and UPGRADE, not on disable/enable of
an app that is already installed. So the hello-world fixture was never
created, and the dedicated `openbuild:seed-hello-world-fixture` occ command
was never invoked at all. Disabling and re-enabling an app genuinely succeeds,
so the step reported success honestly — it just was not doing the work.
MEASURED, not inferred. I had first "confirmed" from response byte size that
the row was present; that was wrong. Logging the register contents from inside
the suite settled it — the three Applications in CI were:
newman-wizard-single, newman-wizard-dsp, newman-permits
every one created by the collections themselves during the run. There was no
`hello-world` at all.
Four collections open by locating it (versioning, application-versions, rbac,
page-editor). The lookup returned nothing, `application_uuid` stayed empty,
and every later URL in those collections interpolated an empty path segment —
which is where the `//`, trailing-`/` and `null` 404s in the failure list come
from. They are one cause wearing many hats.
The fix is to use the seed PLAYWRIGHT has been using all along — the same
script, now referenced by both inputs. It really does the work: ci-seed.sh
runs `php occ openbuild:seed-hello-world-fixture`. It is `set -euo pipefail`,
the shared workflow checks its exit status, and it refuses to run outside CI
without an explicit base URL, so it stays ABLE TO FAIL — there is no `|| true`
anywhere in that path.
Verified both seed steps in the shared workflow `cd server` first, so the
identical relative path is valid for Newman as it already is for Playwright.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:28 UTC
Download the full PDF report from the workflow artifacts.
The real seed ran and did the work — the log shows it:
[ci-seed] occ openbuild:seed-hello-world-fixture
Seeded hello-world fixture (application b6cef6b9-...).
[ci-seed] hello-world manifest -> HTTP 200
and then failed at the very end on the SPA bundle gate, which the Newman job
cannot satisfy: it never runs the frontend build and never loads the SPA. It
calls the REST API and asserts on JSON.
That the seed FAILED here is the point — the previous one could not. It is now
doing enough work to be wrong about.
`SEED_REQUIRE_BUNDLE` defaults to 1, so Playwright's gate is untouched and
stays exactly as able to fail as before; proven with a positive control, both
directions:
default (unset) -> gate fires, exit 1
SEED_REQUIRE_BUNDLE=0 -> announced skip, exit 0
The skip is ANNOUNCED in the script's own output ("SPA bundle NOT verified by
this run"), because a gate that quietly stops checking is the exact failure
this file was written to catch. Only the browser artefact is unchecked for the
API suite; every API fixture it depends on is still seeded, and the seed still
fails loudly if any of that goes wrong.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:35 UTC
Download the full PDF report from the workflow artifacts.
…ab-case
The collections addressed `objects/openbuild/application-version`. That schema
does not exist. Measured against a live OpenRegister:
.../objects/openbuild/application-version -> total=0
.../objects/openbuild/applicationVersion -> total=119
(CI is harsher and 404s the wrong slug outright, which is what the failure
list was showing.) The seed's own output lists the registered schemas and
`applicationVersion` is right there among them.
Six URLs in openbuild-versioning fixed, in both `raw` and `path[]`. The one
remaining match for the old string is the collection's `_postman_id`, which is
an identifier, not a URL — left alone deliberately.
Also corrected a comment I had written one commit earlier that said the
request was "filtered server-side". It is not: it now fetches unfiltered and
matches client-side, precisely so the suite does not depend on which
OpenRegister build is installed. A stale comment that describes the opposite
of what the code does is its own small dead check.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:41 UTC
Download the full PDF report from the workflow artifacts.
0.3 now returns 200 with rows, but the v1.0.0 match still misses. The seed links the version with `'application' => $applicationUuid` and `semver => '1.0.0'`, so the predicate's SHAPE is right — the identifier it compares against is the open question. The Application object exposes `id`, `uuid` and `@self.id`, and betting on one of them is what produces a failure that reads as 'the seed did not run' — the exact misdiagnosis this suite already caused once. Capture both candidates and accept either, and log the rows on a miss so the next run names the real linkage instead of inviting another guess.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 04:48 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-10 05:29 UTC
Download the full PDF report from the workflow artifacts.
… never anonymous
Continues this branch's burn-down. Baseline for these numbers is run
31356594486 (45 failing assertions across 13 collections).
## 1. Five "unauthenticated" probes were logged-in requests (6 assertions)
`auth: { type: noauth }` removes the Authorization header. It does NOT
remove the SESSION COOKIE Newman already holds for that origin.
Measured with `newman --verbose`, one origin apart, same request:
auth, {{base_url}} -> 324B up, 9 headers (Authorization, no Cookie)
noauth, {{base_url}} -> 576B up, 9 headers (no Authorization, COOKIE)
noauth, {{noAuthBase}} -> 285B up, 8 headers (neither — truly anonymous)
The header COUNT is the tell: 9 vs 8. The middle row is the shape four
collections were using.
The consequence is visible in the CI run order. In
openbuild-application-versions the FIRST anonymous probe returned 401, and
every anonymous probe AFTER the first authenticated write returned 412
"CSRF check failed" — because a request carrying a logged-in session with no
requesttoken is not anonymous, it is a logged-in request that fails CSRF.
Nextcloud's 401 fires before the CSRF check, so a 412 is proof the probe was
authenticated.
openbuild-icon-endpoints is the same defect wearing the opposite face: its
probe returned 200 and was on its way to being read as an authorization
hole. It was the admin, correctly served an icon.
Fixed the way this repo already solved it once:
openbuild-api-contract.postman_collection.json reaches the same server by
its other name (`noAuthBase = http://127.0.0.1:8080`), so the cookie jar —
which is keyed by origin — has nothing for it. All five of its anonymous
assertions passed in the very run these failed.
Each assertion now demands 401 EXACTLY and names the trap when it sees a
412/200, so a future regression reports its own cause.
Rejected: `pm.cookies.jar().clear()`. It reports success — and does nothing.
`getAll()` returns an EMPTY list for these origins, so the clear "succeeds"
over a view that never had the cookies, while `--verbose` shows the upload
size unchanged at 576B with the Cookie still attached. A control that passes
because it cannot see its subject.
## 2. PRODUCT: export submit 500s on a malformed applicationVersion (4 assertions)
`exportJob.applicationVersion` is declared in openbuild_register.json as a
semver, and ExportDialog.vue sends one. ExportsController checked only that
it was non-empty, so any other string passed validation and died three
layers down, where OpenRegister refused the record and
ExportJobService::queue() rethrew — HTTP 500, with nothing telling the
caller which field was wrong. `target` one block up was already validated
exactly this way.
Now a 422 naming the field. All four failing assertions were in ONE request,
one cause. The collection was ALSO wrong — it sent the version SLUG
"production" — so it now sends a real semver, and a new request keeps a bad
one on purpose so the 4xx stays proven instead of assumed.
## 3. PRODUCT: diffVersions turns a 404 into a 500 (2 of 11)
`resolveVersionBlob()` says "Returns null on miss so the caller can surface
404", and diffVersions() has two `if (...=== null) return 404` branches.
Neither could ever be taken: `ObjectService::find()` THROWS on a miss, so
the throw went past them into the outer `catch (Throwable)` and came back as
500 `internal_error`.
This is the eighth instance of the family PR #159 fixed. It was not among
those seven because gate-49 only flags an untranslated lookup OUTSIDE a
try/catch — and being inside one is exactly what converted a precise 404
into an opaque 500 on a #[NoAdminRequired] endpoint. An empty token is now
answered as a miss instead of being asked of the object store at all.
## 4. openbuild-versioning: a filter that CANNOT match, and a PATCH that isn't (11)
Two causes, both measured against a live instance:
applicationVersion?_limit=5000 -> 119 rows
applicationVersion?semver=1.0.0 -> 20 rows (scalar prop: filters)
applicationVersion?application=<uuid> -> 0 rows (relation: does NOT)
The third used a uuid taken straight out of row 1 of the first, so the value
was demonstrably present. OpenRegister does not apply a query-string data
filter to an x-openregister-relation. Three list reads used it and got zero
— which looks exactly like "the seed never ran", the misdiagnosis this
suite's own comments say it already made once. Step 0.2 had already learned
to fetch unfiltered and match in-script; 0.3, 1.2 and 3.2 now do too.
The three PUTs sent {version, status, manifest} only. OR's saveObject() is
PUT-semantic and nulls every omitted property; the Application schema
declares `required: ['slug', 'name']`; so OR refused the write with 400 and
said nothing about slug or name. The full record is now captured in 0.2 and
merged, and 1.1 additionally asserts slug/name SURVIVED — without that,
trimming the merge back would break somewhere else and this request would
still look fine.
Same trap as SeedHelloWorldFixture.php's "permissions IS REPEATED HERE ON
PURPOSE" and appRoles.ts. Third time it has cost something.
phpcs clean on both changed PHP files.
…nt "null"
## openbuild-component-blocks (5 assertions, one cause)
OpenRegister returns an object's identity in the `@self` envelope. The
create step asserted `to.have.property('uuid')` and stashed `body.uuid` —
undefined. Postman interpolates an undefined variable as the literal string
`null`, so the next three requests went to `/component-block/null` and 404'd.
Four of the five failures were in requests with nothing wrong with them.
⚠️ And one assertion in this collection was GREEN over the same two broken
values: "imported record gets a NEW uuid, distinct from the source block"
compared `body.uuid` (undefined) with `block_uuid` (the string 'null'),
found them unequal, and passed — while neither side was a uuid at all.
Both sides must now BE identifiers before they are compared, and a missing
identifier fails at the create step, where the cause is, instead of as a 404
on a URL three requests later.
## openbuild.postman_collection (4 assertions, two causes)
1. `strategy=hard` is not a strategy. ApplicationVersionsController accepts
exactly `delete-now | orphan-grace | keep-register` and answers anything
else 400 `invalid_strategy`. The assertion was right; the URL was wrong
from the day it was written.
2. The round-trip never registered a BuiltAppRoute.
`resolveApplicationBySlug()` does not search Applications by slug — it
resolves the slug through the `built-app-route` schema. So a published
Application with a productionVersion pointer is STILL unreachable by
slug, and `GET /api/applications/newman-roundtrip/manifest` 404'd after
every preceding step had succeeded (201, 201, 200, 200). Three assertions
failed and the cause was in none of them.
This folder deliberately drives the raw OR object API (ADR-022) instead
of the wizard, and the wizard is what normally creates that route — so
the round-trip has to create it itself, exactly as
SeedHelloWorldFixture.php does in its step 4. Torn down again afterwards:
a leftover route makes the next run's slug AMBIGUOUS, and an ambiguous
set 404s on a run where everything else is correct.
The six assertions still failing after the previous commit turn out to demand the exact opposite of the specification. openspec/specs/openbuild-version-snapshots/spec.md (status: done) says: REQ-OBV-002: "The system SHALL NOT spawn sibling ApplicationVersion rows on draft -> published transitions. Snapshot-on-publish writeback is retired under ADR-002." "The system SHALL NOT subscribe any PHP listener (ApplicationVersionSnapshotListener or any successor)..." Scenario: "Publishing does NOT create a sibling ApplicationVersion" REQ-OBV-003: "restoring a previous state of an ApplicationVersion MUST NOT be implemented by copying from a sibling snapshot row" The collection required "the listener must have appended a v1.1.0 row", "at least three ApplicationVersion rows after rollback" and "a rollback row like 1.0.0-rb1". The only way any of those could go green is for the product to REGRESS to an architecture the spec explicitly forbids. And the listener does not exist. lib/Listener/ holds six listeners, none of them it; the surviving references are an archived change proposal, two PHPUnit files and this collection. It was written against `openbuild-versioning` (archived 2026-05-12) and never updated when `openbuild-version-snapshots` superseded it. Inverted to assert REQ-OBV-002/003 as written, NON-VACUOUSLY: the row count is captured before the publish and asserted UNCHANGED, and the before-count is separately required to be >= 1 — otherwise a broken list read leaves both sides at zero and "unchanged" passes while measuring nothing. LEFT RED ON PURPOSE: the three REQ-OBV-005 diff assertions. The 500 they used to get was a product defect and is fixed (that lookup now answers 404). What remains is a spec-vs-controller divergence: REQ-OBV-005 defines the endpoint parameters as a version SLUG, `current:<slug>` or `history:<slug>:<rev>`, and resolveVersionBlob() implements none of that grammar — it accepts `draft` or a UUID. Rewriting the assertions to match todays behaviour would lock in the divergence and delete the only evidence of it, so it is documented in the folder description and filed instead.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 14:39 UTC
Download the full PDF report from the workflow artifacts.
Newman burn-down: 45 → 30 measured, three more batches pushedMeasured, not projectedJob 93487483023 (run 31398481286) reached
1. Five "unauthenticated" probes were logged-in requests — 6 assertions, all now green
The header count is the tell: 9 vs 8. In CI the consequence was visible in run order: the first anonymous probe returned 401, and every one after the first authenticated write returned 412 CSRF. Nextcloud's 401 fires before the CSRF check, so a 412 is proof the probe was authenticated. Fixed the way this repo already solved it once: Rejected: 2. PRODUCT — export submit 500s on a malformed
|
…pals
Two independent defects in the same three requests.
(1) The body was a bare {"permissions": {...}}. OR saveObject() is
PUT-semantic and nulls every schema property the body omits;
Application.required is [slug, name]; so OR refused the write with 400
and said nothing about slug or name. Three assertions failed on it.
(2) The principals were BARE — "admin", and the group NAME.
PermissionResolver::matchesCaller() classifies principals by a `user:`
/ `group:` PREFIX. That is the grammar SeedHelloWorldFixture.php writes
(owners: [user:admin]) and the one tests/e2e/support/appRoles.ts writes
(editors: [group:rbac-editors]). A bare value matches nothing, so even
a write that LANDED would have granted no one.
(2) matters because it is invisible: the write returns 200 and the object
comes back with a permissions block that simply grants nobody. So the
restored assertions now read the block BACK and require the prefixed
principals, rather than trusting the status code.
Still open on this collection: the three "outsider" assertions returned
BYTE-IDENTICAL responses for viewer, editor and outsider (1.94kB x3 on the
list, 1.1kB x3 on the manifest). That is the cookie-carryover shape this
branch already fixed for the anonymous probes — a session established by an
earlier authenticated request outliving the per-request basic auth — and if
it holds, this collection has never actually tested RBAC. Not asserted
either way here; it needs its own measurement.
Working notes on the collections I did not finishDiagnoses from the run-31356594486 log, so the next person does not re-derive them. None of these is asserted anywhere yet — they are read off the request/response lines, and each needs its own measurement before being acted on.
|
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 14:47 UTC
Download the full PDF report from the workflow artifacts.
…het 0.05% short
Run 31399299443's PHPUnit leg failed, and NOT on a test: `OK (797 tests,
2486 assertions)` followed by
FAIL: coverage dropped by 0.05% against the merge base.
That drop is mine. Both product fixes on this branch added branches with no
test behind them. The ratchet is doing exactly its job; the answer is tests,
not a baseline bump.
## 4 tests, each proven to fail, ONE mutation at a time
ExportsController — non-semver `applicationVersion`:
- planted `if (false && preg_match(...))` -> testSubmitReturns422For…
fails with "Failed asserting that 500 is identical to 422" — i.e. the
mutation reproduces the ORIGINAL DEFECT'S STATUS CODE, not just some red.
- restored -> 12/12 green.
- planted an OVER-strict `^\d+\.\d+\.\d+$` (no pre-release group) ->
testSubmitAcceptsSemverWithPreReleaseSuffix fails with "422 is not
identical to 422". Without this second test a guard that rejected
everything would have satisfied the first one while breaking every real
export.
- restored -> 12/12 green.
ApplicationsController::diffVersions — the unreachable 404:
- planted `if (false && $token === '')` -> testDiffVersionsReturns404For
AnEmptyToken fails.
- restored -> green.
- removed the try/catch translation entirely ->
testDiffVersionsReturns404WhenTheVersionLookupThrows fails with "Failed
asserting that 500 is identical to 404" — again the original symptom.
- restored -> green.
Full unit suite: 797 -> 801 tests, 2486 -> 2499 assertions, all passing.
## One thing the new test surfaced
`PermissionResolver::resolveUserGroups()` foreach-ed over null in my first
draft. That is NOT a product defect — `IGroupManager::getUserGroups()` is
declared to return an array and an unstubbed mock returns null — so the fix
is mock fidelity, and it is stubbed to `[]` deliberately: `bob` is authorised
as a `user:` principal in the owners list, so the verdict must not depend on
group membership. A stub returning a group named in `permissions` would have
let both tests pass without proving the owner grant works at all.
phpcs: both changed lib/ files are clean (phpcs.xml scopes `<file>lib</file>`,
so tests/ is out of its scope — as the 44 pre-existing findings in
ExportsControllerTest on a green baseline already showed).
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 14:56 UTC
Download the full PDF report from the workflow artifacts.
…irst
Measured after the previous batch: openbuild.postman went 4 -> 1, and the
one left was the version DELETE, now answering 422 rather than the old 400.
422 is CORRECT. ApplicationVersionsController::destroy()'s own docblock says
"Attempts to delete the parent Application's production version yield 422",
and this round-trip pointed the Application's productionVersion at exactly
that row three steps earlier. Deleting it first could never have worked; the
old `strategy=hard` 400 had been masking that all along, because an invalid
strategy is rejected BEFORE the guard is reached.
Two changes:
* The attempt is kept and turned into the assertion it should always have
been — deleting a production version is refused with 422 — plus a second
assertion on WHICH refusal it is. Without that, any 422 from any cause
would satisfy the first and read as "the guard works".
This matters beyond tidiness: openbuild-application-versions carries a
test named "DESTROY — reject production version deletion (422)" that
SKIPS ("production_version_slug not set — seed step not run"), so this
guard had no executing coverage anywhere in the suite.
* The real cleanup moves AFTER the Application delete, and goes through
OR's object API — openbuild's versions route resolves a version THROUGH
its parent Application, which by then no longer exists, so it could only
404. Ordering, not preference: the openbuild route is the right surface
while the app is alive, and it is exercised as such by the guard
assertion above.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-10 15:01 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-10 15:31 UTC
Download the full PDF report from the workflow artifacts.
Final measurement on this branch: 45 → 15Run 31401193088 (SHA
Seven collections fully green. Note the assertion count went 251 → 258: the fixes added regression guards rather than removing checks (a deliberate non-semver 422 probe, a permissions read-back, a production-delete guard, a slug/name-survived check).
|
The
|
Retraction: the "single cookie jar makes the outsider run as admin" premise does not reproduceI was about to fix I measured it before writing the fix, and the mechanism is not there. Nextcloud 34.0.0.12, Newman 6.2.2. Arm 1 — Newman, no jar clear, request-level Basic override (exactly the collection's shape)Arm 2 — curl control, decisiveSeeded a real jar from an admin Basic request to Then, reusing that jar:
The second row is the one that settles it: those admin cookies authenticate nobody. NC does not persist a reusable login session for Basic-auth API requests, so there is no admin identity for a later request to inherit — whether or not the client sends the cookies. The Basic header is the only thing deciding identity. So the collection's auth-override design is sound as written, and I have not pushed a jar-clear fix. Writing one, with a comment asserting a defect I could not reproduce, would have been inventing the finding. Scope of the retraction — please read before relying on it
What is worth doing insteadAn identity guard — a Blocker status for
|
Re-ran this against current
|
| link | verdict |
|---|---|
PermissionResolver::matchesCaller() :107 |
if ($permissions === []) return false; — fails closed on an empty block |
ApplicationsController::filterApplicationsByRole() :905-945 |
per-app matchesCaller(..., allowAdminBypass: false, roles: [owners, editors, viewers]); no role ⇒ skipped |
ApplicationsController::requirePermission() :1206-1247 |
same check, allowAdminBypass: false, then an explicit admin branch, then 403 openbuild.rbac.no_role |
getManifest() :216-220 |
requirePermission() is called before any branch that emits the manifest |
The fixture is not the problem either — 1.10's own assertions (which pass) prove the
permissions block reads back with correctly user:/group:-prefixed principals,
and the outsider is in none of them.
So the code cannot produce a 200 for a genuinely role-less caller, and yet a 200
came back. Something other than the guard answered.
The likely mechanism: a shared cookie jar, so every request runs as admin
Newman keeps one cookie jar for the whole run. The collection authenticates at
collection level as admin; Nextcloud issues a session cookie; the per-request
basic auth on 2.3 / 3.1 then competes with a session that is already established.
The corroboration is in this run's own output, in a different collection:
expected { message: 'CSRF check failed' } to be an array
Nextcloud only enforces CSRF when a session cookie is present — a pure
basic-auth request never trips it. That error is therefore direct evidence that a
session cookie is in play across these collections.
🔑 And this inverts the positive control. If every request executes as admin,
then:
- 2.1 "Viewer sees the test app" and 2.2 "Editor sees the test app" pass for
the wrong reason —adminsees the app through the audited bypass regardless of
role; - 2.3 and 3.1 fail for the wrong reason, for the same reason.
That is the whole RBAC collection measuring one principal. It is the shillinq lesson
turned around: a suite that runs every request as admin cannot fail — or pass — a
membership check on its merits.
What I did not do
I did not file an IDOR issue. This repo already has one open claim of that shape
(#171) that was refuted after measurement, and filing a second unverified one is
worse than filing none. The evidence I have is a failing assertion contradicted by
four hand-read guards.
The decisive experiment, for whoever picks this up
- Pin the identity per request — clear the cookie jar between principals
(newman --cookie-jarper identity, or an explicitCookie:reset), then re-run.
If 2.3 and 3.1 go green, they were fixture artifacts and the collection was never
testing RBAC. - If they stay red with a provably role-less identity, it is a real IDOR and
should be filed immediately — at that point the guards would have to be
bypassed by something upstream of the controller. - Either way, assert the effective UID inside each RBAC request (e.g. a
/ocs/v2.php/cloud/userprobe) so the collection proves which principal it ran
as. Right now nothing in it does, which is why this is ambiguous at all.
The other 13, briefly
Independent of the identity question, and each worth its own look:
generatewith an unknowntemplateIdreturns 500, twice (ExportsController)
— an unknown id should be a 4xx.PUTan invalid manifest returns 200 where a 4xx is expected.GET .../hello-world/manifestdoes not reflect a newly-added page.- Version
diffendpoint returns 404. createFromTemplatefor a different user with the same slug returns 409
slug_collision where the collection expects cross-user reuse — that one may well
be the test's model being wrong rather than the product's.approvepass-through returns 405, not the expected 404.
Suggestion
Land the CI wiring and the collections, but keep the Newman job non-blocking
(continue-on-error) in the same change, with an issue tracking the 15 assertions
down to zero. That gets the collections running and visible without parking a
permanently-red required cell on development — and it avoids the alternative
temptation, which is deleting assertions until it goes green.
Not merged in this sweep for that reason.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-12 07:47 UTC
Download the full PDF report from the workflow artifacts.
What this measures
enable-newman: falsemakes theIntegration Tests (Newman)job reportskipped, and a skipped job is indistinguishable from a passing one in the run summary. Eighteen Postman collections sit intests/integration/and CI has never executed one of them. This PR turns the job on so it produces a verdict.The seed could not fail, so "it would just be red anyway" was never measured
The shared workflow interpolated
newman-seed-commandinto an unquotedevalwith no status check. This repository is the one caller in the fleet that passes an&&chain:The outer shell split that before
evalever saw it,evalran only thedisable, and bash'sset -eexemption for&&lists returned 0 regardless. A failed disable meant the enable never ran and Newman would have tested whatever state was left — green.Fixed upstream in ConductionNL/.github#212: the seed now reaches
evalas one quoted unit and its combined status is the step's, with an::error::and a hard exit when it fails. So a red Newman leg here is now a genuine finding, and so is a green one.The two blockers the flag carried, re-measured 2026-08-09
SeedHelloWorld failed: Call to undefined function React\Async\await()— OR's runtime-schema-API pullsreact/asyncbut it wasn't surfacing in the CI installreact/async ^4.3is arequireinopenregister@development'scomposer.jsonand is present in its committedcomposer.lock. The CI install resolves it.[PermissionHandler] no such function: REGEXPfrom OR's MagicMapper under SQLitedatabase:is nowpgsql, and that switch was made for exactly this family of failure. Newman reads the samedatabaseinput.Neither was re-measured after
database: pgsqllanded. This change makes CI answer the question instead of the comment answering it.Honest expectation
I am not claiming this will be green. I am claiming the number it produces will be real for the first time. If it is red, the failures are a finding to act on, not a regression this PR introduced — the leg reported
skippedbefore, which was never evidence of anything.Refs openbuild#151