Skip to content

Release: merge development into beta - #1866

Merged
rubenvdlinde merged 87 commits into
betafrom
development
Sep 13, 2026
Merged

rubenvdlinde merged 87 commits into
betafrom
development

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

github-actions Bot and others added 4 commits September 6, 2026 12:46
…0260906124625

chore(sync): carry beta back into development
… dynamically

Nine call sites in the rule editor pass a VARIABLE to t(), so no static
scan can see them:

  ACTION_TYPES / TIMING_OPTIONS / ACTION_OPTIONS (views/Rule/ruleDraft.js)
    -> t('integriq', entry.label) in RuleActionConfig.vue,
       RuleDetailPage.vue and modals/v2/RuleEditorModal.vue
  OPERATORS (views/Rule/RuleConditionLeaf.vue)
    -> t('integriq', op.label) and t('integriq', op.group)
  the field rows in views/Rule/actionForms/{Approval,Authentication,
    Locking,WebhookSignature}Form.vue -> t('integriq', row.label)

DYNAMIC_KEYS exists for exactly this and was empty, on the stated grounds
that integriq reaches all its dynamic UI copy through src/manifest.json.
It does not, and the cost was two-sided across 69 strings:

  - 9 were in en.js, reported UNUSED, and offered up by clean:l10n for
    deletion. Deleting a live translation leaves the English source
    rendering correctly, so nothing would have failed.
  - 60 were not in en.js at all, which is why the entire rule editor
    renders untranslated in every locale: action types, condition
    operators ("does not equal", "greater than"), authentication methods,
    webhook signature schemes.

The 60 are added and all 69 are now claimed. Adding 60 keys did NOT raise
the unused count (670 -> 668), which is the control: without the claim it
would have gone to 730.

