From 163e245b0034319b8fa1ad01e0f425d8dcc9882a Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 23 Sep 2026 19:15:08 +0800 Subject: [PATCH] fix: stabilize release CI regression tests Address Desktop CI 35849362694 failures with isolated OpenCode config dependencies, cleared IPC lookup counts, Supervisor default-off expectations, and polling-safe UI fixtures and timeouts. Record the failed candidate CI and follow-up validation: 14 focused tests, typecheck, and lint passed. Product code, versions, and bilingual release notes are unchanged. The revised candidate requires approval and successful main-branch CI before tagging. --- .../release-preparation-0.13.12.md | 45 +++++++++++++++++++ .../opencode-selection-isolation.test.ts | 21 +++++++-- src/main/ipc.test.ts | 2 + src/renderer/src/ActivityPanel.test.tsx | 2 +- src/renderer/src/App.test.tsx | 22 ++++----- 5 files changed, 78 insertions(+), 14 deletions(-) diff --git a/docs/development/release-preparation-0.13.12.md b/docs/development/release-preparation-0.13.12.md index 37794abc..822edbf2 100644 --- a/docs/development/release-preparation-0.13.12.md +++ b/docs/development/release-preparation-0.13.12.md @@ -62,3 +62,48 @@ Agent packaging and publication verification remain pending. No local production build or package was run. No push, tag, remote synchronization, or publication was performed during preparation. Approval must identify the exact candidate commit and both bilingual note files before either release tag is created. + +## Approved Candidate CI Attempt + +The user approved `bf8ece05902509411454b27b742ebfec930005ad` and both bilingual +note files. On 2026-09-23 that exact commit was fast-forwarded to `main` on +`origin` and `github`; both remote refs were verified. GitHub SSH stalled, so +the GitHub push used HTTPS with the existing GitHub CLI keyring credentials. +No persistent Git configuration was changed. GitHub reported the account's +existing pull-request-rule bypass for this requested direct push. + +- [Desktop main CI](https://github.com/mesalogo/goodbuddy/actions/runs/35849362694) + failed in the full suite: 4 files failed, 418 passed, 13 skipped; 10 tests + failed, 4,997 passed, 43 skipped. Type checks, lint, and the production build + did not execute after the test failure. +- [Agent main CI](https://github.com/mesalogo/goodbuddy/actions/runs/35849362799) + passed source validation and native Linux x64, Linux arm64, and Darwin arm64 + deterministic compound-package verification with ephemeral test signatures. +- Neither release tag was created or pushed. No Desktop or Agent publication + workflow was started, and no production release assets were verified. + +The follow-up changes are limited to tests. Two IPC tests clear the startup +settings-read count before measuring interactive request lookups. The settings +failure UI test now expects Supervisor to remain disabled. OpenCode isolation +tests prepare their real configuration dependencies in a dedicated temporary +directory instead of relying on a prior local package preparation; connection +failures now include their diagnostic detail. The large activity-list and +multi-page navigation and external knowledge binding tests have explicit +15-second budgets; the binding test also waits up to five seconds for its +resulting heading. +Its snapshot fixture now selects by library ID, and its instance list remains +available during polling instead of assuming a fixed number of asynchronous +reads. Both mocks reset between tests; the binding test waits for the +selected-library read. +These budgets address timeouts observed in the full CI run without removing +assertions or changing product behavior. + +The approved source and bilingual notes remain unchanged. These test changes +need a revised candidate commit and approval before another release attempt; +the failed commit must not be tagged. No local production build, package, +external model request, or LoongArch build was performed. + +Follow-up validation: the seven selected IPC/UI regression cases and all seven +OpenCode isolation cases passed locally. Type checks and lint passed. Full +remote CI and the production build still require a new approved commit; these +focused results do not satisfy that release gate. diff --git a/src/main/agent/opencode-selection-isolation.test.ts b/src/main/agent/opencode-selection-isolation.test.ts index 9561a182..32c450aa 100644 --- a/src/main/agent/opencode-selection-isolation.test.ts +++ b/src/main/agent/opencode-selection-isolation.test.ts @@ -1,10 +1,11 @@ // @vitest-environment node import { createServer } from 'node:http' import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' import spawn from 'cross-spawn' -import { describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { runtimeSettingsInputSchema } from '../../shared/contracts' import { RuntimeSettingsStore } from '../runtime-settings-store' import { applyRuntimeSelection } from './runtime-selection' @@ -19,6 +20,19 @@ const newId = '00000000-0000-4000-8000-000000000123' const denial = "OpenCode's free tier can only be used from within OpenCode" describe('OpenCode isolated configuration paths', () => { + let fixtureRoot: string + let bundledConfigPath: string + beforeAll(async () => { + fixtureRoot = await mkdtemp(join(tmpdir(), 'goodbuddy-opencode-config-isolation-')) + const { prepareBundledOpenCodeConfig } = createRequire(import.meta.url)( + '../../../build/opencode-config.cjs' + ) + bundledConfigPath = prepareBundledOpenCodeConfig(fixtureRoot) + }, 60_000) + afterAll(async () => { + if (fixtureRoot) await rm(fixtureRoot, { recursive: true, force: true }) + }) + it.each(['default', 'profile', 'platform', 'external'] as const)( 'resolves persisted %s source independently of the global default', async (source) => { const root = await mkdtemp(join(tmpdir(), 'goodbuddy-source-matrix-')) @@ -95,7 +109,7 @@ describe('OpenCode isolated configuration paths', () => { embedded: true, binaryPath, defaultWorkspace: root, configPath: source === 'native-empty' ? '' : configPath, sharedCacheRoot: join(root, 'runtime-cache'), - bundledConfigPath: join(process.cwd(), '.runtime-resources', 'opencode-config'), + bundledConfigPath, ...(source === 'profile' ? { modelProfile: { id: newId, name: 'Local mock', baseUrl: baseURL, modelName: 'probe-model', protocol: 'openai-chat-completions' as const, authentication: 'none' as const @@ -122,7 +136,8 @@ describe('OpenCode isolated configuration paths', () => { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 25_000) try { - expect(await runtime.testConnection()).toMatchObject({ available: true }) + const connection = await runtime.testConnection() + expect(connection, connection.detail).toMatchObject({ available: true }) const config = JSON.parse(launchEnvironment.OPENCODE_CONFIG_CONTENT!) expect(config.model).toBe(source === 'profile' ? 'goodbuddy-openai-chat/probe-model' : undefined) expect(launchEnvironment.OPENCODE_CONFIG).toBe(source === 'native-file' ? configPath : undefined) diff --git a/src/main/ipc.test.ts b/src/main/ipc.test.ts index 012abaf6..3c728aa9 100644 --- a/src/main/ipc.test.ts +++ b/src/main/ipc.test.ts @@ -9649,6 +9649,7 @@ describe('registerIpcHandlers agent terminal state', () => { } ) const event = trustedEvent(harness.webContents) + harness.getApplicationSettings.mockClear() const request = { requestId: '00000000-0000-4000-8000-000000000013', conversationId: 'duplicate-request', @@ -9737,6 +9738,7 @@ describe('registerIpcHandlers agent terminal state', () => { getEnabledBuiltinMcpServerIds } ) + harness.getApplicationSettings.mockClear() harness.getApplicationSettings.mockReturnValueOnce( applicationSettings ) diff --git a/src/renderer/src/ActivityPanel.test.tsx b/src/renderer/src/ActivityPanel.test.tsx index dcb916b3..1713e4f8 100644 --- a/src/renderer/src/ActivityPanel.test.tsx +++ b/src/renderer/src/ActivityPanel.test.tsx @@ -759,7 +759,7 @@ describe('ActivityPanel', () => { }) ) expect(screen.getByText('活动 500')).toBeInTheDocument() - }) + }, 15_000) it('shows totals without double-counting cache tokens', () => { render( diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 4d5947aa..c888bd12 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -1139,6 +1139,8 @@ describe("App", () => { delete document.documentElement.dataset.theme; document.documentElement.style.colorScheme = ""; vi.clearAllMocks(); + vi.mocked(api.knowledge.getSnapshot).mockReset(); + vi.mocked(api.knowledge.externalInstancesList).mockReset(); vi.mocked(api.agent.respondQuestion).mockReset().mockResolvedValue(); vi.mocked(api.agent.respondApproval).mockReset().mockResolvedValue(); magicTodoStatusChangedListener = undefined; @@ -7288,8 +7290,10 @@ describe("App", () => { const created = { ...existing, id: '33333333-3333-4333-8333-333333333333', name: 'Remote handbook', external: { knowledgeBaseId: '33333333-3333-4333-8333-333333333333', instanceId: instance.id, provider: 'dify' as const, remoteKnowledgeBaseId: 'remote-1', remoteName: 'Remote handbook', commonConfig: { resultLimit: 6, requestTimeoutMs: 15000, maxSnippetCharacters: 4000 }, providerConfig: { provider: 'dify' as const, useDatasetDefaults: true as const }, lastVerifiedAt: '2026-09-12' } }; const initial = { libraries: [existing, excluded], selectedLibraryId: existing.id, sources: [], documents: [], graphNodes: [], graphRelations: [], evidence: [] }; const updated = { ...initial, libraries: [...initial.libraries, created], selectedLibraryId: created.id }; - vi.mocked(api.knowledge.getSnapshot).mockResolvedValueOnce(initial).mockResolvedValueOnce(updated); - vi.mocked(api.knowledge.externalInstancesList).mockResolvedValueOnce([instance]).mockResolvedValueOnce([instance]); + vi.mocked(api.knowledge.getSnapshot).mockImplementation(async (libraryId) => + libraryId === created.id ? updated : initial + ); + vi.mocked(api.knowledge.externalInstancesList).mockResolvedValue([instance]); vi.mocked(api.knowledge.externalCatalogList).mockResolvedValueOnce({ items: [{ id: 'remote-1', name: 'Remote handbook' }], hasMore: false }); vi.mocked(api.knowledge.externalCatalogGet).mockResolvedValueOnce({ id: 'remote-1', name: 'Remote handbook' }); vi.mocked(api.knowledge.externalBindingsCreate).mockResolvedValueOnce(updated); @@ -7306,15 +7310,15 @@ describe("App", () => { fireEvent.click(screen.getByRole('button', { name: '测试检索' })); await waitFor(() => expect(screen.getByRole('button', { name: '添加知识库' })).toBeEnabled()); fireEvent.click(screen.getByRole('button', { name: '添加知识库' })); - await screen.findByRole('heading', { name: 'Remote handbook' }); - expect(api.knowledge.getSnapshot).toHaveBeenLastCalledWith(created.id); + await screen.findByRole('heading', { name: 'Remote handbook' }, { timeout: 5_000 }); + await waitFor(() => expect(api.knowledge.getSnapshot).toHaveBeenLastCalledWith(created.id)); fireEvent.click(screen.getByRole('button', { name: /^External scope test/u })); openComposerOptions(); fireEvent.click(await screen.findByRole('button', { name: '选择知识库,本次已启用 2 个' })); expect(screen.getByRole('checkbox', { name: /Existing/u })).toBeChecked(); expect(screen.getByRole('checkbox', { name: /Excluded/u })).not.toBeChecked(); expect(screen.getByRole('checkbox', { name: /Remote handbook.*Dify.*Company Dify/u })).toBeChecked(); - }); + }, 15_000); it("ignores stale Git changes after switching projects", async () => { const secondProject = { @@ -13882,17 +13886,15 @@ describe("App", () => { expect(screen.queryByDisplayValue('Unsaved navigation title')).not.toBeInTheDocument() expect(screen.queryByText('Unsaved body')).not.toBeInTheDocument() expect(api.magicNotes.update).not.toHaveBeenCalled() - }) + }, 15_000) - it('keeps fixed apps available when application settings cannot be loaded', async () => { + it('keeps Knowledge available and Supervisor disabled when application settings cannot be loaded', async () => { vi.mocked(api.updates!.getSettings).mockRejectedValue(new Error('unavailable')) render() fireEvent.click(screen.getByRole('button', { name: '知识库' })) expect(await screen.findByLabelText('知识工作区')).toBeInTheDocument() expect(screen.queryByRole('button', { name: '应用设置' })).not.toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '监督者' })) - expect(await screen.findByRole('heading', { name: '监督者' })).toBeInTheDocument() - expect(screen.queryByRole('button', { name: '应用设置' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: '监督者' })).not.toBeInTheDocument() expect(api.heartbeats.update).not.toHaveBeenCalled() })