From 84bcdb5f5250748d4bdd6615d7fb58ab0fd14dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 13 Jun 2026 12:09:57 +0800 Subject: [PATCH] fix: restore release gate checks Keep provider ordering compatible with older desktop store state, keep workspace traversal blocked even when external changed-file roots are registered, and align the quality-contract test with the current AGENTS wording. Tested: bun test scripts/pr/quality-contract.test.ts Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx src/__tests__/skillsSettings.test.tsx src/__tests__/pluginsSettings.test.tsx src/__tests__/diagnosticsSettings.test.tsx --run Tested: bun run check:policy Tested: bun run check:desktop Tested: bun test src/server/__tests__/workspace-service.test.ts -t 'does not allow relative traversal' Tested: bun test src/server/__tests__/sessions.test.ts -t 'workspace/file and tree should reject traversal|workspace/diff should reject traversal' --timeout=20000 Confidence: high Scope-risk: narrow --- desktop/src/__tests__/generalSettings.test.tsx | 14 ++++++++++++++ desktop/src/pages/Settings.tsx | 10 +++++++--- scripts/pr/quality-contract.test.ts | 4 ++-- src/server/__tests__/workspace-service.test.ts | 13 +++++++++++++ src/server/services/workspaceService.ts | 7 ++++++- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index a0cf84d2..6491d660 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -1547,6 +1547,20 @@ describe('Settings > Providers tab', () => { ]) }) + it('falls back to the default provider order when stored order is missing', () => { + providerStoreState.providerOrder = undefined as unknown as string[] + + render() + + const rows = screen.getAllByRole('button', { name: 'Drag to reorder' }) + .map((handle) => handle.closest('[data-testid]')?.getAttribute('data-testid')) + expect(rows).toEqual([ + 'provider-provider-1', + 'claude-official-provider', + 'openai-official-provider', + ]) + }) + it('requires confirmation before deleting a provider', async () => { render() diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 83bf956c..3b757d49 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -270,12 +270,16 @@ function defaultProviderOrder(providers: SavedProvider[]): string[] { ] } -function normalizeProviderOrder(providerOrder: string[], providers: SavedProvider[]): string[] { +function normalizeProviderOrder(providerOrder: string[] | undefined, providers: SavedProvider[]): string[] { const knownIds = new Set(defaultProviderOrder(providers)) const seen = new Set() const order: string[] = [] - for (const id of providerOrder.length > 0 ? providerOrder : defaultProviderOrder(providers)) { + const source = providerOrder && providerOrder.length > 0 + ? providerOrder + : defaultProviderOrder(providers) + + for (const id of source) { if (!knownIds.has(id) || seen.has(id)) continue seen.add(id) order.push(id) @@ -292,7 +296,7 @@ function normalizeProviderOrder(providerOrder: string[], providers: SavedProvide function buildProviderListItems( providers: SavedProvider[], - providerOrder: string[], + providerOrder: string[] | undefined, ): ProviderListItem[] { const savedItems = new Map( providers.map((provider) => [ diff --git a/scripts/pr/quality-contract.test.ts b/scripts/pr/quality-contract.test.ts index 128c37d9..d249a20f 100644 --- a/scripts/pr/quality-contract.test.ts +++ b/scripts/pr/quality-contract.test.ts @@ -11,12 +11,12 @@ describe('feature quality contract', () => { expect(agents).toContain('`~/.claude/settings.json` is user-owned shared state') expect(agents).toContain('persistence upgrade gate') expect(agents).toContain('Production code changes under `desktop/src`, `src/server`, `src/tools`, `src/utils`, or `adapters` must include a same-area test file') - expect(agents).toContain('Coverage is part of the feature, not an afterthought.') + expect(agents).toContain('Coverage is part of PR readiness, not an afterthought.') expect(agents).toContain('changed executable production line must meet the changed-line coverage gate') expect(agents).toContain('E2E is required when the feature crosses process boundaries') expect(agents).toContain('AI agents must include this evidence') expect(agents).toContain('Unified local entrypoint: `bun run verify`') - expect(agents).toContain('If `bun run verify` fails, do not stop at reporting the failure') + expect(agents).toContain('If `bun run verify` is intentionally run and fails, do not stop at reporting the failure') }) test('keeps PR authors accountable for tests, coverage, E2E, and risk', () => { diff --git a/src/server/__tests__/workspace-service.test.ts b/src/server/__tests__/workspace-service.test.ts index 8ac31c59..0b0dc9a6 100644 --- a/src/server/__tests__/workspace-service.test.ts +++ b/src/server/__tests__/workspace-service.test.ts @@ -115,6 +115,19 @@ describe('WorkspaceService outside-workspace preview', () => { await expect(service.readFile('session-1', unrelatedFile)).rejects.toThrow(/outside workspace/) }) + + it('does not allow relative traversal through a registered outside dir', async () => { + const baseDir = await makeTempDir('workspace-service-base-') + const workDir = path.join(baseDir, 'work') + const outsideFile = path.join(baseDir, 'outside.txt') + await fs.mkdir(workDir) + await fs.writeFile(outsideFile, 'secret\n') + registerFilesystemAccessRoot(baseDir) + + const service = new WorkspaceService(async (sessionId) => sessionId === 'session-1' ? workDir : null) + + await expect(service.readFile('session-1', '../outside.txt')).rejects.toThrow(/outside workspace/) + }) }) describe('WorkspaceService', () => { diff --git a/src/server/services/workspaceService.ts b/src/server/services/workspaceService.ts index 8e339e37..eda596e0 100644 --- a/src/server/services/workspaceService.ts +++ b/src/server/services/workspaceService.ts @@ -1065,7 +1065,7 @@ export class WorkspaceService { // registered as access roots when the turn checkpoint is built. Preview // those by absolute path — they have no workspace-relative form — instead // of rejecting them as out-of-sandbox. - if (isWithinRegisteredFilesystemRoot(absolutePath)) { + if (this.isAbsoluteRequestPath(requestedPath) && isWithinRegisteredFilesystemRoot(absolutePath)) { return this.resolveOutsideWorkspacePath(absolutePath, requestedPath) } throw new Error(`Path is outside workspace: ${requestedPath}`) @@ -1192,6 +1192,11 @@ export class WorkspaceService { return isSameOrInsidePathForPlatform(targetPath, rootPath) } + private isAbsoluteRequestPath(requestedPath: string): boolean { + const pathApi = process.platform === 'win32' ? path.win32 : path + return pathApi.isAbsolute(normalizeDriveRootPathForPlatform(requestedPath)) + } + private normalizeRelativePath(filePath: string): string { if (!filePath || filePath === '.') return '' return filePath.split(path.sep).join('/')