fix(ci): repair the failing master checks (#13051)

* test(ui): fix stale and timing-bound Home and NotFound specs

The Home slash menu spec dropped /skill from its expected list, but the
test server always keeps built-in skills, so /skill is offered. Restore
the expected list.

The NotFound spec loaded fourteen removed addresses in one test. A CI
trace showed about four seconds per address, so the test passed the 30
second limit before the loop ended. Run one test per address.

The Home skeleton spec delayed the answer by a fixed 800 ms, which a
slow runner can spend before it paints. Hold the answer until the
skeleton was seen.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(addons): register new Ginkgo cases in the suite inventory

The harness compares the cases a suite reports with the cases listed in
scripts/addons/suites.json. The Sandbox document skills change added
five packaging cases that no entry listed, and the apps entry listed 265
cases while its four reports list 274. Both suites failed with an
inventory mismatch although every spec passed.

List the five packaging cases in the contracts entry and its packaging
report, and the nine missing apps cases in the apps entry.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(ui): let the route change finish before counting add-on reads

The apps polling spec leaves the add-ons page for a lazily loaded route
while the page clock is paused. The transition does not finish until the
clock moves, so the first step of the 180 second run still saw the
add-ons page mounted and fired one more read. The spec failed in four of
six local runs. Its sibling spec already advances the clock and settles
before it counts.

Do the same here, and wait for the NotFound page so the check is made on
a page that no longer holds the add-ons list.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(studio): stop the scoped-chat spec timing out on the nib harness

The spec "opens a scoped chat with load_skill without reentering session
access resolution" opened its chat on the real nib harness. Starting a
nib session takes about a second under -race and more on a loaded CI
runner, so Open sometimes passed the two second budget and the spec
reported a re-entrant resolution that did not exist. The distributed
test job failed on it while the plain tests job passed.

Run the spec on the stub harness, so Open only builds the chat tools,
which is what the spec is about. Widen the hang guard to 30 seconds: a
real re-entry blocks on the open lock for good, so any finite bound
tells it from a slow run.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
mudler-agentandEttore Di Giacinto authored and GitHub committed 2026-11-03 08:34:58 +01:00
1 parent 7904b3d0ab
commit 579e20ab11
6 files changed
+63 -16

No files matched your search

@@ -56,11 +56,13 @@ for (const surface of ['installed','gallery','detail']) {
expect(new URL(page.url()).pathname).not.toContain('/app/addons/')
})
}
test('removed addon addresses reach NotFound rather than addon or install promotion',async({page})=>{
await setup(page,[])
for (const path of ['studio','talk','image','video','3d','tts','sound','diarization','transform','face','voice','voice-library','fine-tune','quantize']) {
// One test per address: each loads the whole app, and a slow runner needs
// several seconds for every load, more than one test may spend on fourteen.
for (const path of ['studio','talk','image','video','3d','tts','sound','diarization','transform','face','voice','voice-library','fine-tune','quantize']) {
test(`removed addon address /app/${path} reaches NotFound rather than addon or install promotion`,async({page})=>{
await setup(page,[])
await page.goto(`/app/${path}`)
await expect(page.getByTestId('not-found')).toBeVisible()
expect(new URL(page.url()).pathname).toBe(`/app/${path}`)
}
})
})
}
@@ -253,7 +253,12 @@ test.describe('Add-ons: apps', () => {
await expect(page.getByText('may be out of date')).toHaveCount(0)
// Leave for a route without a mounted add-on consumer (Home keeps commands current).
await page.evaluate(() => { window.history.pushState({}, '', '/app/polling-left'); window.dispatchEvent(new PopStateEvent('popstate')) })
// The route loads lazily and the page clock is paused: let the transition
// finish, and the add-ons page unmount, before counting reads again.
await page.clock.runFor(500)
await settle(page)
await expect(page.locator('[data-app="notes"]')).toHaveCount(0)
await expect(page.getByRole('heading', { name: 'This page does not exist' })).toBeVisible()
const seen = state.log.appsList
await page.clock.runFor(180_000)
expect(state.log.appsList).toBe(seen)
+10 -5
View File
@@ -35,15 +35,15 @@ test.describe('Home slash menu', () => {
const names = await page.locator('.dk-cmd-name').allTextContents()
// Native Apps opens the generic installed list, never a promoted package.
// Home's own commands first; the commands it shares with a chat follow:
// /attach, /skills and any declared skills, then /help.
// /attach, /skill, /skills and one per skill of the server, then /help.
expect(names.slice(0, 15)).toEqual([
'/model', '/new', '/chat', '/assistant', '/models',
'/gallery', '/installed', '/import', '/stop',
// No installation advice for a particular provider.
'/agents', '/mcp', '/memory', '/apps', '/settings', '/docs',
])
expect(names.slice(15, 18)).toEqual(['/attach', '/skills', '/help'])
expect(names).not.toContain('/skill') // No skills are declared by this fixture.
// The test server always keeps built-in skills, so /skill is offered.
expect(names.slice(15, 18)).toEqual(['/attach', '/skill', '/skills'])
expect(names[names.length - 1]).toBe('/help')
// A chat that does not exist yet has nothing to compact or clear.
expect(names).not.toContain('/compact')
@@ -355,12 +355,17 @@ test.describe('Home memory strip', () => {
test('shows a skeleton while the first answer is on its way', async ({ page }) => {
await mockHome(page)
// Hold the answer until the skeleton was seen: a fixed delay can run out
// before a slow runner has painted the page.
let release
const held = new Promise(resolve => { release = resolve })
await page.route('**/v1/models', async route => {
await new Promise(resolve => setTimeout(resolve, 800))
await held
await route.fulfill({ json: { data: [{ id: 'a' }] } })
})
await page.goto('/app')
await page.goto('/app', { waitUntil: 'commit' })
await expect(page.getByTestId('home-strip-skeleton')).toBeVisible()
release()
await expect(page.locator('.home-strip__head')).toBeVisible()
})
})
@@ -58,6 +58,14 @@ func md(name, desc string) []byte {
// newSkillService is a chats service on a real workspace with a broker that
// ships add-on skills.
func newSkillService(b *skillBroker, pins ...map[string]string) (*Service, *files.Service) {
return newSkillServiceWith(b, nil, pins...)
}
// newSkillServiceWith is newSkillService with the harness a chat runs on. Nil
// keeps the real nib harness, whose session setup costs about a second and
// grows with load; a spec that opens a chat only to see how its tools are
// built passes a stub.
func newSkillServiceWith(b *skillBroker, h session.Harness, pins ...map[string]string) (*Service, *files.Service) {
dsn := "file:" + filepath.Join(GinkgoT().TempDir(), "ws.db") + "?_busy_timeout=5000&_txlock=immediate"
db, err := sqlitedb.Open(dsn, &gorm.Config{})
Expect(err).NotTo(HaveOccurred())
@@ -66,7 +74,7 @@ func newSkillService(b *skillBroker, pins ...map[string]string) (*Service, *file
Expect(err).NotTo(HaveOccurred())
f := files.New(ws)
st, _ := store.NewJSON("")
d := Deps{Store: st, Provider: provider.New(), StateDir: GinkgoT().TempDir(), Files: f, AddonTools: b}
d := Deps{Store: st, Provider: provider.New(), StateDir: GinkgoT().TempDir(), Files: f, AddonTools: b, Harness: h}
if len(pins) > 0 {
d.ToolPins = func(context.Context) (map[string]string, error) { return pins[0], nil }
}
@@ -10,6 +10,7 @@ import (
"time"
"github.com/mudler/LocalAI/core/services/studio/session"
"github.com/mudler/LocalAI/core/services/studio/studiotest"
"github.com/mudler/LocalAI/core/services/studio/skills"
. "github.com/onsi/ginkgo/v2"
@@ -175,7 +176,11 @@ var _ = Describe("document provider matrix", func() {
It("opens a scoped chat with load_skill without reentering session access resolution", func() {
ctx := context.Background()
svc, f := newSkillService(&skillBroker{})
// A stub harness: the spec checks that Open builds the chat tools
// without waiting on itself, not that nib can start a session. The
// real harness takes about a second under -race, and a loaded CI
// runner pushed it past the two second budget this spec once had.
svc, f := newSkillServiceWith(&skillBroker{}, studiotest.NewGateHarness())
store, err := agents.NewJSON("")
Expect(err).NotTo(HaveOccurred())
svc.d.Agents = store
@@ -200,7 +205,10 @@ var _ = Describe("document provider matrix", func() {
select {
case err := <-done:
Expect(err).NotTo(HaveOccurred())
case <-time.After(2 * time.Second):
case <-time.After(30 * time.Second):
// A re-entrant resolution blocks on the open lock for good, so
// any finite bound tells it from a slow runner; keep it far
// above the cost of an Open.
// Fail without cleanup attempting to reacquire the stuck lock.
// The test process timeout also bounds teardown on a regression.
Fail("scoped chat setup reentered session access resolution")
+22 -3
View File
@@ -554,7 +554,12 @@
"accepts empty arguments with optional property constraints",
"refuses protected or reserved plain names, whatever the add-on id",
"keeps embedded names protected or reserved without requiring a slot fallback",
"does not promise a core fallback for document provider names"
"does not promise a core fallback for document provider names",
"packages all four document slots with the actual validator and no core fallback",
"proves every source block and local reference without Git or network",
"installs unchanged PDF helpers and their license with a build-time fixture check",
"declares and packages a loadable, receipt-verified skill without shadows",
"rejects a missing skill, mismatched frontmatter, and scripts"
],
"report": ".cache/addons/logs/contracts.json",
"reports": [
@@ -985,7 +990,12 @@
"lets a tag push prepare the immutable version and never move latest",
"promotes latest only for a published, stable, non-draft release",
"rejects unknown events and a malformed commit",
"prints the decision as key=value lines"
"prints the decision as key=value lines",
"packages all four document slots with the actual validator and no core fallback",
"proves every source block and local reference without Git or network",
"installs unchanged PDF helpers and their license with a build-time fixture check",
"declares and packages a loadable, receipt-verified skill without shadows",
"rejects a missing skill, mismatched frontmatter, and scripts"
]
},
{
@@ -4012,7 +4022,16 @@
"returns the SDK refusal text, the unverified-package text and the missing-release text to the administrator",
"logs the cause of a failed install and types it for the client",
"logs the reason behind a generic denial and leaves the response unchanged",
"names every typed cause of a failed install in the 403 of the dispatch route"
"names every typed cause of a failed install in the 403 of the dispatch route",
"installs upgrades and uninstalls the actual Sandbox document skills through verified receipts",
"finishes a real backup and commit after the submitting request is cancelled",
"polls and retries during a blocked backup, rejects conflicting IDs, and retains the receipt on restart",
"reconciles durable accepted work to interrupted without replay",
"Close cancels and joins an accepted owner worker before releasing storage",
"traces completion, retries, detach, interruption and late delivery without replay",
"keeps terminals immutable for every event and gives each active state a terminal exit",
"Close cancels a backup in flight and joins its cleanup before releasing ownership",
"serializes synchronous disable against an accepted upgrade without overwriting its journal"
],
"report": ".cache/addons/logs/apps.json",
"reports": [