CLAUDE.md was wrong about the catalogues and that is what made this
awkward. l10n/*.js is GENERATED from l10n/*.json by build-l10n-js.js, and
check:l10n-js fails on drift, so they are not "separate catalogues, not
two renderings of one source". The practical consequence is that
scripts/l10n-ai.js is the wrong tool for adding a key: its help says it
never touches the JSON, so its writes are discarded by the next
l10n:build, silently. I added the 60 with it first and only noticed when
check:l10n-js reported a stale browser catalogue. The keys now go into
en.json and l10n:build regenerates en.js.

Verified: check:l10n MISSING 0, UNUSED 670 -> 668; test:l10n exit 0;
check:l10n-js exit 0; clean:l10n no longer offers "Save object",
"Approval", "does not equal" or "Lock resource"; all 77 applicable hydra
gates green with the runner CI resolves, and all 77 ran.

Dutch values are deliberately absent: en is required, other locales are
optional, and an absent value falls back to the English source while
staying visible on the to-translate list. A value equal to its key would
not.
fix(l10n): claim the rule editor's option lists, which are translated dynamically
rubenvdlinde and others added 19 commits September 7, 2026 09:40
CLAUDE.md said there was "no scanner for the backend set" and that
auditing it "would mean walking lib/ for PHP $l->t() calls". This is that
walk.

It matters because the two catalogues are not independent:
l10n/<locale>.js is GENERATED from l10n/<locale>.json, and the JSON is
what PHP IL10N reads. A string only the backend translates therefore
appears in en.js legitimately, and the frontend scan calling it "unused"
is a false positive rather than a finding.

Measured 2026-09-07: 409 distinct strings reach PHP ->t() in lib/, and 71
of them were reported as UNUSED frontend keys and offered up by
clean:l10n for deletion. Among them "Account is disabled", "Commit
contracts" and "Apply a mapping". Deleting one removes the backend's
translation and leaves the English source rendering correctly, so nothing
would have failed and nobody would have noticed.

check:l10n and clean-l10n now union collectBackendKeys() into the set
they already build from collectDynamicKeys(), which is the same idea for
the frontend: keys no scan of src/ can see.

The matcher is deliberately generous, treating any object's ->t() with a
literal first argument as a translation. A false positive here only keeps
a key alive, which is the safe direction; a false negative deletes a live
translation.

Verified: UNUSED 668 -> 596, MISSING still 0, test:l10n exit 0,
clean:l10n no longer proposes the three named above, eslint and prettier
clean.

What is left is a real number for the first time: 596 keys that neither
the frontend nor the backend references.
feat(l10n): scan the backend catalogue, which nothing was auditing
`<p v-if="schemasTemplateLoading">Loading...</p>` rendered raw, so it
said Loading in every locale. It is the one live-prose hit from
cross-checking find:unwrapped against the unused-key set, which is the
check the app's own notes ask for before removing anything.

Uses the ellipsis character, which @nextcloud/l10n-enforce-ellipsis
requires and which is already a key. Wrapping the triple-dot spelling
tripped the rule and, because the file carries an eslint suppression
count, surfaced a second pre-existing violation at line 1579 along with
it: the suppression absorbs a fixed number, so one new error reveals the
others.

'Loading...' with three dots is now correctly dead.
…ying

check:l10n reported 674 unused keys when this work started and now
reports none. The count was never the problem; the fact that it could not
be trusted was.

Three classes of false positive were removed first, each in its own
commit, because deleting before that would have taken live translations
with it:

  - the manifest strings collectDynamicKeys did not claim
  - the rule editor's option lists, reached through a variable
  - the 409 strings PHP ->t() translates, which nothing audited

Then the documented cross-check: find:unwrapped against what remained.
It found exactly one live string, "Loading...", which is now wrapped and
uses the ellipsis character the lint rule wants.

The remaining 595 are referenced by neither the frontend nor the backend.
They are removed from all 37 locale catalogues, 19,594 entries, and the
.js files are regenerated from the .json, which is the source.

GATE-102 CAUGHT EIGHT I WOULD HAVE LOST. Walkthrough copy, four column
labels and a header action are manifest strings in fields
collectDynamicKeys did not harvest: it took `pages[].title` and report
card labels, not `body`, `task` or a column's `label`. The gate reads a
wider set than the harvester did, and only the gate noticed. They are
restored with Dutch values, and collectDynamicKeys now harvests the SAME
visible-field set gate-102 checks, so the two cannot disagree again.
Field-scoped rather than every string: `observability.metrics[].name`
holds Prometheus identifiers and stays out.

Verified: check:l10n exit 0 and "all clean" for the first time;
MISSING 0; test:l10n exit 0; check:l10n-js exit 0; all 79 applicable
hydra gates green with the runner CI resolves, and all 79 ran.
test:l10n:parity still fails, as it did before, and slightly less: 169
missing became 164, because there are fewer English keys to translate.
chore(l10n): remove 595 keys nothing references, and stop the audit lying
CI caught what I missed. The 595-key sweep removed 55 strings that a
shipped register descriptor declares: schema titles, property titles and
helper text, rendered by OpenRegister's form renderer. They reach the
user through neither a t() call in src/ nor PHP ->t(), so both existing
collectors missed them, and check:schema-l10n was the only thing that
noticed - on CI, because it is not part of check:strict and I never ran
it locally. It reported 1166 uncovered against a baseline of 1120.

The 55 are restored, and collectSchemaKeys() now claims them so a future
sweep cannot repeat this.

The collector is deliberately the SAME walk as
scripts/check-schema-l10n.js `collect()`, copied rather than reinvented:
recurse everywhere, and at any node carrying `properties` harvest the
schema title, each property's title and description, and x-enum-labels.
My first attempt walked one level and missed seven nested titles
(event_subscription.properties.action.properties.*, and a title under
items.properties) plus every enum label. Both now report exactly 1215
schema strings, which is the check that they agree.

That is the same lesson as gate-102 earlier in this work: when one tool
decides coverage and another decides what a sweep may delete, they have
to read the same set or the gap is where live text disappears.

Baseline lowered 1120 -> 1111, which the tool offers and which locks in
the nine strings that are now covered and were not before.

Verified: check:l10n exit 0 and all clean; check:schema-l10n exit 0;
test:l10n exit 0; check:l10n-js exit 0; eslint and prettier clean; all 77
applicable hydra gates green with the runner CI resolves, and all 77 ran.
fix(l10n): the register descriptors are a fourth source of live keys
…ding

The connector-catalog specs were skipped wholesale in one describe, with
an accurate reason and tracker #1187. Measured against a live instance
rather than taken on trust, two of the four are reachable today and two
are not, so the blanket skip was costing more coverage than it needed to.

What the specs assumed, and what is actually there:

  getByRole('searchbox')            0 matches. The one visible
                                    input[type=search] is Nextcloud's own
                                    header search and does not filter.
  getByRole('button', 'Source       the quick-filter chips are role="tab",
    templates')                     so the click never landed. This is why
                                    "the filter does not work".
  a non-paginated grid              it pages at 20 of 57, so PDOK is on
                                    page 2 and toBeVisible() failed before
                                    any badge assertion ran.
  BRP shows "available"             it is dormant. available and dormant
                                    are the only two status values across
                                    all 57 rows.

So:

- The quick-filter test is revived against role="tab", and asserts the
  "Showing N of M" TOTAL rather than the card count. The count cannot
  answer the question: it is pinned at the page size of 20 whether 57 or
  35 items match, so the original assertion could not have failed.
- The PDOK dormant test is revived behind a helper that walks pages until
  the card is on screen. The badge assertion re-resolves on every poll,
  because the grid re-renders as its page query settles and a locator
  captured once goes stale, which reads as "no badge" (it did, once).
- BRP and the search test keep individual skips carrying what was
  measured, so the report says why rather than pointing at a describe.
- REQ-002 is untouched: I did not probe it, so I have nothing to say
  about it that #1187 does not already say.

Both revived tests were verified to FAIL when inverted (asserting the
total grows, and asserting a status the schema cannot hold), so neither
is a test that cannot fail.

gate-19 goes 337 to 334 findings. All 77 applicable hydra gates green
with the runner CI resolves, and all 77 ran; the spec file runs 5 passed,
4 skipped, exit 0. Full measurements are posted on #1187 for whoever
takes the remaining four.
test(catalog): revive the two store specs that a describe.skip was hiding
…1840)

* fix(sync): parse RFC 6266 Content-Disposition (filename*) — WOO-552

Port the RFC 6266 parser fix from the WOO stable hotfix
(hotfix/woo-552-content-disposition-rfc6266, release
`v0.2.24-woo-1`) to the development branch, so the next main /
stable release does not re-introduce the bug.

Same shape as the stable fix:
- Replace the naive `explode('=', $header)` in
  `SynchronizationService::getFilenameFromHeaders()` with a call
  to a new private helper `parseContentDispositionFilename()`
  that:
  - splits the header on `;` (so `filename*=UTF-8''pct-encoded`
    no longer fools the extractor),
  - prefers `filename*` over `filename` per RFC 6266 §4.3,
  - accepts case-insensitive parameter names,
  - strips quotes on the plain `filename` value,
  - decodes the pct-encoded UTF-8 payload of `filename*` via
    `rawurldecode()` (RFC 3986 §2.1),
  - logs and falls back on non-UTF-8 charsets.
- Add the second helper `decodeRfc5987ExtendedValue()`.
- Add 10 unit tests via
  `SynchronizationServiceContentDispositionTest`, reaching the
  private parser through `ReflectionClass` — same tests that
  cover the stable fix.

Method signature on development is
`getFilenameFromHeaders(array $response, ObjectEntity $result)`
(the URL-fallback branch reads the request via
`$result->getObject()['request']`); only the Content-Disposition
branch is touched by this commit, the URL-fallback logic is
left as-is.

Triggered by xxllnc's Zaken API change of 2026-08-19 (started
emitting both `filename` and `filename*`, breaking OpenWoo.App
document downloads; rolled back but scheduled to re-roll
behind a feature toggle). Stable hotfix `v0.2.24-woo-1` is
already live as prerelease for direct install on WOO
production; this PR ensures the next main-line release does
not regress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): address PR #1840 review — CI style + quoted-string tokenizer

Addresses the 8 findings from the self-review on #1840:

phpcs / phpmd (CI-red) — SynchronizationService.php:
- Line 9056, 9128: use named arguments on internal helper calls.
- Line 9120: rename `$eq` → `$eqPos` (phpmd ShortVariable, min 3).
- Line 9171: capitalize inline comment ("Decode via rawurldecode()…").

Parser correctness (RFC 6266 §4 quoted-string grammar):
- Replace `explode(';', $header)` with a quoted-string-aware tokenizer
  `splitHeaderParameters()` so a `;` inside `filename="…"` no longer
  corrupts the value. `filename="foo;bar.pdf"` now returns
  `foo;bar.pdf` (was `foo`).

Test file (SynchronizationServiceContentDispositionTest.php):
- Fix namespace: `OCA\OpenConnector\Tests\...` → `OCA\Integriq\Tests\...`
  to match composer autoload-dev psr-4 rule (was skipped by composer's
  autoloader with a warning; every other test in tests/Unit/Service/
  uses the Integriq namespace).
- Add 3 tests:
  - `testFilenameWithSemicolonInsideQuotedValuePreservesFilename`
  - `testFilenameWithPathTraversalPayloadIsReturnedVerbatim` (parser
    contract test — sanitization is downstream's responsibility)
  - `testFilenameWithWhitespaceAroundEqualsIsAccepted`

Refs: WOO-552, WOO-553

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(sync): lock Barry's rapport-versie example as explicit test — WOO-553

Add `testFilenameWithSemicolonAndSpaceInsideQuotedValuePreservesFilename`
covering Barry's exact string from his PR #1840 review:

    attachment; filename="rapport; versie 2.pdf"

The existing `testFilenameWithSemicolonInsideQuotedValuePreservesFilename`
already exercises the same tokenizer path via `foo;bar.pdf`, but the
new test locks the concrete example he raised so the guarantee is
explicit in the suite, not only implied.

Also mirrored to the WOO stable hotfix branch alongside the
tokenizer backport (WOO-552 hotfix cut of v0.2.24-woo-2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): correct SynchronizationService namespace in test — WOO-553

The test imported OCA\OpenConnector\Service\SynchronizationService, but
integriq's class lives under OCA\Integriq\Service\SynchronizationService
(matching every other test in this directory). ReflectionClass failed on
the missing class, producing 14 PHPUnit errors across every PHP/NC matrix
cell and cascading into the Quality Report failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): honour RFC 9110 quoted-pairs in Content-Disposition tokenizer — WOO-553

splitHeaderParameters() toggled $inQuotes on every '"', so an escaped
quote inside a quoted filename (\") flipped the state and a following
';' split the value. Skip backslash quoted-pairs as a unit inside
quotes, and unescape them (\" → ", \\ → \) in the plain filename via
new unquoteHeaderValue(). Review case from PR #1840:
  attachment; filename="rapport \"final\"; versie 2.pdf"
now yields: rapport "final"; versie 2.pdf

+3 unit tests (17/17 green).

* fix(sync): harden Content-Disposition parsing per Strict re-review — WOO-553

Addresses the six findings of the round-3 self-review on PR #1840:

- Empty parameter values no longer yield '': an empty filename*
  (UTF-8'') cannot clobber a usable plain filename, and filename="" /
  filename= return null so the URL/MIME fallback runs instead of
  saveFile('') aborting the sync.
- An undecodable second filename* no longer overwrites a decoded one.
- filename* is unquoted before decoding (sloppy servers send
  filename*="UTF-8''x.pdf"); malformed ext-values are now logged like
  unsupported charsets instead of failing silently.
- Content-Disposition is looked up case-insensitively via new
  getHeaderValue() so lowercase HTTP/2 headers reach the parser; the
  body-decoding guard uses the same lookup.
- Test docblock lists all covered scenarios; the path-traversal test
  comment names the real guard (NC core Filesystem::isValidPath()).

+6 unit tests (23/23 green).

* fix(sync): enforce decodable filename* and harden header lookup (review round 4) — WOO-553

- decodeRfc5987ExtendedValue(): rawurldecode() never fails, so reject
  results that are invalid UTF-8 or contain control characters (%00);
  a valid plain filename then wins instead of garbage.
- Empty-checks on filename*/filename are trim-based (whitespace-only
  counts as absent).
