fix: stabilize release CI regression tests
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
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.
This commit is contained in:
@@ -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
|
build or package was run. No push, tag, remote synchronization, or publication
|
||||||
was performed during preparation. Approval must identify the exact candidate
|
was performed during preparation. Approval must identify the exact candidate
|
||||||
commit and both bilingual note files before either release tag is created.
|
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.
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
import { createServer } from 'node:http'
|
import { createServer } from 'node:http'
|
||||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import spawn from 'cross-spawn'
|
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 { runtimeSettingsInputSchema } from '../../shared/contracts'
|
||||||
import { RuntimeSettingsStore } from '../runtime-settings-store'
|
import { RuntimeSettingsStore } from '../runtime-settings-store'
|
||||||
import { applyRuntimeSelection } from './runtime-selection'
|
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"
|
const denial = "OpenCode's free tier can only be used from within OpenCode"
|
||||||
|
|
||||||
describe('OpenCode isolated configuration paths', () => {
|
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)(
|
it.each(['default', 'profile', 'platform', 'external'] as const)(
|
||||||
'resolves persisted %s source independently of the global default', async (source) => {
|
'resolves persisted %s source independently of the global default', async (source) => {
|
||||||
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-source-matrix-'))
|
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-source-matrix-'))
|
||||||
@@ -95,7 +109,7 @@ describe('OpenCode isolated configuration paths', () => {
|
|||||||
embedded: true, binaryPath, defaultWorkspace: root,
|
embedded: true, binaryPath, defaultWorkspace: root,
|
||||||
configPath: source === 'native-empty' ? '' : configPath,
|
configPath: source === 'native-empty' ? '' : configPath,
|
||||||
sharedCacheRoot: join(root, 'runtime-cache'),
|
sharedCacheRoot: join(root, 'runtime-cache'),
|
||||||
bundledConfigPath: join(process.cwd(), '.runtime-resources', 'opencode-config'),
|
bundledConfigPath,
|
||||||
...(source === 'profile' ? { modelProfile: {
|
...(source === 'profile' ? { modelProfile: {
|
||||||
id: newId, name: 'Local mock', baseUrl: baseURL, modelName: 'probe-model',
|
id: newId, name: 'Local mock', baseUrl: baseURL, modelName: 'probe-model',
|
||||||
protocol: 'openai-chat-completions' as const, authentication: 'none' as const
|
protocol: 'openai-chat-completions' as const, authentication: 'none' as const
|
||||||
@@ -122,7 +136,8 @@ describe('OpenCode isolated configuration paths', () => {
|
|||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeout = setTimeout(() => controller.abort(), 25_000)
|
const timeout = setTimeout(() => controller.abort(), 25_000)
|
||||||
try {
|
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!)
|
const config = JSON.parse(launchEnvironment.OPENCODE_CONFIG_CONTENT!)
|
||||||
expect(config.model).toBe(source === 'profile' ? 'goodbuddy-openai-chat/probe-model' : undefined)
|
expect(config.model).toBe(source === 'profile' ? 'goodbuddy-openai-chat/probe-model' : undefined)
|
||||||
expect(launchEnvironment.OPENCODE_CONFIG).toBe(source === 'native-file' ? configPath : undefined)
|
expect(launchEnvironment.OPENCODE_CONFIG).toBe(source === 'native-file' ? configPath : undefined)
|
||||||
|
|||||||
@@ -9649,6 +9649,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
const event = trustedEvent(harness.webContents)
|
const event = trustedEvent(harness.webContents)
|
||||||
|
harness.getApplicationSettings.mockClear()
|
||||||
const request = {
|
const request = {
|
||||||
requestId: '00000000-0000-4000-8000-000000000013',
|
requestId: '00000000-0000-4000-8000-000000000013',
|
||||||
conversationId: 'duplicate-request',
|
conversationId: 'duplicate-request',
|
||||||
@@ -9737,6 +9738,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
getEnabledBuiltinMcpServerIds
|
getEnabledBuiltinMcpServerIds
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
harness.getApplicationSettings.mockClear()
|
||||||
harness.getApplicationSettings.mockReturnValueOnce(
|
harness.getApplicationSettings.mockReturnValueOnce(
|
||||||
applicationSettings
|
applicationSettings
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -759,7 +759,7 @@ describe('ActivityPanel', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
expect(screen.getByText('活动 500')).toBeInTheDocument()
|
expect(screen.getByText('活动 500')).toBeInTheDocument()
|
||||||
})
|
}, 15_000)
|
||||||
|
|
||||||
it('shows totals without double-counting cache tokens', () => {
|
it('shows totals without double-counting cache tokens', () => {
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -1139,6 +1139,8 @@ describe("App", () => {
|
|||||||
delete document.documentElement.dataset.theme;
|
delete document.documentElement.dataset.theme;
|
||||||
document.documentElement.style.colorScheme = "";
|
document.documentElement.style.colorScheme = "";
|
||||||
vi.clearAllMocks();
|
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.respondQuestion).mockReset().mockResolvedValue();
|
||||||
vi.mocked(api.agent.respondApproval).mockReset().mockResolvedValue();
|
vi.mocked(api.agent.respondApproval).mockReset().mockResolvedValue();
|
||||||
magicTodoStatusChangedListener = undefined;
|
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 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 initial = { libraries: [existing, excluded], selectedLibraryId: existing.id, sources: [], documents: [], graphNodes: [], graphRelations: [], evidence: [] };
|
||||||
const updated = { ...initial, libraries: [...initial.libraries, created], selectedLibraryId: created.id };
|
const updated = { ...initial, libraries: [...initial.libraries, created], selectedLibraryId: created.id };
|
||||||
vi.mocked(api.knowledge.getSnapshot).mockResolvedValueOnce(initial).mockResolvedValueOnce(updated);
|
vi.mocked(api.knowledge.getSnapshot).mockImplementation(async (libraryId) =>
|
||||||
vi.mocked(api.knowledge.externalInstancesList).mockResolvedValueOnce([instance]).mockResolvedValueOnce([instance]);
|
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.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.externalCatalogGet).mockResolvedValueOnce({ id: 'remote-1', name: 'Remote handbook' });
|
||||||
vi.mocked(api.knowledge.externalBindingsCreate).mockResolvedValueOnce(updated);
|
vi.mocked(api.knowledge.externalBindingsCreate).mockResolvedValueOnce(updated);
|
||||||
@@ -7306,15 +7310,15 @@ describe("App", () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '测试检索' }));
|
fireEvent.click(screen.getByRole('button', { name: '测试检索' }));
|
||||||
await waitFor(() => expect(screen.getByRole('button', { name: '添加知识库' })).toBeEnabled());
|
await waitFor(() => expect(screen.getByRole('button', { name: '添加知识库' })).toBeEnabled());
|
||||||
fireEvent.click(screen.getByRole('button', { name: '添加知识库' }));
|
fireEvent.click(screen.getByRole('button', { name: '添加知识库' }));
|
||||||
await screen.findByRole('heading', { name: 'Remote handbook' });
|
await screen.findByRole('heading', { name: 'Remote handbook' }, { timeout: 5_000 });
|
||||||
expect(api.knowledge.getSnapshot).toHaveBeenLastCalledWith(created.id);
|
await waitFor(() => expect(api.knowledge.getSnapshot).toHaveBeenLastCalledWith(created.id));
|
||||||
fireEvent.click(screen.getByRole('button', { name: /^External scope test/u }));
|
fireEvent.click(screen.getByRole('button', { name: /^External scope test/u }));
|
||||||
openComposerOptions();
|
openComposerOptions();
|
||||||
fireEvent.click(await screen.findByRole('button', { name: '选择知识库,本次已启用 2 个' }));
|
fireEvent.click(await screen.findByRole('button', { name: '选择知识库,本次已启用 2 个' }));
|
||||||
expect(screen.getByRole('checkbox', { name: /Existing/u })).toBeChecked();
|
expect(screen.getByRole('checkbox', { name: /Existing/u })).toBeChecked();
|
||||||
expect(screen.getByRole('checkbox', { name: /Excluded/u })).not.toBeChecked();
|
expect(screen.getByRole('checkbox', { name: /Excluded/u })).not.toBeChecked();
|
||||||
expect(screen.getByRole('checkbox', { name: /Remote handbook.*Dify.*Company Dify/u })).toBeChecked();
|
expect(screen.getByRole('checkbox', { name: /Remote handbook.*Dify.*Company Dify/u })).toBeChecked();
|
||||||
});
|
}, 15_000);
|
||||||
|
|
||||||
it("ignores stale Git changes after switching projects", async () => {
|
it("ignores stale Git changes after switching projects", async () => {
|
||||||
const secondProject = {
|
const secondProject = {
|
||||||
@@ -13882,17 +13886,15 @@ describe("App", () => {
|
|||||||
expect(screen.queryByDisplayValue('Unsaved navigation title')).not.toBeInTheDocument()
|
expect(screen.queryByDisplayValue('Unsaved navigation title')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('Unsaved body')).not.toBeInTheDocument()
|
expect(screen.queryByText('Unsaved body')).not.toBeInTheDocument()
|
||||||
expect(api.magicNotes.update).not.toHaveBeenCalled()
|
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'))
|
vi.mocked(api.updates!.getSettings).mockRejectedValue(new Error('unavailable'))
|
||||||
render(<App />)
|
render(<App />)
|
||||||
fireEvent.click(screen.getByRole('button', { name: '知识库' }))
|
fireEvent.click(screen.getByRole('button', { name: '知识库' }))
|
||||||
expect(await screen.findByLabelText('知识工作区')).toBeInTheDocument()
|
expect(await screen.findByLabelText('知识工作区')).toBeInTheDocument()
|
||||||
expect(screen.queryByRole('button', { name: '应用设置' })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: '应用设置' })).not.toBeInTheDocument()
|
||||||
fireEvent.click(screen.getByRole('button', { name: '监督者' }))
|
expect(screen.queryByRole('button', { name: '监督者' })).not.toBeInTheDocument()
|
||||||
expect(await screen.findByRole('heading', { name: '监督者' })).toBeInTheDocument()
|
|
||||||
expect(screen.queryByRole('button', { name: '应用设置' })).not.toBeInTheDocument()
|
|
||||||
expect(api.heartbeats.update).not.toHaveBeenCalled()
|
expect(api.heartbeats.update).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user