mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix: preserve lint migration contracts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
type Rules = Record<string, unknown>
|
||||
@@ -12,22 +13,22 @@ interface Profile {
|
||||
}
|
||||
|
||||
// Captured from eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2
|
||||
// after mapping @typescript-eslint/* to typescript/* and the five extension
|
||||
// after mapping @typescript-eslint/* to typescript/* and the four extension
|
||||
// rules to their Oxlint core equivalents.
|
||||
const profiles = {
|
||||
source: {
|
||||
count: 88,
|
||||
indexes: [0, 3, 4],
|
||||
indexes: [0, 1, 4, 5],
|
||||
sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b',
|
||||
},
|
||||
example: {
|
||||
count: 87,
|
||||
indexes: [0, 1, 3, 4],
|
||||
indexes: [0, 1, 2, 4, 5],
|
||||
sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08',
|
||||
},
|
||||
test: {
|
||||
count: 83,
|
||||
indexes: [2, 3, 4],
|
||||
indexes: [0, 3, 4, 5],
|
||||
sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78',
|
||||
},
|
||||
} as const satisfies Record<string, Profile>
|
||||
@@ -75,7 +76,11 @@ function mergedRules(config: unknown, indexes: readonly number[]): Rules {
|
||||
|
||||
describe('Oxlint migration rule parity', () => {
|
||||
const path = fileURLToPath(new URL('../.oxlintrc.json', import.meta.url))
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
||||
const result = parseConfigFileTextToJson(path, readFileSync(path, 'utf8'))
|
||||
if (result.error !== undefined) {
|
||||
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
|
||||
}
|
||||
const parsed: unknown = result.config
|
||||
|
||||
it.each(Object.entries(profiles))('matches the ESLint %s profile pairwise', (_name, profile) => {
|
||||
const rules = mergedRules(parsed, profile.indexes)
|
||||
|
||||
119
scripts/oxlint-contract.spec.ts
Normal file
119
scripts/oxlint-contract.spec.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { join, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||
const eslintCli = fileURLToPath(new URL('../node_modules/eslint/bin/eslint.js', import.meta.url))
|
||||
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
|
||||
|
||||
function runStagedFormatter(paths: readonly string[]) {
|
||||
return spawnSync(process.execPath, [eslintCli, '--config', 'eslint.format.config.mjs', '--fix', '--no-warn-ignored', ...paths], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
})
|
||||
}
|
||||
|
||||
function runOxlint(args: readonly string[]) {
|
||||
return spawnSync(process.execPath, [oxlintCli, ...args], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
})
|
||||
}
|
||||
|
||||
function normalizedOutput(result: ReturnType<typeof runOxlint>): string {
|
||||
return `${result.stdout}${result.stderr}`.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
async function writeContractConfig(suffix: string): Promise<string> {
|
||||
const path = join(repositoryRoot, `.oxlintrc.contract-${suffix}.json`)
|
||||
await writeFile(path, JSON.stringify({ extends: ['./.oxlintrc.json'], ignorePatterns: [] }))
|
||||
return path
|
||||
}
|
||||
|
||||
describe('Oxlint executable contract', () => {
|
||||
it('runs type-aware rules for every owned TypeScript file class', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const probes = [
|
||||
['host package source', 'packages/fs/fs-policy/src', 'host-source.ts'],
|
||||
['host package test', 'packages/fs/fs-policy/tests', 'host-test.spec.ts'],
|
||||
['client package source', 'packages/client/ui-primitives/src', 'client-source.ts'],
|
||||
['client package test', 'packages/client/ui-trajectory/tests', 'client-test.spec.ts'],
|
||||
['client aggregate script', 'scripts', 'client-bundle-purity.spec.ts'],
|
||||
['example', 'examples', 'example.ts'],
|
||||
['website', 'website', 'website.ts'],
|
||||
] as const
|
||||
const directories: string[] = []
|
||||
const source = `function probePromise(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
probePromise()
|
||||
`
|
||||
|
||||
try {
|
||||
const paths: Array<readonly [label: string, path: string]> = []
|
||||
for (const [label, parent, filename] of probes) {
|
||||
const directory = join(repositoryRoot, parent, `.oxlint-contract-${suffix}`)
|
||||
directories.push(directory)
|
||||
await mkdir(directory, { recursive: true })
|
||||
const path = join(directory, filename)
|
||||
await writeFile(path, source)
|
||||
paths.push([label, relative(repositoryRoot, path)])
|
||||
}
|
||||
|
||||
const result = runOxlint([
|
||||
'--config',
|
||||
relative(repositoryRoot, configPath),
|
||||
'--format',
|
||||
'unix',
|
||||
...paths.map(([, path]) => path),
|
||||
])
|
||||
const output = normalizedOutput(result)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, output).toBe(1)
|
||||
for (const [label, path] of paths) {
|
||||
expect(output, label).toContain(`${path.replaceAll('\\', '/')}:5:1: Promises must be awaited`)
|
||||
}
|
||||
expect(output.match(/typescript\(no-floating-promises\)/g)).toHaveLength(probes.length)
|
||||
} finally {
|
||||
await Promise.all([
|
||||
...directories.map(directory => rm(directory, { recursive: true, force: true })),
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
|
||||
it('applies staged stylistic fixes before Oxlint validation', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const directory = join(repositoryRoot, 'scripts', `.oxlint-contract-${suffix}`)
|
||||
const path = join(directory, 'fix.ts')
|
||||
|
||||
try {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(path, 'const value={answer:1}; \nconsole.log(value)\n')
|
||||
|
||||
const relativePath = relative(repositoryRoot, path)
|
||||
const formatResult = runStagedFormatter([relativePath])
|
||||
const lintResult = runOxlint(['--config', relative(repositoryRoot, configPath), '--fix', relativePath])
|
||||
|
||||
expect(formatResult.error).toBeUndefined()
|
||||
expect(formatResult.status, normalizedOutput(formatResult)).toBe(0)
|
||||
expect(lintResult.error).toBeUndefined()
|
||||
expect(lintResult.status, normalizedOutput(lintResult)).toBe(0)
|
||||
await expect(readFile(path, 'utf8')).resolves.toBe('const value={ answer:1 }\nconsole.log(value)\n')
|
||||
} finally {
|
||||
await Promise.all([
|
||||
rm(directory, { recursive: true, force: true }),
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
})
|
||||
@@ -121,7 +121,7 @@ describe('Oxlint gate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('passes the configured native thread bound to Oxlint', () => {
|
||||
it('passes the configured worker bound to both Oxlint backends', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
|
||||
|
||||
@@ -130,10 +130,11 @@ describe('Oxlint gate', () => {
|
||||
displayCommand: 'pnpm exec oxlint . --threads=4',
|
||||
command: process.execPath,
|
||||
args: ['/private/pnpm.cjs', 'exec', 'oxlint', '.', '--threads=4'],
|
||||
env: { GOMAXPROCS: '4' },
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a non-positive or non-integer native thread bound', () => {
|
||||
it('rejects a non-positive or non-integer worker bound', () => {
|
||||
expect(() => withEnv('DSH_OXLINT_THREADS', 'auto', () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint'))))
|
||||
.toThrow('DSH_OXLINT_THREADS must be a positive integer')
|
||||
|
||||
@@ -376,22 +376,25 @@ function ciWindowsObservationalGates(): Gate[] {
|
||||
]
|
||||
}
|
||||
|
||||
function lintGate(oxlintTargets: readonly string[] = ['.']): Gate {
|
||||
const threadArgs = oxlintThreadArgs()
|
||||
if (threadArgs.length > 0) {
|
||||
return pnpmExec('lint', ['oxlint', ...oxlintTargets, ...threadArgs], { label: 'lint' })
|
||||
function lintGate(): Gate {
|
||||
const threadBound = oxlintThreadBound()
|
||||
if (threadBound !== undefined) {
|
||||
return pnpmExec('lint', ['oxlint', '.', `--threads=${threadBound}`], {
|
||||
label: 'lint',
|
||||
env: { GOMAXPROCS: threadBound },
|
||||
})
|
||||
}
|
||||
return pnpmScript('lint', 'lint')
|
||||
}
|
||||
|
||||
function oxlintThreadArgs(): string[] {
|
||||
function oxlintThreadBound(): string | undefined {
|
||||
const raw = process.env.DSH_OXLINT_THREADS
|
||||
if (raw === undefined || raw === '') return []
|
||||
if (raw === undefined || raw === '') return undefined
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`run-gates: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`)
|
||||
}
|
||||
return [`--threads=${raw}`]
|
||||
return raw
|
||||
}
|
||||
|
||||
function coverageGate(): Gate {
|
||||
|
||||
Reference in New Issue
Block a user