- getHeaderValue(): accepts mixed (persisted scalar headers → null),
  skips unusable values (empty list, null, nested array, false) and
  keeps scanning for a differently-cased duplicate; docblock widened.
- Content-Type fallback in getFilenameFromHeaders() now uses the same
  helper instead of two hard-coded casings.
- Malformed-ext-value log carries valueLength, not the raw value.
- Docblock prose updated (parser fallback conditions, test helper).

+6 unit tests (29/29 green).

* style(sync): capitalise inline comment (phpcs Squiz.Commenting.InlineComment) — WOO-553

* style(sync): review round 5 nits — comment period, distinct log keys, named args on mb_check_encoding — WOO-553

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hydra-gates v1.15.0 -> v1.16.0
nc-vue      2.37.0 -> 2.39.0

Lock-only: both packages are already declared with caret ranges that
permit these versions, so nothing about what this app ACCEPTS changes
- only what it currently resolves to. Opened by the weekly fleet
shared-dependency bump, because a lock nobody re-resolves is a pin
nobody chose.

Merging is gated by this repository's own suite, deliberately: taking
hydra-gates v1.8.1 added patchObject() to a published interface, which
is a load-time fatal for any concrete double that implements it without
the method. CI is the only thing that can tell a safe bump from that.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
First capability of the gate-19 review. The 21 untagged scenarios here
were not an annotation backlog: a third of them describe a manifest that
no longer exists, and tagging those would have cemented the staleness.

