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
This commit is contained in:
程序员阿江(Relakkes) 2026-06-13 12:09:57 +08:00
parent 7c37384ebc
commit 84bcdb5f52
5 changed files with 42 additions and 6 deletions

View File

@ -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(<Settings />)
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(<Settings />)

View File

@ -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<string>(defaultProviderOrder(providers))
const seen = new Set<string>()
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) => [

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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('/')