mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-20 13:53:32 +08:00
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:
parent
7c37384ebc
commit
84bcdb5f52
@ -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 () => {
|
it('requires confirmation before deleting a provider', async () => {
|
||||||
render(<Settings />)
|
render(<Settings />)
|
||||||
|
|
||||||
|
|||||||
@ -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 knownIds = new Set<string>(defaultProviderOrder(providers))
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const order: 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
|
if (!knownIds.has(id) || seen.has(id)) continue
|
||||||
seen.add(id)
|
seen.add(id)
|
||||||
order.push(id)
|
order.push(id)
|
||||||
@ -292,7 +296,7 @@ function normalizeProviderOrder(providerOrder: string[], providers: SavedProvide
|
|||||||
|
|
||||||
function buildProviderListItems(
|
function buildProviderListItems(
|
||||||
providers: SavedProvider[],
|
providers: SavedProvider[],
|
||||||
providerOrder: string[],
|
providerOrder: string[] | undefined,
|
||||||
): ProviderListItem[] {
|
): ProviderListItem[] {
|
||||||
const savedItems = new Map(
|
const savedItems = new Map(
|
||||||
providers.map((provider) => [
|
providers.map((provider) => [
|
||||||
|
|||||||
@ -11,12 +11,12 @@ describe('feature quality contract', () => {
|
|||||||
expect(agents).toContain('`~/.claude/settings.json` is user-owned shared state')
|
expect(agents).toContain('`~/.claude/settings.json` is user-owned shared state')
|
||||||
expect(agents).toContain('persistence upgrade gate')
|
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('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('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('E2E is required when the feature crosses process boundaries')
|
||||||
expect(agents).toContain('AI agents must include this evidence')
|
expect(agents).toContain('AI agents must include this evidence')
|
||||||
expect(agents).toContain('Unified local entrypoint: `bun run verify`')
|
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', () => {
|
test('keeps PR authors accountable for tests, coverage, E2E, and risk', () => {
|
||||||
|
|||||||
@ -115,6 +115,19 @@ describe('WorkspaceService outside-workspace preview', () => {
|
|||||||
|
|
||||||
await expect(service.readFile('session-1', unrelatedFile)).rejects.toThrow(/outside workspace/)
|
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', () => {
|
describe('WorkspaceService', () => {
|
||||||
|
|||||||
@ -1065,7 +1065,7 @@ export class WorkspaceService {
|
|||||||
// registered as access roots when the turn checkpoint is built. Preview
|
// registered as access roots when the turn checkpoint is built. Preview
|
||||||
// those by absolute path — they have no workspace-relative form — instead
|
// those by absolute path — they have no workspace-relative form — instead
|
||||||
// of rejecting them as out-of-sandbox.
|
// of rejecting them as out-of-sandbox.
|
||||||
if (isWithinRegisteredFilesystemRoot(absolutePath)) {
|
if (this.isAbsoluteRequestPath(requestedPath) && isWithinRegisteredFilesystemRoot(absolutePath)) {
|
||||||
return this.resolveOutsideWorkspacePath(absolutePath, requestedPath)
|
return this.resolveOutsideWorkspacePath(absolutePath, requestedPath)
|
||||||
}
|
}
|
||||||
throw new Error(`Path is outside workspace: ${requestedPath}`)
|
throw new Error(`Path is outside workspace: ${requestedPath}`)
|
||||||
@ -1192,6 +1192,11 @@ export class WorkspaceService {
|
|||||||
return isSameOrInsidePathForPlatform(targetPath, rootPath)
|
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 {
|
private normalizeRelativePath(filePath: string): string {
|
||||||
if (!filePath || filePath === '.') return ''
|
if (!filePath || filePath === '.') return ''
|
||||||
return filePath.split(path.sep).join('/')
|
return filePath.split(path.sep).join('/')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user