Stale, corrected against the live manifest:

  "All 15 menu entries are present"   ADR-097 grouped the nav. There are
                                      10 top-level entries, five of them
                                      groups, and Import and Settings are
                                      not among them.
  "Settings section entries carry     the surviving chrome moved from
   section field"                     section "settings" to "footer"
                                      under ADR-114.
  "Documentation entry uses href"     the href moved with the rename, to
                                      integriq.conduction.nl.
  "Primary nav entries have route"    named a flat id list, half of it
                                      gone. The invariant survives the
                                      regrouping and is what is asserted.
  "version is 1.0.0 for D1"           the manifest is 2.1.0. The durable
                                      claim is that the version tracks the
                                      schema generation.
  Import / Settings pages             INVERTED rather than deleted, per
                                      the fleet pattern: the scenario now
                                      asserts the page is absent, so its
                                      accidental return fails rather than
                                      passing silently.

Three scenarios gained real coverage rather than a waiver, because
anchoring without an assertion is the defect this gate exists to catch:

  - every domain leaf sits inside a group, walking the whole main list.
    Asserting it for one or two leaves would not establish "no loose
    leaves", which is what the scenario claims.
  - Documentation leaves the app, pinning the href in the rendered DOM.
  - every navigating menu entry has a route that names a real page,
    walking children too, with a positive control so an empty menu cannot
    satisfy it vacuously.

Both new browser tests were verified to FAIL when inverted.

The remaining 13 carry reason-bearing `@e2e exclude`: source-text greps
over main.js, package.json keys, JSON type assertions, and three
validateManifest cases that need a deliberately INVALID manifest, which
an e2e against a running app cannot produce. Each reason names what does
cover it instead.

gate-19 goes 334 to 318. All 78 applicable hydra gates green with the
runner CI resolves, and all 78 ran; check:specs exit 0; app-chrome 14
passed, manifest-pages 52 passed.
…fest

spec(app-manifest): correct the stale scenarios and annotate the rest
… code that was never built

Second capability of the gate-19 review, and the largest at 46 scenarios.
Almost none of it is browser-observable: it is the OpenRegister cutover,
so the claims are about files being absent, constructors carrying no
mapper, quality gates rejecting bad input, and services calling
ObjectService.

