Skip to content

Release: merge development into beta - #571

Open
github-actions[bot] wants to merge 34 commits into
betafrom
development
Open

Release: merge development into beta#571
github-actions[bot] wants to merge 34 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 3 commits September 6, 2026 12:47
…260906124710

chore(sync): carry beta back into development
hydra-gates v1.15.0 -> v1.16.0
nc-vue      - -> 2.37.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>
…n the test bootstrap (#576)

The CLI php.ini sets memory_limit=-1, so a runaway test had nothing to stop it: on 2026-09-08 one openregister test recursed inside the DI container and took 19 GB of RAM. With PHPUNIT_USE_NC_BOOTSTRAP=1 the test bootstrap also loaded the workspace's lib/base.php from a source tree that was never installed, which declares OC and builds a half-built OC::$server before throwing, and that state cannot be undone.

- phpunit.xml and phpunit-stubs.xml: hard memory_limit of 2G (the unit suite peaks at 123 MB without coverage)
- tests/bootstrap.php: the PHPUNIT_USE_NC_BOOTSTRAP opt-in now also requires launchpad_nc_root_is_installed(), which reads config/config.php in a closure and demands installed => true; a bare source tree says so on STDERR and falls back to the OCP stubs; if base.php still throws, the run stops with exit 1 instead of continuing half-booted
- composer.json: psalm gets --memory-limit=2G and each phpmd call runs under php -d memory_limit=2G

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde and others added 19 commits September 8, 2026 12:50
… suite (#578)

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Supersedes #533, which fails Frontend Build with

    Module not found: Can't resolve 'pinia' in
      node_modules/@conduction/nextcloud-vue/dist/esm/composables

That reads as a broken shared library and is nothing of the kind. webpack
names the ISSUER of an unresolvable import, not the config line that made
it unresolvable, and the config line is ours.

pinia 2.1.7 declared `main: index.js` and `module: dist/pinia.mjs`, so
aliasing the package DIRECTORY resolved. 4.0.3 declares NEITHER — only

    "exports": { ".": "./dist/pinia.js" }

A directory alias bypasses `exports`, finds no main or index, and
resolves to nothing.

This file already documents the same failure for @nextcloud/vue and
@nextcloud/dialogs a few lines below, and prescribes the fix: point at
the concrete entry rather than the directory. pinia 4 has joined that
category, so it gets the same treatment. The `$` stays, so any deep
import keeps going through the exports map.

Same shape as the `@nextcloud/axios` alias that broke on 2.6.0 across
planninq and zaakafhandelapp: a config encoding an assumption about
someone else's package layout, correct until the day it is not.

Verified locally rather than left to CI: `npm install` clean on pinia
4.0.3, then `npm run build` — webpack compiled, exit 0, warnings only
(a pre-existing runtimeChunk recommendation).

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* fix(livetile): point the connector probe at the class integriq actually ships

The connector source mode has been a silent no-op. Three separate names in
LiveTileService named things that do not exist:

  - the app id `openconnector`, retired in the fleet rename;
  - the FQCN `OCA\\OpenConnector\\Service\\DashboardDataSourceService`, which
    is a namespace segment short and a capital S out from the real class and
    has never existed under EITHER name (checked with `git log -S` over
    openconnector's full 3944-commit history);
  - the method `resolveDashboardValue`, likewise never present.

Every one of those failures is guarded. `isEnabledForUser()` on a name the
instance never registered returns false, `ContainerInterface::has()` on an
absent class returns false, and `method_exists()` on a missing method returns
false. So the tile rendered an 'unavailable' state and nothing was logged.

Read off integriq `development`, the real surface is
`OCA\\Integriq\\Service\\Datasource\\DashboardDatasourceService::resolve($sourceId,
$valueExpr, $params, $ttl)`, returning {value, fetchedAt, stale}. The value
key is what fetchFromConnector already reads, so the shape needed no change.

Resolution now runs through a FleetAppId helper ported from dossiq. A hard
swap to the new literal would re-break every instance still on a pre-rename
release, which is the same bug pointing the other way, so both the id and the
namespace resolve against a candidate list, newest first.

The old test faked a `resolveDashboardValue()` that nothing implements, so it
passed against a contract no app on the other side offers. It now mirrors the
real signature, and two new tests pin the FQCN and the old-id fallback.

* fix(frontend): name live fleet apps in the cross-app fixtures and docs

Renames retired fleet ids where the target on the other side was read and
confirmed to exist:

  - nldesign -> thematiq in the TileEditor icon-URL fixtures. thematiq ships
    img/icons/Star.svg, so /apps/thematiq/img/icons/Star.svg resolves.
  - procest -> dossiq in the graphqlClient and deep-link fixtures. dossiq
    serves /apps/dossiq/{path} through the AppHost SPA catch-all, so the
    deep-link builder's output is a real URL.

Both call sites take the app id as an argument, so these are sample values in
tests of generic helpers, not production lookups. No behaviour changes.

Two production lookups in spendAnalytics.js are deliberately NOT repointed,
because the target does not exist under the new name either, and a rename
would have read as a fix while leaving both dead:

  - /apps/openconnector/api/sources/{source}/call. integriq publishes no such
    route, `git log -S` finds it was never added under the old name, and the
    app has no LLM or inference endpoint at all.
  - SPEND_SOURCES.PROCEST, which builds /apps/<app>/graphql. No fleet app
    publishes that route under either name; OpenRegister serves GraphQL at
    /apps/openregister/api/graphql, so the endpoint is what has to change.

Each carries a comment recording what was read and what the real target is.

* chore(quality): allowlist twelve pre-existing spec-annotation offenders

`composer lint:spec-annotations` has been failing on `development`, which took
`composer check:strict` red for everyone. All twelve post-date the 2026-05-03
baseline and match shapes already allowlisted wholesale: mapper query methods,
entity serialisers, a migration schema hook and an IRepairStep label getter.

check:strict now exits 0.

* test(support): cover FleetAppId's fallback paths

The coverage guard on #582 was right: FleetAppId landed with most of its
statements unexercised. Only the paths LiveTileService happens to walk were
covered, which is the wrong half — the branches that decide whether a
cross-app binding survives a half-migrated instance were all untested.

Those branches matter more than average because every one of them fails
SILENTLY. isInstalled() on an unregistered name returns false rather than
raising, class_exists() on a moved namespace answers false, and get() throws
into a caller that is already catching. Nothing announces itself.

28 tests, covering both halves of the rename:

  - id: new id preferred, OLD id resolved when only it is present, both
    present resolves newest-first, neither returns null, an unmapped app
    falls back to its own name, and one throwing candidate does not abort
    the search for the next.
  - namespace: candidates newest-first, the irregular OCA\OpenBuilt pinned
    against anyone deriving it from the id, container fallback to the old
    FQCN, and a throwing container reading as absent rather than fatal.
  - appPath: uses the REGISTERED id, no doubled separator, null when absent.

Seven mutations were run and each reddened only its own assertion: reversing
the candidate order, dropping either per-candidate catch, dropping the old
namespace, skipping id resolution in isEnabledForUser, removing hasService's
catch, and dropping appPath's ltrim.

The fixture declares a class under OCA\OpenBuilt, a namespace no launchpad
code binds to, so it cannot collide with a real class or reach another test.

---------

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

`fetchSpendNarrative()` calls `/apps/openconnector/api/sources/{source}/call`
and its spec asserts the path. gate-115 reports both as cross-app lookups
naming a retired app id. The comment beside the call already explained why it
is not repointed; this adds the machine-readable half so the gate stops
re-raising an answered question, and puts the same marker on the assertion
that pins the path.

Re-verified 2026-09-10 against integriq `development`. The route exists under
NEITHER name. SourcesController publishes only test, logs, tripCircuitBreaker
and resetCircuitBreaker, and `git log -S "sources/{source}/call"` and
`git log -S "sources#call"` over integriq's full 3,960-commit history, which
spans the whole openconnector era, both return nothing. The app has no
inference endpoint at all.

So REQ-SAW-006 waits on an integriq surface that has never been built.
Repointing the segment would turn one 404 into another while hiding that gap,
on a diff that reads as a fix.

Recorded with `@stale-fleet-app-id exclude`, the shared exclusion convention
gate-115 reads (.github#738).

Comments only. No behaviour changes, and no test assertion changes: the spec
still asserts the path the caller actually uses.

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

* fix(auth): admin menu entries rendered for every account

CnAppNav treats an absent permissions prop as permission to render
everything, and launchpad passed none, so two entries declared admin only
were visible to anyone. Seventeen of nineteen fleet apps carry a filter
reading a value that is always empty, so their menu filtering is dead code
today and safe only because they declare nothing.

Work in progress: preserved after a rate limit ended the session that was
writing it. The e2e proof was still running when it stopped.

* test(e2e): key the admin-entry absence on the menu id, not the route

The absence assertions read `data-cn-route`, which renders `item.route`
verbatim. This fix changes `item.route`: both entries declared the URL path
where CnAppNav wants a route name. So against a bundle built without the fix,
`expect(routes).not.toContain('admin-templates-index')` passed, because the
unfixed manifest never emits that string. The entries were on screen
throughout, and the same run recorded them for the admin as "/admin/templates"
and "/admin/settings".

The entry id is untouched by the fix, so the absence now keys on
`data-testid="cn-nav-entry-<id>"`. `data-cn-route` is kept for the one
question that really is about the route: whether an admin's entry resolves to
a working link.

Also raises the budget for the three tests that log in cold. A first login for
a freshly provisioned account took 59s on the shared box, so the 60s config
timeout expired during login and all three reported a timeout rather than an
assertion. A guard test that times out is red whether the guard works or not.

Adds tests/e2e/support/bundleOverride.ts, which serves the compiled bundle
from a local directory so the same spec can be run against a build with the
fix and one without it. Inert unless LAUNCHPAD_BUNDLE_DIR is set.

* test(e2e): assert where a blocked non-admin lands, not one exact URL

The redirect tests pinned the landing URL to `${APP_BASE}/?$`. The guard sends
the browser to '/', and LaunchPad resolves '/' onward to its active dashboard,
so a WORKING redirect arrives at /apps/launchpad/dashboard and the pattern
called it a failure. Both redirect tests were red against a bundle that had
already redirected correctly.

Asserted instead: the browser is still inside this app, and no longer on an
admin path. That still fails when the guard is absent, where the route is
served, AdminSettingsRedirect bounces to /settings/admin/launchpad, and the
first assertion does not match.

* test(e2e): check the URL before the shell, so the failure names itself

Against an unguarded bundle the first thing to fail was the wait for
.workspace-shell, so both redirect tests reported the shell as not visible.
True, and not the point: the shell is missing because the browser left
LaunchPad for /settings/admin/launchpad, which is to say the non-admin reached
the admin surface. "Shell not visible" equally describes an app that failed to
boot, so the run could not tell a security failure from a broken build.

The wait is now a settle whose rejection is swallowed, the URL is asserted
first, and the shell is asserted properly afterwards so that a page rendering
nothing still fails.

* test(e2e): establish the identity before the action, not after it

whoami was read after navigating to the gated route. Against an unguarded
bundle that lands on /settings/admin/launchpad, which Nextcloud refuses to a
non-admin with an error page carrying no OC bundle, so OC.getCurrentUser()
returned null and the test died on "expected e2e-perm-..., received null":
red for the right underlying reason, reported as something else entirely.

The session is now confirmed to be the throwaway non-admin on the app's own
page, before the navigation under test.

Also raises the app-boot waits in openApp from 30s to 60s. A cold boot on a
loaded instance went past 30s and turned a run that had already passed on the
same bundle red at .workspace-shell.

* chore(lint): sort the imports the fix added

* chore(format): run prettier over the files this branch touched

---------

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

The replacement assertion anchored the path with ^, which passes locally and
fails in CI. Nextcloud there serves index.php routing, so a WORKING redirect
arrives at /index.php/apps/launchpad/dashboard and the pattern called it a
failure. That is the same mistake as the ${APP_BASE}/?$ pattern it replaced:
an assertion that only passes for one incidental spelling of success.

Now unanchored and built from APP_BASE. It stays false for the unguarded
outcome, which lands on /settings/admin/launchpad under either routing style.

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

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.69.0 to 8.70.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.70.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 [conduction/hydra-gates](https://github.com/ConductionNL/.github) from 1.16.0 to 1.18.0.
- [Release notes](https://github.com/ConductionNL/.github/releases)
- [Commits](ConductionNL/.github@v1.16.0...v1.18.0)

---
updated-dependencies:
- dependency-name: conduction/hydra-gates
  dependency-version: 1.18.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 [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.12 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>
Bumps [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) from 4.1.11 to 5.0.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.0/packages/ui)

---
updated-dependencies:
- dependency-name: "@vitest/ui"
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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>
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 [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>
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>
Bumps [@conduction/nextcloud-vue](https://github.com/ConductionNL/nextcloud-vue) from 2.37.0 to 2.41.1.
- [Release notes](https://github.com/ConductionNL/nextcloud-vue/releases)
- [Changelog](https://github.com/ConductionNL/nextcloud-vue/blob/main/CHANGELOG.md)
- [Commits](ConductionNL/nextcloud-vue@v2.37.0...v2.41.1)

---
updated-dependencies:
- dependency-name: "@conduction/nextcloud-vue"
  dependency-version: 2.41.1
  dependency-type: direct:production
  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 [marked](https://github.com/markedjs/marked) from 18.0.11 to 18.0.12.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](markedjs/marked@v18.0.11...v18.0.12)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 18.0.12
  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>
Two problems, both mine, both from admin-merging a dependabot batch without
resolving it first. `npm ci` on `development` exits 1, so every frontend job
would fail.

1. nc-vue 2.41.1 pins `peerOptional marked@^12.0.0` and launchpad carries
   marked 18, so the graph cannot resolve:

     Found: marked@18.0.12
     Could not resolve: peerOptional marked@^12.0.0 from @conduction/nextcloud-vue@2.41.1

   nc-vue widened that range to `>=12 <19` in 2.43.0, so taking 2.44.1 fixes it
   rather than pinning marked back. dossiq already runs 2.44.1 with marked 18.

2. `@vitest/ui` went to ^5.0.0 alone. It peer-requires EXACTLY `vitest 5.0.0`
   while vitest and @vitest/coverage-v8 stay ^4.1.11. That is a held major and I
   merged it by mistake in a loop that only skipped conflicting PRs; restored to
   ^4.1.11 so the family is one version again.

Verified locally, by exit code, on a CLEAN tree:

    rm -rf node_modules && npm ci   -> 0
    npm run build                   -> 0
    npm run test                    -> 0   (64 files, 708 tests)

    resolved: nc-vue 2.44.1  marked 18.0.12  vitest 4.1.11  @vitest/ui 4.1.11

The lock is updated in place rather than regenerated: deleting it makes npm fail
to resolve the `"pinia": "$pinia"` override.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde and others added 11 commits September 10, 2026 19:15
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…silently wrote nothing (#605)

* feat(showcases): a case handler dashboard, and the settings PUT that silently wrote nothing

Two things, both found by building the dashboard against a live instance.

THE SHOWCASE. `case-handler` is the sixth bundled demo showcase and the
first ROLE one. The five that came before are Dutch fictional
organisations answering "what does an intranet built on this look
like"; this one answers a different question, what one person's working
day looks like on one page: their cases, their tasks, today's agenda and
their unread mail.

It is English because the widgets it places are the fleet's own (a
dossiq case list, the Tasks app, a calendar, unread mail) and those
carry English labels, so a Dutch shell around English content would read
as a half-translation.

Authored as a personal dashboard and re-shaped for shipping: a showcase
must not carry its author, so it goes out as `group_shared` with a null
userId, `view_only`, and no calendar selected. Calendar ids are
per-instance, and pointing a stranger's widget at calendar "1" is worse
than asking them to choose.

REQ-DEMO-001 is widened to admit role showcases, and CORRECTED while I
was in it. It described an `export.json` plus an `nl/` locale tree, and
neither has ever been in any bundled ZIP: the archives are
`manifest.json` + `dashboards/`, which is what ExportService writes and
ImportService reads. Anyone implementing against that paragraph would
have built something no bundled archive matches.

THE SILENT NO-OP. GET /api/admin/settings answers
`allowUserDashboards`; PUT accepted only `allowUserDash`. Five settings
had a read name and a write name and they did not match. An unrecognised
key binds to null, null means "not supplied", so a caller round-tripping
GET into PUT wrote NOTHING and was answered `{"status": "ok"}`.
Measured: setting `allowUserDashboards: true` reported success and left
the flag off, which reads as "this setting will not stick" rather than
as a rejected key. Both spellings are accepted now; the short one wins,
because AdminSettings.vue sends it.

Tests. Two guards over the REAL archives, because every other test in
DemoShowcasesServiceTest writes its own fixture ZIP into a temp dir, so
the files actually shipped in data/demo-showcases were never opened by
any test: an id in BUNDLED_IDS with a missing or malformed archive
passed the whole suite and failed only on a user's instance. Four more
for the alias. All six mutation checked: deleting the archive reddens
both showcase guards by name, and removing the alias lines reddens the
read-side test with `updateSettings(null, null, ...)`, which is the bug
exactly.

phpmd's ExcessiveClassLength deserves its own line: AdminController was
1024 of an allowed 1050 and this change took it to 1058. The comment is
shorter than I first wrote it for that reason, not for taste.

* chore(info.xml): take the em-dashes out of the App Store description

gate-96 (manifest-copy-style) flags both descriptions against voice.md
section 8, which bans em-dashes outright. It is advisory because 20 of
21 fleet apps carry the same debt, and the gate's own note says to fix
it anyway: this is the first prose a stranger reads about the app.

Twenty-six of them, in both languages. The bullets were `**Label** —
text`, which is a label followed by its explanation, so they take a
colon. The two lead sentences joined two independent clauses, so those
become two sentences.

The six em-dashes left are in XML COMMENTS. The gate does not read them
and the writing rules do not cover them: comments explain, they do not
sell.

Also `**Key Features**` to `**Key features**`, same section, sentence
case in headings.

* fix(showcases): one showcase without a preview no longer 500s the whole gallery

CI's Newman lane caught this and no unit test could: GET
/api/admin/demo-showcases answered 500 on this branch.

describeShowcase() resolves each card's thumbnail through
IURLGenerator::imagePath(), which THROWS for an image that does not
exist, and it runs once per showcase inside the listing. I added
case-handler without img/showcases/case-handler.png, so the listing died
on the sixth card and took the other five down with it.

The unit suite was structurally blind to it: its setUp() stands in for
imagePath() with a callback that always returns a string, so the throw
never happens under test.

Two fixes, because they answer different questions:

  - case-handler.png is added, and it is a real thumbnail of the
    dashboard rather than a copy of the 1x1 placeholders the other five
    ship, since it was cheap to make one from the live instance.
  - describeShowcase() catches the RuntimeException, logs it, and
    returns the row with a null thumbnailUrl. A missing preview is a
    cosmetic gap in one card, not a reason to hide every other showcase.

Two guards, one per fix, each mutation checked: deleting the PNG
reddens testEveryBundledIdShipsAPreviewImage by name; narrowing the
catch to LogicException reddens testAMissingPreviewDoesNotFailTheListing
with the RuntimeException. phpcs, phpmd, psalm and phpstan exit 0;
test:all OK, 1646 tests.

* chore(deps): take nextcloud-vue 2.46.0 so the case-handler's New case form is the scoped two-column one

The showcase's My cases widget declares content.formSize, formColumns,
formIncludeFields and formFieldOverrides. CnObjectListWidget reads all
four only from 2.46.0 (nextcloud-vue#1068). On the 2.44.1 development
carries, the New case button opens the WHOLE case schema: about thirty
fields in one column at default width, which is what the live instance
showed while this dashboard was being built.

Five lines of lock. build exit 0, vitest 64 files / 708 tests, lint
exit 0, prettier --check exit 0.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* docs(store): investigate the store plane and propose dashboard sharing

* feat(store): browse a dashboard registry and install a template

* test(store): cover discovery, install and the wiring both defects hid in

* style(store): satisfy phpcs — no inline ternaries or inline docblocks

* fix(l10n): retitle the store page string and translate it for Dutch

* fix(manifest): move the store note into the page note so the schema accepts it

* feat(store): let an administrator connect a dashboard registry from the Sharing tab

* test(store): cover the store page, the registry form and the admin gate in e2e

* fix(lint): declare the registry spec's field stub as a function

* test(store): let an unrouted search fail on its status, not on parsing the 404 page

* docs(store): record the admin form and the e2e coverage in the change

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…e e2e that caught it (#606)

* feat(showcases): a case handler dashboard, and the settings PUT that silently wrote nothing

Two things, both found by building the dashboard against a live instance.

THE SHOWCASE. `case-handler` is the sixth bundled demo showcase and the
first ROLE one. The five that came before are Dutch fictional
organisations answering "what does an intranet built on this look
like"; this one answers a different question, what one person's working
day looks like on one page: their cases, their tasks, today's agenda and
their unread mail.

It is English because the widgets it places are the fleet's own (a
dossiq case list, the Tasks app, a calendar, unread mail) and those
carry English labels, so a Dutch shell around English content would read
as a half-translation.

Authored as a personal dashboard and re-shaped for shipping: a showcase
must not carry its author, so it goes out as `group_shared` with a null
userId, `view_only`, and no calendar selected. Calendar ids are
per-instance, and pointing a stranger's widget at calendar "1" is worse
than asking them to choose.

REQ-DEMO-001 is widened to admit role showcases, and CORRECTED while I
was in it. It described an `export.json` plus an `nl/` locale tree, and
neither has ever been in any bundled ZIP: the archives are
`manifest.json` + `dashboards/`, which is what ExportService writes and
ImportService reads. Anyone implementing against that paragraph would
have built something no bundled archive matches.

THE SILENT NO-OP. GET /api/admin/settings answers
`allowUserDashboards`; PUT accepted only `allowUserDash`. Five settings
had a read name and a write name and they did not match. An unrecognised
key binds to null, null means "not supplied", so a caller round-tripping
GET into PUT wrote NOTHING and was answered `{"status": "ok"}`.
Measured: setting `allowUserDashboards: true` reported success and left
the flag off, which reads as "this setting will not stick" rather than
as a rejected key. Both spellings are accepted now; the short one wins,
because AdminSettings.vue sends it.

Tests. Two guards over the REAL archives, because every other test in
DemoShowcasesServiceTest writes its own fixture ZIP into a temp dir, so
the files actually shipped in data/demo-showcases were never opened by
any test: an id in BUNDLED_IDS with a missing or malformed archive
passed the whole suite and failed only on a user's instance. Four more
for the alias. All six mutation checked: deleting the archive reddens
both showcase guards by name, and removing the alias lines reddens the
read-side test with `updateSettings(null, null, ...)`, which is the bug
exactly.

phpmd's ExcessiveClassLength deserves its own line: AdminController was
1024 of an allowed 1050 and this change took it to 1058. The comment is
shorter than I first wrote it for that reason, not for taste.

* chore(info.xml): take the em-dashes out of the App Store description

gate-96 (manifest-copy-style) flags both descriptions against voice.md
section 8, which bans em-dashes outright. It is advisory because 20 of
21 fleet apps carry the same debt, and the gate's own note says to fix
it anyway: this is the first prose a stranger reads about the app.

Twenty-six of them, in both languages. The bullets were `**Label** —
text`, which is a label followed by its explanation, so they take a
colon. The two lead sentences joined two independent clauses, so those
become two sentences.

The six em-dashes left are in XML COMMENTS. The gate does not read them
and the writing rules do not cover them: comments explain, they do not
sell.

Also `**Key Features**` to `**Key features**`, same section, sentence
case in headings.

* test(e2e): pin the admin-settings key round-trip and the case-handler showcase

* test(e2e): make both specs survive a slow shell load and report every key

* test(e2e): align the case-handler spec with #605 and cite its two new scenarios

* fix(showcases): one showcase without a preview no longer 500s the whole gallery

CI's Newman lane caught this and no unit test could: GET
/api/admin/demo-showcases answered 500 on this branch.

describeShowcase() resolves each card's thumbnail through
IURLGenerator::imagePath(), which THROWS for an image that does not
exist, and it runs once per showcase inside the listing. I added
case-handler without img/showcases/case-handler.png, so the listing died
on the sixth card and took the other five down with it.

The unit suite was structurally blind to it: its setUp() stands in for
imagePath() with a callback that always returns a string, so the throw
never happens under test.

Two fixes, because they answer different questions:

  - case-handler.png is added, and it is a real thumbnail of the
    dashboard rather than a copy of the 1x1 placeholders the other five
    ship, since it was cheap to make one from the live instance.
  - describeShowcase() catches the RuntimeException, logs it, and
    returns the row with a null thumbnailUrl. A missing preview is a
    cosmetic gap in one card, not a reason to hide every other showcase.

Two guards, one per fix, each mutation checked: deleting the PNG
reddens testEveryBundledIdShipsAPreviewImage by name; narrowing the
catch to LogicException reddens testAMissingPreviewDoesNotFailTheListing
with the RuntimeException. phpcs, phpmd, psalm and phpstan exit 0;
test:all OK, 1646 tests.

* test(e2e): install the case-handler showcase once so every leg reports its own verdict

* style(e2e): prettier reflow of the case-handler spec

* chore(deps): take nextcloud-vue 2.46.0 so the case-handler's New case form is the scoped two-column one

The showcase's My cases widget declares content.formSize, formColumns,
formIncludeFields and formFieldOverrides. CnObjectListWidget reads all
four only from 2.46.0 (nextcloud-vue#1068). On the 2.44.1 development
carries, the New case button opens the WHOLE case schema: about thirty
fields in one column at default width, which is what the live instance
showed while this dashboard was being built.

Five lines of lock. build exit 0, vitest 64 files / 708 tests, lint
exit 0, prettier --check exit 0.

* test(e2e): fix the two test bugs the first CI run against the fix exposed

The settings key tests leaked real values into each other once the fix made
the writes land: allowMultipleDashboards=false put the admin at a quota of one,
which renamed the Add-Dashboard control and failed the browser leg's
precondition. Restore the baseline in afterEach and set the flags the browser
leg depends on explicitly.

The showcase lookup read /api/dashboards, which lists only the caller's own
dashboards and so never contains a group_shared showcase. Read
/api/dashboards/visible instead. Also hand the admin's active dashboard back
after the render test, and correct the van-der-berg note: its mail widget
lands, only calendar is skipped.

* fix(showcases): install the widgets LaunchPad renders itself, configured

The case-handler showcase installed as an empty dashboard and reported
success. partitionWidgets() kept only tiles and ids in Nextcloud's own
dashboard registry, and object-list, nc-widget and calendar are LaunchPad
types, so all four were skipped. The e2e in #606 caught it in CI.

LaunchPad's types now live in lib/widget-types.json. The installer keeps a
widget whose type is on it, and the widgetRegistry completeness test reads
the same file, so the two cannot drift. A bare Nextcloud widget id whose app
is not installed is still skipped. A missing or malformed list throws rather
than quietly skipping every LaunchPad widget again.

Behind that sat a second defect: buildPlacement() never copied `content`, so
a kept widget would have landed unconfigured (no register for the case list,
no target for the nc-widget proxies). It is copied now.

The new unit test runs the install on the real bundled archive; the old ones
only opened the zip.

* docs(openspec): say what the settings PUT and the showcase installer now do

admin-settings REQ-ASET-002 described the defect as the contract: its note
said the PUT accepts only the short names. It now accepts both spellings,
the short one wins, and two scenarios pin the GET-to-PUT round trip and the
tie-break; the round-trip e2e cites them.

demo-data-showcases REQ-DEMO-005 is rewritten for the new rule (tiles,
LaunchPad's own types from lib/widget-types.json, registered Nextcloud
widgets; content kept). The whole-spec "pure backend, no UI surface"
exclusion was false, since showcases render in the admin gallery, so it is
gone: scenarios the case-handler e2e covers are cited, and every other one
carries its own reason, naming the PHPUnit or Newman test that pins it or
saying plainly that the behaviour is not implemented or not yet tested.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Dashboard import never landed a dashboard on a real database: the importer
left created_at and updated_at unset, both NOT NULL on the dashboards and
placements tables, so every insert failed and was reported as 'skipped'.
Store installs (#607) hand their payload to the same importer, so none of
those ever landed either. The unit tests mocked the database layer and saw
neither this nor the second defect: an import that did land dropped every
widget's content.

One placement builder is now shared by import and the showcase installer
(they had drifted apart). It carries content, title, icon, style, grid
position and every tile field, and leaves behind fields that point at the
exporting instance. Import sets the timestamps.

Spec corrections: REQ-ASET-001 lists the 12 keys GET actually returns;
REQ-DEMO-008 states who may edit an installed showcase (admins and Editors;
everyone else read-only, with 'Add dashboard' forking an editable copy).

Tests: export/import round trip, a timestamp guard on the rows handed to
the database, a StoreService test through the real importer, the twelve-key
set, three showcase permission tests, and an e2e that exports a configured
widget, imports it through the admin page's own upload, deletes the source
and checks the copy renders its configuration. Mutation-checked, including
in CI. CI: 168 passed, 0 failed, 3 preexisting skips.
…ng spec seed its own dashboard (#611)

Three preexisting defects, found while covering the store plane.

Deleting a dashboard left its user and group shares behind: the delete
cascade (REQ-CSC-002) had no entry for oc_launchpad_dashboard_shares.
Nothing leaked, because occ launchpad:cleanup removes the orphans, but
the rows outlived the dashboard until a sweep.

dashboard-sharing.spec.ts failed 4 of 4 when run alone on a fresh
instance: it assumed the admin already had a personal dashboard. It now
seeds its own through the repo's fixture and removes it afterwards, so
it no longer depends on CI's file order. Four specs also read BASE_URL
directly instead of going through support/baseUrl.ts, so a local run
with only the documented PLAYWRIGHT_BASE_URL failed them with 'Invalid
URL'; they now resolve it the documented way.

l10n: #559 added 'Admin settings' to en.json and nl.json only, and to no
.js file at all, which is what the browser reads. 35 of 36 locales were
missing it for six days. 28 take Nextcloud's own translation of the same
label, nl keeps LaunchPad's wording, and 7 locales with no Nextcloud
translation carry the English source, which is what their users already
saw. The parity check now runs on every push as a warning, not a gate.

Verified: check:strict exit 0, lint and format exit 0, the spec alone on
a fresh instance 4/4 (and 4/4 red with the seeding removed), CI 169
passed with 3 preexisting skips.
…ee import defects the tests found (#616)

The export-import spec carried one blanket e2e exclusion over ~40
scenarios. Each of its 48 scenarios now has its own verdict: 18 cited by
new e2e (import validation and collisions, site export, schema
versioning, partial import), 1 by #612's round trip, and 29 reason-bearing
exclusions (10 not implemented, checked against every commit and the
original task list; 12 with no browser surface; 5 that cannot be arranged
on a shared instance; 2 other). Every exclusion names the unit test that
pins the behaviour, or says there is none.

Defects found and fixed, each with a test:
- a corrupt dashboard file was reported with a blank ID and a misleading
  message; it now names the file;
- a dashboard skipped for a missing field was reported under a freshly
  generated ID instead of the archive's own;
- the admin page replaced the server's refusal reason with 'Import failed.
  Please try again.'; it now shows the reason, and an archive from a newer
  schema version tells the admin to upgrade, as the spec requires.

Mutation-checked 11 ways in an isolated instance (with PHP opcache
revalidation off, after two mutations first read green because the
container cached the unbroken code). CI: 181 passed, 0 failed, 3
preexisting skips; all 12 new e2e ran. check:strict exit 0 (1696 tests),
lint and format exit 0, scoped gates exit 0 (54/54 applicable).
Adds openspec/changes/e2e-coverage-programme: 2,834 scenarios across 78
specs, 5% cited by a Playwright test, 46 specs behind a spec-level
blanket @e2e exclude. Five tiers (safety, data integrity, core surfaces,
widgets, admin and operations), one PR per spec, a definition of done,
refusal-test rules, and exact counts per spec measured 2026-09-11. No
requirements change. Tier 0 is in progress.
…wo silent write failures (#617)

#612 shipped an importer that never set created_at or updated_at, so every
insert failed and was reported as 'skipped'. The unit tests mocked the
database and saw none of it. The lane that would have caught it did not
run: CI's PHPUnit used stub mode, so #611's database test SKIPPED there.
One line in phpunit.xml (PHPUNIT_USE_NC_BOOTSTRAP=1) makes CI boot the
Nextcloud it already installs. Head run: 1705 tests, 0 skipped, on all six
PHP 8.3/8.4 x NC 32/33/34 legs. A plain clone still falls back to stubs.

Four write paths now have real-database tests: dashboard create, dashboard
update, add widget, share a dashboard, plus the share delete cascade. Each
was mutation-checked by leaving a required column unset: the database test
reddens (SQLSTATE 23502) while the mocked tests stay green, 24 of 25, 14 of
14, 7 of 7, 21 of 21, 12 of 12 respectively.

Checking all 33 insert sites against the 73 required columns found two more
live instances of the same bug:
- Confluence import never saved a dashboard. The placeholder widget carried
  no timestamps, so every page rolled back and the command still exited 0
  saying 'Imported 0 dashboards, skipped 1'. The 26 Confluence tests only
  covered parsing.
- A role permission or layout default saved without a name hit the database
  constraint. It is now refused as a missing field before the insert.

CI on the merged head: 38 pass, 6 skipping, 0 failing; e2e 169 passed.
* feat(e2e): refuse the shared instance unless the run names it

The box runs one Nextcloud on localhost:8080 that bind-mounts everybody's
checkout and holds data colleagues are working on. This suite seeds and
deletes objects, so a run aimed there edits someone else's environment.

tests/e2e/shared-instance.ts refuses loopback port 80 or 8080 unless the
run set LAUNCHPAD_E2E_ALLOW_SHARED_INSTANCE or E2E_ALLOW_SHARED_INSTANCE
to that same origin. CI is exempt, where 8080 is the job's own throwaway
instance.

The guard sits on the resolved value in tests/e2e/support/baseUrl.ts, the
one module all three playwright configs import, so there is a single place
a target enters the suite. A rig on its own port is unaffected.

* style(e2e): format the shared-instance guard the way the format check wants

The format leg runs prettier --check over the tree and it read all four
files this branch touches as dirty, eslint green or not. They are two
separate checks.

The guard and its test are re-copied from the corrected fleet template,
which is prettier-clean at source; the two edited files were clean before
this branch and are clean again. No file this branch does not touch was
reformatted. Ten unit cases still pass, with and without CI set.

* fix(e2e): move the guard's unit test out of the playwright testDir

The E2E job on this branch failed with 'Vitest cannot be imported in a
CommonJS module using require()', and the reason is worse than the message.

Playwright's default testMatch is **/*.@(spec|test).?(c|m)[jt]s?(x), so
tests/e2e/shared-instance.test.ts was COLLECTED BY PLAYWRIGHT, threw on its
vitest import, and took the whole run down. Measured on this checkout with the
file in place: 'Total: 0 tests in 0 files'. With it moved: 184 tests in 39
files. A tally of zero reads like an empty suite, not like one poisoned file.

The test now lives in tests/vitest/, which is where the rest of the fleet puts
it, and vitest's include points there instead of at tests/e2e/**/*.test.ts.

Verified on the CI pins: 10 of 10 passing both with CI=true and without, the
full unit suite 67 files and 727 tests all passing, playwright collecting 184
tests in 39 files, prettier and eslint clean on every file this commit
touches.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant