Skip to content

release: develop → master cut for Laravel 13 / PHP 8.5#503

Merged
thekevinm merged 66 commits into
mainfrom
develop
May 27, 2026
Merged

release: develop → master cut for Laravel 13 / PHP 8.5#503
thekevinm merged 66 commits into
mainfrom
develop

Conversation

@thekevinm
Copy link
Copy Markdown
Contributor

Release prep: cut develop into master/main

Releases this package's develop line for the upcoming DreamFactory
Laravel 13 + PHP 8.5
release.

Verification

The develop HEAD on this branch is the SHA locked into the L13/PHP 8.5
test bundle that the team has been smoking. Full end-to-end smoke
passed 2026-05-26
: PHP 8.5.5, Laravel 13.11.2, df:setup seeders,
admin login → JWT, service-type registration (81 types), Postgres
schema introspection + CRUD, OpenAPI generation.

Coordinated release PRs

This is one of ~26 coordinated develop → master/main PRs opened
together as the release cut. Suggested merge order:

  1. Foundation: df-core → df-system → df-user → df-database
  2. Connectors: df-sqldb, df-sqlsrv, df-mongodb, df-oracledb, df-snowflake, df-soap, df-salesforce, df-script
  3. Auth: df-oauth → df-adldap, df-azure-ad, df-oidc, df-saml
  4. Services: df-email, df-file, df-cache, df-limits, df-logger, df-scheduler
  5. AI tier: df-ai → df-ai-chat → df-mcp-server
  6. Admin UI: df-admin-interface
  7. Host app: dreamfactory/dreamfactory (released last)

Customer upgrade path

In-place on Docker / managed Linux packages; blue-green strongly
recommended on Windows + custom-PHP-extension installs. Pre-flight
checklist in the release notes.

Read-only visualization of a role's effective service+component access.
Renders allowed services with verbs and a collapsible "denied" list.

Foundation for the AI admin UI: surfaces "what can the AI see" — same
panel will be embedded in the AI Chat service form so admins can read
the scope before assigning a role to an AI.

Adds /api-connections/role-based-access/:id/scope.
Pre-commit hook formatted files in the previous commit but the writes
weren't re-staged. Catching them up here.
Use a local const + explicit null/undefined guard so TS narrows the
type before passing roleId into the role service.
- Adds "View scope" row action on the roles list (eye icon).
- Adds "View scope" button on the role detail page in edit mode.
- Adds an ng-content slot in df-role-scope's error block so pages
  can project a recovery link; standalone scope page projects
  "Back to roles list" so an invalid id doesn't dead-end.
Keys like 'roleScope.heading' were rendering as raw text because DF's
transloco scope provider exposes scoped keys as 'roles.<key>'. Updated
every reference (component, page wrapper, role detail button, list
action) to use the 'roles.' prefix.

Bumped font sizes, padding, gaps and used dark-mode-friendly colors so
the panel visually matches the rest of the DF admin UI rather than
looking like a tiny widget.
The single /ai route was filtering services to the MCP group only.
Splitting into three sub-routes so connections, chat services, and MCP
servers each get their own list/create/edit UI without inventing
new components — they all reuse ServiceRoutes filtered by group.

  /ai
    /connections  -> service-type group "AI"      (ai_connection)
    /chat-services-> service-type group "AI Chat" (ai_chat)
    /mcp          -> service-type group "MCP"     (mcp)

Sidebar picks up the children automatically via transformRoutes.
Adds nav i18n keys + extends the always-allowed route list so
non-admin tabs that allow ROUTES.AI inherit the children.

Empty /ai redirects to /ai/connections as the landing tab.
When a service of type ai_chat is being created/edited and ai_role_id
is set, render the role-scope panel inside the config expansion panel.

The panel reacts to ai_role_id changes via @Input/ngOnChanges so an
admin picking different roles sees the AI's effective data scope
update inline — answering "what will this AI be able to see?" before
the service is saved.

Visible only when type === 'ai_chat'; the same form template renders
every non-network/non-script service type, so the panel is gated to
avoid showing on all other services.
New module under src/app/adf-ai-chat/ providing the in-browser chat
experience for ai_chat services:

- df-ai-chat (main): service picker (when >1 chat service exists),
  session sidebar, message feed, polling-based "thinking" updates,
  data-scope display in the header.
- df-chat-session-list: session sidebar with new-chat button.
- df-chat-message: per-message render dispatching by role
  (user / assistant / tool / system) with bubble styling and
  optional usage stats.
- df-chat-tool-result: collapsible card for tool execution results,
  pretty-prints JSON, flags errors.
- df-chat-input: textarea with send button + Enter-to-send.
- ai-chat.service: HTTP client for /api/v2/{service}/session*.

Send-message flow: optimistic user-bubble append, 1s polling against
GET /session/{id} while awaiting the assistant response so tool calls
appear as the agentic loop produces them; final reconciliation on
completion. SSE streaming is intentionally a fast-follow.

Routes: /ai/chat and /ai/chat/:sessionId both point at the same
component; sessionId param triggers an open-on-load.
…sations"

The role-scope panel embedded in the chat-service form was reading
getConfigControl('ai_role_id').value at template-init time — but that
control doesn't exist until the user picks a Service Type and the
config FormGroup is built. The null deref threw an error during change
detection, leaving the Service Type dropdown stuck open and the form
unable to populate fields.

Adds an aiRoleId getter that null-checks the form path and returns a
number-or-null so the template can use it without optional chaining
(which Angular's strict template type-check warns on because the cast
in getConfigControl claims non-null).

Also rename "Chat" → "Conversations" in the AI sub-nav so the user
doesn't see "Chat Services" and "Chat" as visually-paired siblings;
"Chat Services" is the admin config and "Conversations" is the
in-browser chat experience.
When creating an AI Chat service, the form needs an integer
ai_service_id and ai_role_id — admins were guessing what to type.

The prereqs panel appears above the config when type === 'ai_chat'
and:
- Lists existing AI Connections with their IDs (copy/paste source).
- Lists existing Roles with their IDs and a "view scope" link to the
  read-only role-scope page.
- Flags missing prerequisites in red with a "Create one now" button.
- Links to /ai/connections/create and
  /api-connections/role-based-access/create.

Removes the "stare at an empty integer field and guess" UX.
…mart scroll

- df-ai-chat-prereqs now writes selections back to the form (selectConnection
  / selectRole). The form's integer ai_service_id and ai_role_id fields are
  hidden when type=ai_chat — admins click chips instead of typing IDs.
- df-ai-test-connection: stroked button on the AI Connection form that POSTs
  to /_internal/ai/test-connection with the current config and surfaces
  pass/fail + model count inline. Lets admins validate provider/key without
  saving first.
- Chat tool-result cards auto-expand when is_error so failures aren't hidden.
- Optimistic user-message id uses a monotonic counter (not -Date.now()) so
  rapid sends can't collide.
- Chat scroll: track whether the user is pinned to the bottom; only auto-
  scroll on new messages when they are. Sending always pins. Opening a
  session always pins.
- Assistant content: split fenced code blocks (```...```) into <pre><code>
  segments via pure-text binding. No innerHTML, so no XSS surface.
7 new spec files, 56 tests, all green under jest.config.ci.js:

- df-role-scope: bitmask decoding (GET=1, all=31, etc.), denied list,
  '*' wildcard for empty component, error handling, hasRoleId guard,
  unknown service-id skip. Pure-class tests (no TestBed) so the
  transloco-pipe template doesn't drag in @ngneat/transloco's ESM.
- ai-chat.service: HttpClientTestingModule covering listChatServices,
  list/get/create sessions, sendMessage, deleteSession URL+method+body.
- df-chat-input: canSend gating (busy/disabled/empty/whitespace),
  Enter-to-send vs Shift+Enter, trimmed emit + field clear.
- df-chat-message: segments parser for fenced code blocks, hasToolCalls
  / hasUsage flags.
- df-chat-tool-result: auto-expand-on-error, JSON pretty-print, blank
  fallback.
- df-ai-chat-prereqs: parallel fetch + failure tolerance, selection
  emits.
- df-ai-test-connection: provider-required guard, POST shape, success +
  error response handling, plain-string model labels.

