fix(ci): repair master failures in CLI bind test, addons UI build and e2e (#12876)

* test(cli): use a globally routable IPv6 address in the public bind spec

2001:db8::/32 is the IPv6 documentation prefix and is now classed as
non-public, so the bind is trusted and the spec failed. Use a public
address from 2606:4700:4700::/48 instead.

* fix(addons): rename installFlow.js so it no longer clashes with InstallFlow.jsx

installFlow.js and InstallFlow.jsx differ only by case. On a
case-insensitive filesystem the bundler can resolve
`import FlowStatus from './InstallFlow'` to the .js module, which has
no default export, and the macOS build fails with MISSING_EXPORT.

Rename the module to installAndTurnOn.js, after its main export, and
update its two importers. No code changes.

Assisted-by: Claude Code:claude-sonnet-5-5

* fix(addons): keep Reload on the page of an add-on that is turned off

The clearer empty states for a stopped add-on dropped the Reload button
from the unavailable page, so a user who turned the add-on back on had
no way to mount it again without leaving the page. Render the button in
the actions of the off state, next to Turn on.

* test(ui): serve the Voice add-on in the diarization profiles spec

The remembered voices link now opens the page of the Voice add-on, which
the core build no longer draws. Install the built Voice bundle next to
Studio, as the other specs do for their add-ons, and build it in the
global setup.

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
mudler-agentandEttore Di Giacinto authored and GitHub committed 2026-10-24 10:05:30 +02:00
1 parent 795d9dac7b
commit 994d3acd4d
7 files changed
+11 -9

No files matched your search

+1 -1
View File
@@ -88,7 +88,7 @@ var _ = Describe("requireAuthOrTrustedBind", func() {
})
It("rejects public IPv6", func() {
Expect(requireAuthOrTrustedBind("[2001:db8::1]:8080", false, false)).To(HaveOccurred())
Expect(requireAuthOrTrustedBind("[2606:4700:4700::1111]:8080", false, false)).To(HaveOccurred())
})
})
@@ -27,8 +27,9 @@ async function setup(page, permission = true, diarizationPermission = true) {
return route.fulfill({ json: data })
})
await page.route('**/v1/audio/diarization', route => route.fulfill({ json: result }))
// Studio is an add-on. Installed after the catch-all above, so its routes win.
await installStudioAddons(page, { ids: ['studio'] })
// Studio and Voice are add-ons. Installed after the catch-all above, so their routes win.
// The remembered-voices link opens the Voice add-on page.
await installStudioAddons(page, { ids: ['studio', 'voice'] })
await page.goto('/app/diarization')
if (!diarizationPermission) return
await expect(page.getByRole('button', { name: 'diarizer', exact: true })).toBeVisible()
+4 -4
View File
@@ -24,9 +24,9 @@ export async function buildAddonBundles(ids) {
for (const id of ids) await import(path.join(addons, id, 'ui/build.mjs')).then(m => m.build())
}
// The add-ons the specs serve from their build output: Studio and Talk through
// installStudioAddons, Face and Fine-tuning through their own routes.
export async function buildStudioAddons(ids = ['studio', 'talk', 'face', 'fine-tuning']) {
// The add-ons the specs serve from their build output: Studio, Talk and Voice
// through installStudioAddons, Face and Fine-tuning through their own routes.
export async function buildStudioAddons(ids = ['studio', 'talk', 'voice', 'face', 'fine-tuning']) {
await buildAddonBundles(ids)
process.env[BUILT] = '1'
}
@@ -55,7 +55,7 @@ export function packagedApp(id, over = {}) {
// Returns the state of mockAddons.
export async function installStudioAddons(page, { ids = ['studio', 'talk'], ...options } = {}) {
const dirs = await Promise.all(ids.map(build))
await page.route(/\/api\/addons\/(studio|talk)\/assets\//, route => {
await page.route(/\/api\/addons\/(studio|talk|voice)\/assets\//, route => {
const parts = new URL(route.request().url()).pathname.split('/')
const id = parts[3]
const file = parts.slice(6).join('/')
@@ -181,6 +181,7 @@ export default function AddonRoute() {
<Icon name={working ? 'spinner' : 'toggle-on'} spin={working} /> {t('apps.start')}
</button>
)}
{extra}
{outcome && outcome.outcome && describeOutcome(outcome.outcome, t, outcome.error) && (
<span className="ad-card__note" data-tone="error" role="alert">{describeOutcome(outcome.outcome, t, outcome.error)}</span>
)}
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createActionBook, effectOutcome } from './actionBook'
import { runLifecycle, settleOperation } from './lifecycleFlow'
import { runInstallAndTurnOn } from './installFlow'
import { runInstallAndTurnOn } from './installAndTurnOn'
// Runs user actions that change an add-on, one operation identity per action.
//
@@ -17,7 +17,7 @@ import { runLifecycle, settleOperation } from '../../src/components/addons/lifec
import { createActionBook, effectOutcome } from '../../src/components/addons/actionBook.js'
import { describeError, describeOutcome } from '../../src/components/addons/errors.js'
import { backendState, libraryOrigin, shortDigest as shortBackendDigest } from '../../src/utils/backendRows.js'
import { runInstallAndTurnOn } from '../../src/components/addons/installFlow.js'
import { runInstallAndTurnOn } from '../../src/components/addons/installAndTurnOn.js'
import { installSteps, STEP_ORDER } from '../../src/components/addons/installSteps.js'
const json = (status, body) => ({ ok: status >= 200 && status < 300, status, json: async () => body })