35 carry reason-bearing `@e2e exclude`, each naming what actually holds
it, and each checked rather than asserted:

  file-absence and shape claims  verified against the tree. lib/Db is
                                 gone, ImportController and
                                 ExportController do not exist, no file
                                 names ObjectMapperFacade, and the only
                                 two *Mapper.php left are StUFFieldMapper
                                 and FormFieldMapper, which are FIELD
                                 mappers, not entity QBMappers.
  quality-gate scenarios         they work by feeding the gate
                                 deliberately bad input, which an e2e
                                 against a running app cannot produce.
  service behaviours             named against the PHPUnit class that
                                 covers each: JobServiceTest,
                                 CallServiceTest, SourcesControllerTest.
  migration scenarios            occ upgrade runs a migration and its
                                 repair step; a browser session never
                                 reaches that path.

SIX SCENARIOS DESCRIBE CODE THAT DOES NOT EXIST, and are flagged rather
than annotated, because a waiver would have hidden that:

  SourceService          no such class under lib/. Three scenarios read
                         and delete Sources through it.
  SlugTranslatorService  appears ONLY in this spec. Two scenarios.
  SettingsController     exposes `rebase`, not the `applyRetention` the
                         scenario names.

Annotating those would have recorded coverage for a design that never
shipped. They need rewriting against what the cutover actually landed,
which is a change of its own.

One scenario is anchored, after reading the body rather than grepping:
`synchronization-run-endpoint-is-still-reachable` on the pipeline test in
synchronization-workflow.spec.ts, which POSTs to
`/synchronizations/{id}/run` and asserts the endpoint RESOLVES the sync
rather than 404ing. That is the scenario's THEN.

Four are left visible with a reason saying why no test establishes them:
the Sources wire shape, the OR create and export routes, and the
dashboard's dataSource resolution. Each names the test that touches the
subject without proving the claim.

gate-19 goes 318 to 278. All 77 applicable hydra gates green with the
runner CI resolves, and all 77 ran; check:specs exit 0.
spec(direct-or-usage): annotate 40 scenarios and flag 6 that describe code that was never built
…scribe deleted code

Third capability of the gate-19 review. All 28 scenarios are about the
legacy-to-OpenRegister storage transition, and that transition is done,
so the spec documents a state the app has left.

Seven describe machinery that no longer exists, and are FLAGGED rather
than annotated, because a waiver would record coverage for deleted code:

  find(int) / createFromArray / delete-invalidates-cache / append-only on
  log mappers      the entity mappers are gone; lib/Db does not exist
  flag routes to legacy path, flag routes to facade path, rollback to
  legacy path      no file under lib/ carries a legacy path, and none
                   names ObjectMapperFacade

The flag itself SURVIVES, so those scenarios were not swept with the
rest: `storage_migrated` is still read, by SynchronizationContractProvider
among others.

21 carry reason-bearing `@e2e exclude`, split by what actually holds
them: occ upgrade and console behaviours for the migration, its repair
step and the retry command; PHPUnit for the JSON build, UUID
preservation, PK resolution and null-owner paths.

One was verified TRUE rather than waived. "Legacy tables present after
migration" holds: measured 2026-09-07 on a migrated instance, 15
oc_openconnector* tables are still there, because
Version2Date20260520000099 drops each only once it is EMPTY and refuses
on any that is not. The exclude reason records the measurement, not an
assumption.

gate-19 goes 278 to 257. All 77 applicable hydra gates green with the
runner CI resolves, and all 77 ran; check:specs exit 0.
spec(storage-migration): the cutover completed, so seven scenarios describe deleted code
Fourth capability of the gate-19 review. 24 scenarios about the Vue
rewrite, and the same shape as the three before it: mostly static claims
about what the migration deleted, plus a handful that no longer hold.

Four are stale, flagged rather than annotated:

  "All 13 manifest menu items"    ADR-097 grouped the nav. 10 top-level
                                  entries, five of them groups.
  "Import page retains custom     there is no Import page.
   type"
  "Settings page is reachable     there is no Settings page. ADR-114 puts
   via nav"                       settings behind the nav foldout, which
                                  links to /settings/admin/integriq.
  "settings.js webpack entry is   THE ENTRY IS STILL THERE, ON PURPOSE.
   absent"                        webpack.config.js keeps `settings` to
                                  build src/settings.js for the ADR-023
                                  admin panel rendered by
                                  templates/settings/admin.php, which is
                                  a different surface from the SPA this
                                  change migrated. The scenario is not
                                  unmet; it is wrong.

Four are anchored on manifest-pages.spec.ts, after reading the body: the
mount loop drives every manifest route and asserts the router matched,
that content rendered inside #app-content and that no console error
fired, while `manifest page table is complete and current` pins each
page's declared type. Together those establish that a page of the
declared type renders at its route, which is what the navigation,
Sources, Rules and MappingDetail scenarios claim.

17 carry reason-bearing `@e2e exclude`, each verified rather than
asserted: src/router/index.js, src/navigation, Modals.vue and any
src/-root widget JS are all confirmed absent, and zero .php files remain
under src/. The build and lint scenarios name the CI command that holds
them instead.