jest.config.js: map `flat` (transloco's ESM dep) and add it to
transformIgnorePatterns so role-scope's transloco import resolves
under Jest. jest.config.ci.js: add the 7 new specs to the curated
testMatch list.
… switch

The previous implementation used a single shared pollTimer slot. Two
concurrent sends would step on each other: send-2's startPolling would
clear send-1's timer, and send-1's finalize would clear send-2's. The
chat would silently stop refreshing.

Track in-flight sends with a counter:
- send increments inFlightSends and starts the timer (idempotent).
- finalize decrements; only stops polling when count reaches 0.
- selectService and deleteSession force a clean reset (active session
  going away invalidates any in-progress polling).

Adds df-ai-chat.poll.spec.ts covering the three tracked transitions.
Surfaces what the chat module already captures (per-session token
counts + tool-call counts + user/role/service IDs) as a usage
analytics view. No backend changes — pure client-side aggregation
off the existing endpoints. Phase 1 swaps in a backend rollup
endpoint when scale demands it.

New module under src/app/adf-ai-usage/:

- df-ai-usage (main): time-range filter (24h/7d/30d/all), refresh
  button, summary cards, time-series + grouped bars + cost.
- df-usage-summary: 6-card top line (sessions, in/out/total tokens,
  tool calls, avg/session).
- df-usage-stacked-area: hand-rolled SVG stacked-area chart of input
  vs output tokens by day with grid + axis labels (no chart library).
- df-usage-bars: grouped bar chart for top users / roles / services
  with input/output split, "N of M" overflow indicator, optional
  click-through (services jump to /ai/chat?service=).
- df-cost-estimator: per-provider breakdown using stored token counts
  multiplied by editable per-1k rates (defaults for anthropic/openai/
  xai/ollama/openai_compatible). Inline rate edits recompute live.
- usage.service: parallel forkJoin loader — chat services, sessions
  for each, users + roles + AI Connection providers (for the cost
  attribution lookup).

Aggregation utilities (groupBy, summarize, timeSeries, filterByRange)
have full Jest coverage. Cost utilities (estimateCost, formatUSD,
DEFAULT_RATES) have full coverage. 25 new tests, 135 total in the CI
suite.

Sidebar: AI -> Usage. Empty state links straight to /ai/chat to
generate data. Anything cost-attribution-shaped is a one-day Phase 1
backend lift on top of this.
The ID badges were misread as "you must know the ID" rather than
decorative. Removed them — the chip now shows just the human label
and a checkmark on selection. Added an italic hint above the chip
list pointing the admin at the click-to-select behavior.

Renamed the inline scope link from "scope" to "what can this role
see?" so it reads as the question it answers.
…i_chat

The previous *ngSwitchCase="true" was comparing the string "true" to
a boolean true and never matched, so the integer fields rendered
alongside the prereqs panel and the form required users to type IDs.
Replaced with a straightforward *ngIf negation.

Also bumps the prereqs panel typography (1rem base, 1.2rem header,
0.95rem chips) — the original sizes read as a footer rather than the
load-bearing setup step it actually is.
Re-frames the dashboard around the AI gateway story: every AI request
through DreamFactory (REST calls from client apps + the in-DF chat UI)
is logged to ai_usage_log by df-ai. The dashboard now reads that
single source instead of the chat-only ai_chat_sessions table.

Backend call returns totals + by_service / by_user / by_role /
by_provider / by_model / by_resource + daily series. Client-side
hydration adds human-readable user / role / service labels.

UI changes:
- "Sessions" -> "Requests" in the summary
- New "Errors" card (red when > 0) and "Avg latency" card
- New bar charts: by provider, by endpoint (resource type)
- "By chat service" -> "By AI Connection" — connection IS the gateway
- Empty state copy reflects the new framing ("every AI request through
  DreamFactory" not "chat sessions")

Old session-based aggregation utilities still exist for now —
unused but not removed. Will be deleted once the pivot stabilizes.
Three usability improvements + one bug fix tying them together:

1. **df-ai-model-picker**: drop-down model selection on the AI Connection
   form. Hits POST /_internal/ai/test-connection with the form's current
   provider+key+url and lists every model the provider returns. Selecting
   writes to config.default_model. Includes a "type custom" toggle for
   admins who know the model id but don't want to fetch (offline / model
   not yet listed by the provider).

2. **/_internal interceptor fix**: session-token.interceptor only
   attached the X-DreamFactory-Session-Token header to /api/* URLs.
   That meant Test Connection and the org-wide usage endpoint
   silently went unauth'd from the UI ("Admin access required"
   error). Extended to /_internal/* too.

3. **Test Connection validation**: provider-specific upfront checks
   so admins get "openai_compatible requires a Base URL" instead of
   the cURL "No host part in the URL" the provider was bubbling up.

4. **Nav reframe**: routes order Connections first (gateway primary),
   then Usage (visibility), then Conversations (built-in playground),
   then MCP (data exposure), then Chat Services last (admin-only
   setup machinery for the chat UI). Sidebar auto-generates from
   route order so this surfaces immediately.

5. **Cost defaults**: openai_compatible defaults to $0/1k since the
   common case is a self-hosted endpoint. Admins can override per-row
   in the dashboard if modeling GPU amortization. ollama already $0.

Plus the form-bug fix from earlier in the day still applies — ai_chat
form correctly hides ai_service_id/ai_role_id integer fields, and
ai_connection form now hides default_model in favor of the picker.
nicdavidson and others added 29 commits April 28, 2026 11:06
…, MCP section

Rebuild the /ai/usage page to answer team-lead-grade questions about AI
spend through DreamFactory. The page is now a single-pane Gateway view
that overlays paid AI provider calls and inbound MCP traffic.

Performance + theming foundations
- Cache derived views in instance fields populated from a single
  recompute() in the API-response handler, instead of getter-bound
  template inputs. Getter-bound @inputs handed child components new
  array/Map references on every change-detection tick, which made
  ngOnChanges/rebuild loops pin a CPU core and (on weaker machines)
  killed the browser tab.
- Wrap every panel in mat-card so the Gateway view inherits the app's
  typography, dark-mode, and surface chrome. Convert all rem sizes
  to px since the app's `html { font-size: 62.5% }` makes 1rem = 10px,
  not the 16px the components were originally written against.

Filters + attribution
- Multi-select filter row (provider, connection, model, app/API key,
  user, role, endpoint, status); sent as repeated query params to
  /_internal/ai/usage. Active selections render as removable chips
  with a clear-all button. Clicking any bars-chart row drills into
  that filter (provider, user, role, app, connection).
- New "By app / API key" panel powered by ai_usage_log.app_id, plus
  a By model panel.
- Estimated cost summary tile showing total_cost_usd from the
  backend (which is computed at log-time from the per-service rate
  sheet, so it matches the customer's provider invoice).

MCP section
- Loads /_internal/ai/mcp-usage in parallel with /_internal/ai/usage.
- Renders summary + 6 panels (clients, tools, users, services,
  apps, methods) under a collapsible "External MCP traffic"
  section with an explicit disclaimer: token cost is borne by the
  calling AI agent, not billed to DreamFactory.
- Bytes (in/out) replaces tokens in MCP rows since MCP is data
  proxying, not LLM inference.
…ators

Three small cleanups in the Gateway dashboard:

EMPTY_FILTERS was a shared singleton — `{ ...EMPTY_FILTERS }` only
shallow-copies, so all consumers ended up sharing the inner
provider/service_id/etc. arrays. clearFilters() worked around this by
wiping the arrays then re-creating them. Replaced with a
createEmptyFilters() factory; clearFilters is now one assignment +
refresh, and the caller can't accidentally mutate sibling state.

toggleFilter / removeChip dropped their nested generics — the
keyof UsageFilters constraint forced us to cast values to `never`,
which was a type-honesty smell. New signature takes
(keyof UsageFilters, string | number) directly; one cast (Array<string|number>)
remains where TS can't unify the per-key array type.

Cost estimator title + hint: clarified that the panel is a what-if
calculator, not the source-of-truth for spend. The summary tile's
"Estimated cost" comes from the server-stored cost_usd column (computed
at log-time from each AI Connection's configured rate sheet); the
estimator lets you experiment with different rates against the same
token counts without editing your services. Edits don't persist.
Tooltips
- Help icon on the page title explaining what the dashboard tracks.
- matTooltip on every summary tile (Requests, Input/Output/Total tokens,
  Errors, Avg latency, Estimated cost) — explains how each metric is
  computed and what it means for a team lead.
- matTooltip on every filter field describing what dimension it filters by.
- Help icon next to each bars-panel title (new optional `hint` input on
  df-usage-bars) — explains what the panel shows and how to read it.
- matTooltip on every MCP summary tile and on the Refresh / time-range
  controls.

Visual style pass
- Two section headers (DreamFactory AI usage / External MCP traffic)
  with an h3 + subtitle pattern instead of the old dashed border-top —
  cleaner rhythm between sections and explicit framing of what each
  section means.
- "Filters" label + drill-down hint above the filter row so users
  understand the row before they touch it. Active-chip row gains a
  thin top divider.
- MCP summary uses tighter spacing than the AI summary (secondary
  content) and a slightly smaller hero number — visual hierarchy
  signals "auxiliary data" without needing words.
- Disclaimer banner on the MCP section reworded to be less wall-of-text;
  the long form lives in the help tooltip on the section title.
- Dark-mode coverage on every new element.
…default-rates from backend

Frontend half of the dashboard upgrade round.

Latency tile (replaces the avg-latency tile)
- Shows p50 prominently with p95/p99 as a smaller compact row underneath.
- Tooltip explains why p50/p95 matter and that avg lies in the presence
  of slow tails.
- p50 also drives the period-over-period delta arrow (representative
  of the typical experience, unlike avg).

Error breakdown panel
- New "Error breakdown" bars panel in the AI section. Only renders when
  errors > 0. Maps the backend's 10 error classes to human labels
  (Timeout / Rate-limited / Auth failure / Model not found / Context
  too long / Connectivity / Provider 5xx / Provider 4xx / Other /
  Unknown).

Period comparison + sparklines on every summary tile
- Service now sends compare=1 by default; bundle.raw.previous gets the
  immediately-preceding window's totals.
- Each summary tile gets a delta arrow (↑/↓) + signed % change, color-
  coded by metric polarity (errors/cost/latency = red on up; tokens/
  requests = neutral).
- New df-sparkline standalone component renders a tiny 24px-tall inline
  SVG of the per-day series alongside each tile. Just polyline + filled
  area, no axes / labels — visual texture, not data.
- Sparklines pull straight out of the existing daily series, so no
  extra API call.

Budgets
- Top-of-page warning banner if any AI Connection is projected to
  exceed its monthly_budget_usd. Lists the offenders with current spend
  / projection / % of budget.
- New "Budget status" card in the AI section with one row per
  connection: progress bar showing spent-to-date + a hatched extension
  showing projected month-end. Bar turns red and the amount goes red
  when projected to overshoot.

DEFAULT_RATES single source of truth
- bundle.raw.default_rates now comes from the backend's
  UsageRates::DEFAULT_RATES; cost-estimator reads this via a new
  defaultRates input instead of importing a hardcoded const.
- utils/cost.ts keeps a small FALLBACK_RATES const used only when the
  bundle hasn't loaded yet — annotated NOT to be the source of truth.
…charts + expensive calls

Builds out the AI Usage Analytics page to surface the full analytical
output the backend now produces (df-ai PR #3 added five new keys to
/_internal/ai/usage). Answers the question every CTO asks first:
"where is my money being spent on AI?"

New visible sections (between the existing time-series and the bars
grid, under a "Where is my money going?" section header):

- Four stacked cost-over-time charts (custom SVG, no library dep):
    Cost by model      — top 10 models by spend + Other layer
    Cost by provider   — Anthropic / OpenAI / xAI / local breakdown
    Cost by user       — top 10 spenders + Other (id → display name)
    Cost by app/key    — per-tenant cost visibility (id → app name)
  Each layer is auto-coloured from a fixed palette; the long-tail
  "Other" bucket is always rendered last in muted gray so legends are
  stable across refreshes.

- Top 10 most expensive calls table — full attribution per row
  (cost, model, user, app, connection, tokens in/out, latency, status,
  when). Rows tinted by status (error red, partial amber). The
  drill-down hook for outliers averages otherwise hide.

Augmentations to existing widgets:

- by_model bars now show effective per-1k-token rate as a small purple
  badge (e.g. "$0.0046/1k") next to the totals — answers "is this
  premium model worth the rate" without the user doing math
- New "Partials" summary tile (only renders when partials > 0):
  streaming requests where the client disconnected before the model
  finished. Tokens delivered are still billed; tracked separately so
  an uptick (usually network/timeout) doesn't masquerade as a healthy
  success rate. Tinted amber to match severity vs the red error tile.

Components added:

- df-cost-by-dimension/  — generic stacked-area for series_by_<dim>
  data. One component, four instances (model/provider/user/app). Pure
  SVG, deterministic colour palette, Other bucket sentinel ('__other__')
  matches the backend wire contract. Renders empty-state cleanly.
- df-expensive-calls/    — Material-table-style display for the top-N
  rows. Status pill (success/error/partial) with semantic colors,
  monospace cost column, hover tooltip for full timestamp.

Type extensions:

- UsageResponse: + partials, + series_by_{model,user,app,provider},
  + most_expensive_calls
- ModelRowRaw: + cost_per_1k_tokens
- GroupRow: + costPer1kTokens (optional, populated only on by_model)
- UsageSummary: + partials
- New: DimensionSeriesRowRaw, ExpensiveCallRowRaw, OTHER_BUCKET sentinel

The backend's series_by_X rows are normalized HERE in the parent
component (id → display label via bundle.users / bundle.apps maps), so
the chart component itself stays generic and reusable.

dist/ rebuilt fresh — `ng build` completed clean (warnings only from
the pre-existing swagger-ui CommonJS deps; no new ones introduced).

Verified live against /_internal/ai/usage on the dev box: 31 requests,
$0.0095 spend, 5 model rows with cost_per_1k populated (sonnet at
$0.0046/1k vs haiku at $0.0034/1k — the exact "should we move to the
cheaper model" answer), 5 series_by_model rows including 1 streamed.
…fallback

Two issues surfaced during CEO-meeting QA on the AI screens.

1. Font sizes too small across the AI Chat UI, AI Chat services config,
and the shared AI form components (test-connection, model-picker,
allowed-roles). Cause: those components used rem-based sizes (0.875rem,
1rem, 1.2rem, etc.) authored against the standard 16px html baseline.
DF's html sets font-size: 62.5% so 1rem = 10px — those values rendered
at 8.75px / 10px / 12px instead of the intended 14px / 16px / 19px.
The AI Usage tab already uses absolute px values and looks correct;
this brings every other AI surface into the same convention.

Bulk-converted across:
  src/app/adf-ai-chat/**/*.{ts,scss}            (5 files)
  src/app/shared/components/df-ai-test-connection/
  src/app/shared/components/df-ai-model-picker/
  src/app/shared/components/df-ai-allowed-roles/

Conversion table (rem → px to preserve the originally-intended size on
DF's 62.5% html baseline):
  0.7rem  → 11px      0.95rem → 15px      1.1rem  → 18px
  0.75rem → 12px      1rem    → 16px      1.2rem  → 19px
  0.8125rem → 13px    1.05rem → 17px      1.25rem → 20px
  0.875rem → 14px

Zero remaining rem font-size declarations across the AI screens.

2. Test Connection button on AI Connections returned "401 Invalid API
key" for openai_compatible providers (e.g. local llama-server with
--api-key enabled). Root cause: the form doesn't redisplay saved
secrets, so when the admin clicked Test on an existing connection, the
api_key field was blank and the backend made an unauthenticated
listModels() call. Fix has two halves:

  - Backend (df-ai PR #3 follow-up commit): testConnection accepts
    `service_id` and falls back to the saved AiConnectionConfig for
    any field the form left blank. Provider takes form values when
    present, saved values otherwise.
  - Frontend (this commit): df-ai-test-connection has a new
    @input() serviceId; df-service-details passes serviceData.id when
    edit=true; the runtime validator skips the "missing api_key"
    rejection when serviceId is set (the backend will fall back).

Also updated the validation message to mention "Existing connections
fall back to the saved key automatically" so admins know they can
just click Test on a saved connection without retyping.

dist/ rebuilt clean (warnings only from pre-existing swagger-ui CommonJS
deps; no new ones).
The April 2026 security update bumped swagger-ui 4.15.5 -> ^5.32.2
without migrating df-api-docs.component.ts to the v5 calling
convention. v5 introduced breaking changes in bundle layout and
internal call paths, causing every service's API docs to throw
"TypeError: o is not a function" from inside the swagger-ui bundle.

Pinning back to 4.15.5 restores the working UI as a stopgap. The
patched DOMPurify XSS / Handlebars JS injection CVEs return — low
practical risk for the admin UI (specs come from trusted backends),
but a proper v5 migration is still owed.

Rebuilt dist/ artifacts included.
…ategy href

RouterTestingModule uses PathLocationStrategy by default, so [routerLink]
renders as /api-connections in the test bed, not #/api-connections. The
production app provides withHashLocation() in main.ts, but the spec's
intent is "tile is an anchor (supports middle-click)", not "verify the
hash strategy" — so align the expectation with what TestBed actually
renders.
@thekevinm thekevinm merged commit 3b40e1a into main May 27, 2026
1 check 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.

4 participants