Four are left visible with a reason saying why nothing establishes them:
sidebars per page, the Nextcloud Dashboard widget bundles, the
flow-token surviving a synchronization trigger, and the create-source
form opening from CnIndexPage. The last is the interesting one: the CRUD
spec does create a Source through the UI, but it drives the whole round
trip rather than asserting the form opens, so anchoring it would
overclaim.

gate-19 goes 257 to 237. All 77 applicable hydra gates green with the
runner CI resolves, and all 77 ran; check:specs exit 0.
spec(vue-rewrite): four scenarios outlived the migration they describe
rubenvdlinde and others added 29 commits September 8, 2026 12:51
…e canvas (#1928)

tests/e2e/regression/flows-editor.spec.ts:153 has failed on development since at
least 2026-09-07, always the same way: a 60s test timeout, then
'Target page, context or browser has been closed' reported from the NEXT call
inside openFlowActionsMenu. That reads as a hung editor. It is not.

The page snapshot in the trace shows four buttons named Actions: three inside
the flow canvas that are in the accessibility tree but zero-size, and the real
one in the sidebar header next to the New flow heading. The helper matched all
four by accessible name and clicked .first(), which is one of the canvas ones.
Clicking a non-actionable element waits, and the click carried no timeout, so
that wait was the whole test budget.

Two changes. The sidebar-scoped selector moves ahead of the unscoped name match,
because CnFlowSidebar puts the 'Flow actions' aria-label on the NcActions
wrapper while NcAppSidebar wraps that slot in an NcActions of its own, so the
button a user clicks is named plainly 'Actions'. And every candidate is now
filtered to visible and clicked with a 5s timeout, so a wrong trigger costs 5s
and the loop moves on instead of ending the test.

Verified against a fixture that reproduces the DOM: three zero-size buttons
named Actions plus a visible sidebar one. The old helper exceeds a 15s budget;
the new one opens the menu in 191ms. prettier --check passes on the file.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
… suite (#1929)

The test bootstrap guard added earlier today loads lib/base.php only when
config/config.php declares installed => true. That part is right and stays.
The same change also made the bootstrap exit(1) when the tree is installed and
base.php throws part-way, and humaniq proved that wrong: its CI Nextcloud is
installed, base.php dies on a Doctrine constant its vendor and the server
disagree about, and all six PHPUnit legs went red on a suite that passes.

Aborting was the wrong trade. Reaching a half-built container needs an
autowiring lookup, this app has none in lib, and phpunit.xml caps every run at
2G. So a mid-boot failure is now loud rather than fatal: it says the container
is unreliable and lets the pure unit tests run.

The not installed branch is untouched. It still skips base.php and runs with
composer autoload and stubs only, which is the case the guard exists for.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…nrun collection

Two gates landed in hydra-gates at @main between this morning's runs and now
red every integriq build. Neither is caused by the change that surfaced them:
the previous development run's gates job was a clean success.

gate-113 exclusion-evidence. Three @e2e exclusions in the flow-orchestration
spec named `tests/e2e/ci/flow-controls.spec.ts`. That file has never existed in
this repo, so all three read as verified and were not. They now state what
actually covers the scenario, which for two of them is the engine's unit tests
and for the third is nothing at all.

Deliberately NOT repointed at tests/e2e/spec-coverage/flow-orchestration.spec.ts.
That file covers the flows index, the canvas, the trace timeline and replay
safety. It does not touch a run log or its empty state, so citing it would have
replaced one false claim with another, which is the exact thing this gate
exists to catch.

gate-112 newman-reach. tests/integration/integriq.postman_collection.json is a
58-request contract suite sitting outside newman-collection-path, so CI has
never run it. The collection newman does run is tests/postman (101 requests,
chain E). It is marked `@newman exclude` rather than moved, because pointing
newman at it would start executing 58 requests that have never run in CI. That
changes what CI verifies and is a separate decision for whoever owns the suite.

The marker was inserted as text rather than by reserialising the JSON: the diff
is one line changed, not the whole file.

Verified against the gate version CI actually resolves. Running the runner from
ConductionNL/.github@main rather than the vendored copy gives 79 PASS, zero
FAIL, exit 0 — the vendored copy does not carry gates 112 or 113 at all, which
is why they could not be seen locally before.
…tations

Stop citing a test that has never existed, and mark the unrun Postman collection
#1948)

flows-editor.spec.ts still timed out after the previous fix, and the trace says
why in one line: the toolbar Save click was retried 96 times against

  <div class="modal-wrapper"> from <div role="dialog" aria-modal="true">

CnFlowSettingsModal is an NcDialog. It stays open until something closes it, and
its wrapper covers the page. The test opened it, filled the name, and then
clicked the toolbar behind it. Playwright reported the button as visible,
enabled and stable each time, so the run looked like a hung editor rather than a
click landing on an overlay.

The previous fix was not wrong, it was incomplete: it got the Actions menu
opening, which moved the failure from the menu to the Save click. This closes
the dialog and waits for it to go.

Verified: prettier exit 0. The proof this makes the spec green is the
development push run, since E2E runs only there.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…#1951)

Third and I hope last turn on this spec. Each attempt moved the failure
forward, which is how I know the earlier ones were right as far as they went.

- The original 60s timeout was a hidden canvas button eating the click budget.
- Fixing that got the Actions menu open and moved the failure to the toolbar
  Save, which the still-open dialog was intercepting.
- Dismissing the dialog with Escape does not dismiss it. This run watched
  toBeHidden resolve the dialog as visible 24 times over 10s, then failed at
  20.7s instead of timing out at 60s.

The page snapshot in that trace lists the control by name:

    - dialog "Flow settings":
      - button "Close"

So click it. The lesson each time was the same one: read the snapshot in
error-context.md before theorising, because it says what is on the page.

Verified: prettier exit 0. The proof is the development push run, since E2E
runs only there.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Supersedes #1941. vitest 5 stops before a single test runs:

    Cannot find package 'vite' imported from node_modules/vitest/...

vitest 5 expects `vite` to be resolvable from the project; 4 did not.
This app has never declared it, so the bump cannot land on its own.

Adding `vite ^8.2.2`, the version versioniq already carries — which is
why versioniq's identical bump went green while this one did not.

This app declares no `@vitest/coverage-v8`, so unlike decidiq, filinq,
buildiq and dossiq there is no exact-pinned peer to move in step here.

Verified locally rather than left to CI: `npm install` clean, then
`npm run test:unit` on vitest 5.0.0, all passing, exit 0.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Bumps [webpack](https://github.com/webpack/webpack) from 5.110.2 to 5.110.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](webpack/webpack@v5.110.2...v5.110.3)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.110.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [eslint](https://github.com/eslint/eslint) from 10.9.1 to 10.10.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.9.1...v10.10.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…1944)

Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 30.5.0 to 30.5.1.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.1/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-version: 30.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [sass-loader](https://github.com/webpack/sass-loader) from 17.0.0 to 17.0.1.
- [Release notes](https://github.com/webpack/sass-loader/releases)
- [Changelog](https://github.com/webpack/sass-loader/blob/main/CHANGELOG.md)
- [Commits](webpack/sass-loader@v17.0.0...v17.0.1)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-version: 17.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.4.0 to 26.4.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.62.1 to 1.63.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](microsoft/playwright@v1.62.1...v1.63.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.9 to 2.2.13.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
…1935)

Bumps [conduction/hydra-gates](https://github.com/ConductionNL/.github) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/ConductionNL/.github/releases)
- [Commits](ConductionNL/.github@v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: conduction/hydra-gates
  dependency-version: 1.16.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [web-token/jwt-library](https://github.com/web-token/jwt-library) from 4.2.1 to 4.2.2.
- [Commits](web-token/jwt-library@4.2.1...4.2.2)

---
updated-dependencies:
- dependency-name: web-token/jwt-library
  dependency-version: 4.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.14 to 3.4.15.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](cure53/DOMPurify@3.4.14...3.4.15)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben van der Linde <ruben@conduction.nl>
Bumps [stylelint](https://github.com/stylelint/stylelint) from 17.14.1 to 17.15.0.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](stylelint/stylelint@17.14.1...17.15.0)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-version: 17.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [sass](https://github.com/sass/dart-sass) from 1.103.1 to 1.104.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](sass/dart-sass@1.103.1...1.104.0)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.104.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… client that ships (#1958)

The berichtenbox-digital-post-adapter change was already filed and unstarted
at 0 of 10 tasks. This does not add an eleventh proposal for the same
capability. It fixes four things about the one that exists.

THE BLOCKER WAS A BULLET AT THE BOTTOM. It is now the first heading, because
the most likely failure of this change is somebody implementing it, seeing
green tests, and believing post is delivered. And it was incomplete: it named
a PKIoverheid certificate and an OIN, not the Logius BBK OAuth 2.0 client
credentials, and not the third blocker at all. PkiOverheidCredentialResolver
resolves through OpenRegister's CredentialBrokerService::issueSigningMaterial,
which does not exist, so it fails closed for every certificateRef. Even with a
purchased certificate, in-process signing stays refused. Digikoppeling is
blocked on the same thing.

IT PROPOSED A BERICHTENBOX PROVIDER FROM NOTHING while three Berichtenbox
classes were on disk: an abstract BerichtenboxClient with four methods, an
honest mock that answers signatureValid=false on purpose, a source adapter
reading logius.berichtenbox.feature_flag, a catalog descriptor and a DI
registration. The binding is now specified over that client, and REQ-DPA-006
forbids a second Berichtenbox code path.

TWO ARCHITECTURES WERE ON THE BOOKS. absorb-dossiq-deliveries carries an open
task describing this as a provider quintet on the StufZkn pattern. D7 picks
the provider seam, says why (digital post has more than one destination), and
makes closing that item a task rather than an inference.

TWO DEFECTS IN THE SHIPPED CODE ARE NOW SPECIFIED. The feature flag branches
nothing: Application.php binds the mock unconditionally, so setting the flag
to 1 changes a log field. And dispatch() takes raw PEM by value, which
REQ-DK-005 and ADR-007 forbid and which the sibling Digikoppeling adapter
already refuses to do; it becomes a certificateRef resolved inside.

Tasks 1, 2, 5, 6 and 7 are unblocked and land first. Task 8 is the live
network leg and says BLOCKED, do not start.

Spec only. Nothing is implemented here.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…nnector (#1961)

* fix(mappings): resolve the default register instead of pinning openconnector

MappingsController::saveObject() defaulted its register to the literal
'openconnector'. This app's own repair step MigrateRegisterSlug renames
that register to 'integriq', and it runs per instance, so both slugs are
live across the estate and a literal is wrong on whichever half has not
caught up.

The old-slug case is the quiet one. OpenRegister finds no register row,
so the write lands nowhere the UI reads back, and the endpoint answers
200 with an object nobody can find again. No exception, no 404, no log
line. Measured on the local dev instance, which is fully migrated: the
register table holds buildiq, dossiq, integriq, learniq and stackiq, and
no openconnector.

The default is now a resolution through OpenRegister's published
RegisterSlugResolverInterface, and the caller branches on isResolved().
An instance carrying the register under neither slug gets a 409 naming
the repair step, not an empty success. A caller-supplied register is
still used as given: those come from the register picker, whose options
are live openregister_registers rows, so they already carry this
instance's own naming.

Swept the rest of lib/ with the guard's own patterns and a loose
case-insensitive grep for all ten superseded fleet slugs. 30 literals,
one register pin, this one. The other 29 are app ids for the credential
broker's allowedApps, StUF zender/applicatie identities, JWT issuer
claims, a distributed-cache namespace, webhook signature scheme names,
repair-step OLD_APP_ID constants and one comment. Each was read, none
identifies a register.

Also in this change:

- conduction/hydra-gates ^1.8.2 to ^1.18.0, which is the first tag that
  ships the contract. Verified rather than assumed: the lock reads
  v1.18.0 and vendor/conduction/hydra-gates/hydra-gates/contracts/ holds
  all four files.
- The contracts are read out of vendor, never copied in. shillinq's run
  copied them and dragged OpenRegister's @SPEC annotations along, which
  the anchor gate then resolved against the consuming repo: seven
  findings for paths that were never going to be there.
- psalm SCANS the vendored contracts rather than suppressing them.
  Checked both ways: with the extraFiles entry removed psalm raises 4
  UndefinedClass errors, with it psalm resolves the real types and the
  resolution branch is checked for real. No suppression was added.

* chore(mappings): drop em-dashes from the comments this change added

The house prose rule bans them. Six sites, all in text added by the
previous commit; the em-dashes already in these files predate it and are
left alone rather than turned into an unrelated 200-line diff. Also
corrects the class docblock of the resolution test, which said three
assertions reddened under the mutation where the method docblock beside
it recorded the measured four.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…-subscription-connector

Retires the rejected integriq store design (brp-kvk-store-and-subscriptions) with the decision recorded in its archived proposal, and adds registry-subscription-connector scoped to only the adapter half integriq owns. No code changes. Companion: ConductionNL/openregister (registry-subscriptions implementation).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…0260912131727

chore(sync): carry beta back into development
* feat(e2e): refuse the shared instance unless the run names it

The dev box runs one Nextcloud on localhost:8080 that bind-mounts everyone's
checkouts. A suite that seeds and deletes objects there is editing a
colleague's environment. Removing the hardcoded fallback stopped the suite
going there by default, but an environment that still sets NEXTCLOUD_URL to
:8080 walked straight in.

tests/e2e/shared-instance.ts holds the guard and BASE_URL in
tests/e2e/support/baseUrl.ts is now resolved through assertInstancePermitted(),
which is the one place a target enters this suite. Aiming at the shared
instance still works, it just has to be named:
INTEGRIQ_E2E_ALLOW_SHARED_INSTANCE=http://localhost:8080. CI is exempt, where
localhost:8080 is the runner's own throwaway instance.

The unit test sits in tests/vitest/ because that is the only path the vitest
config collects, and it excludes tests/e2e outright.

* fix(e2e): stop the guard's unit test failing on CI, and satisfy prettier

Two things the first commit got wrong.

The guard exempts CI on purpose, and the unit runner runs on a runner, so the
three refusal cases passed on a laptop and failed everywhere it mattered:
CI=true npx vitest run gave 3 failed, 7 passed. The test now stubs CI and
GITHUB_ACTIONS empty in a beforeEach and unstubs them after.

Both files also failed prettier --check, which is its own CI leg and is not
implied by eslint passing. Re-rendered from the template and reformatted.

Verified: 10 of 10 pass with CI=true and without. The whole vitest suite goes
from 16 files / 293 tests to 17 files / 303 tests, so the file is collected
rather than dark, and playwright still collects 348 tests in 40 files, the same
as development.
@rubenvdlinde
rubenvdlinde merged commit 73a5d54 into beta Sep 13, 2026
101 checks passed
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.

2 participants