Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import { detectBundleSkew, isFallbackCommit, type RunGit, RUNTIME_PATHS } from './bundle-skew'
|
||||
|
||||
const REPO = '/repo'
|
||||
const STAMP = { commit: 'a'.repeat(40), source: 'ci' }
|
||||
|
||||
function gitReturning(stdout: string, code = 0): RunGit {
|
||||
return async () => ({ code, stderr: '', stdout })
|
||||
}
|
||||
|
||||
/**
|
||||
* A git fake that answers per subcommand, so a test can say "ancestry fails,
|
||||
* but the count would have claimed skew" — which is the shape of #92233.
|
||||
*/
|
||||
function gitAnswering(answers: Record<string, { code?: number; stderr?: string; stdout?: string }>): {
|
||||
calls: string[][]
|
||||
git: RunGit
|
||||
} {
|
||||
const calls: string[][] = []
|
||||
|
||||
const git: RunGit = async args => {
|
||||
calls.push(args)
|
||||
|
||||
const answer = answers[args[0]] ?? {}
|
||||
|
||||
return {
|
||||
code: answer.code ?? 0,
|
||||
stderr: answer.stderr ?? '',
|
||||
stdout: answer.stdout ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
return { calls, git }
|
||||
}
|
||||
|
||||
/** Every subcommand succeeds; rev-list reports `count`. */
|
||||
function gitCounting(count: string): RunGit {
|
||||
return gitAnswering({ 'merge-base': { code: 0 }, 'rev-list': { stdout: count } }).git
|
||||
}
|
||||
|
||||
describe('isFallbackCommit', () => {
|
||||
it('matches the all-zero placeholder at any stamp length', () => {
|
||||
expect(isFallbackCommit('0'.repeat(40))).toBe(true)
|
||||
expect(isFallbackCommit('0'.repeat(7))).toBe(true)
|
||||
expect(isFallbackCommit('a'.repeat(40))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectBundleSkew', () => {
|
||||
it('reports stale when desktop commits landed after the stamp', async () => {
|
||||
const result = await detectBundleSkew(STAMP, gitCounting('3\n'), REPO)
|
||||
|
||||
expect(result).toEqual({ desktopCommitsBehind: 3, outOfSync: true })
|
||||
})
|
||||
|
||||
it('counts only commits that touch runtime desktop paths', async () => {
|
||||
const { calls, git } = gitAnswering({ 'merge-base': { code: 0 }, 'rev-list': { stdout: '0' } })
|
||||
|
||||
await detectBundleSkew(STAMP, git, REPO)
|
||||
|
||||
expect(calls[1]).toEqual(['rev-list', '--count', `${STAMP.commit}..HEAD`, '--', ...RUNTIME_PATHS])
|
||||
})
|
||||
|
||||
it('is quiet when no desktop commits follow the stamp', async () => {
|
||||
const result = await detectBundleSkew(STAMP, gitCounting('0\n'), REPO)
|
||||
|
||||
expect(result).toEqual({ desktopCommitsBehind: 0, outOfSync: false })
|
||||
})
|
||||
|
||||
it('is quiet without a stamp (dev runs)', async () => {
|
||||
expect(await detectBundleSkew(null, gitReturning('9'), REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
it('is quiet on a fallback stamp (non-git build)', async () => {
|
||||
const fallback = { commit: '0'.repeat(40), source: 'fallback' }
|
||||
|
||||
expect(await detectBundleSkew(fallback, gitReturning('9'), REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
it('is quiet when git fails (unknown commit, shallow clone, no git)', async () => {
|
||||
expect(await detectBundleSkew(STAMP, gitReturning('', 128), REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
it('is quiet when git throws', async () => {
|
||||
const git: RunGit = async () => {
|
||||
throw new Error('spawn ENOENT')
|
||||
}
|
||||
|
||||
expect(await detectBundleSkew(STAMP, git, REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
it('is quiet on unparsable rev-list output', async () => {
|
||||
expect(await detectBundleSkew(STAMP, gitCounting('fatal: bad object'), REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
// #92233: a ZIP-fallback update rewrites the tree into a synthetic root, so
|
||||
// the stamp commit still RESOLVES but is unreachable from HEAD. `A..HEAD`
|
||||
// then counts HEAD's own history instead of measuring skew, and reports a
|
||||
// permanent 1 even though apps/desktop is byte-identical. The user gets an
|
||||
// "App build out of date" warning that cannot go off, so no remedy clears it.
|
||||
it('is quiet when the stamp is not an ancestor of HEAD', async () => {
|
||||
const { git } = gitAnswering({
|
||||
'merge-base': { code: 1 },
|
||||
'rev-list': { stdout: '1\n' }
|
||||
})
|
||||
|
||||
expect(await detectBundleSkew(STAMP, git, REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
it('does not consult the commit count once ancestry is refused', async () => {
|
||||
const { calls, git } = gitAnswering({
|
||||
'merge-base': { code: 1 },
|
||||
'rev-list': { stdout: '9999\n' }
|
||||
})
|
||||
|
||||
await detectBundleSkew(STAMP, git, REPO)
|
||||
|
||||
expect(calls.map(args => args[0])).toEqual(['merge-base'])
|
||||
})
|
||||
|
||||
it('asks about ancestry before counting, against the same stamp', async () => {
|
||||
const { calls, git } = gitAnswering({
|
||||
'merge-base': { code: 0 },
|
||||
'rev-list': { stdout: '2\n' }
|
||||
})
|
||||
|
||||
const result = await detectBundleSkew(STAMP, git, REPO)
|
||||
|
||||
expect(calls[0]).toEqual(['merge-base', '--is-ancestor', STAMP.commit, 'HEAD'])
|
||||
expect(calls[1]?.[0]).toBe('rev-list')
|
||||
expect(result).toEqual({ desktopCommitsBehind: 2, outOfSync: true })
|
||||
})
|
||||
|
||||
it('is quiet when git cannot answer the ancestry question at all', async () => {
|
||||
const { git } = gitAnswering({
|
||||
'merge-base': { code: 128 },
|
||||
'rev-list': { stdout: '4\n' }
|
||||
})
|
||||
|
||||
expect(await detectBundleSkew(STAMP, git, REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
})
|
||||
|
||||
// Shallow clones, measured against git 2.55 rather than assumed. A stamp
|
||||
// commit from BEFORE the graft boundary is not an object the clone has, so
|
||||
// `--is-ancestor` exits 128 with "Not a valid object name" — the same
|
||||
// unknowable bucket as any other missing commit, not a shallow-specific
|
||||
// failure. A stamp INSIDE the shallow graph is answered normally, so
|
||||
// `--fetch-depth`-limited CI checkouts do not lose skew detection wholesale;
|
||||
// only builds stamped deeper than the checkout goes do.
|
||||
it('is quiet on a shallow clone whose stamp predates the graft boundary', async () => {
|
||||
const { calls, git } = gitAnswering({
|
||||
'merge-base': {
|
||||
code: 128,
|
||||
stderr: `fatal: Not a valid object name ${STAMP.commit}`
|
||||
},
|
||||
'rev-list': { stdout: '7\n' }
|
||||
})
|
||||
|
||||
expect(await detectBundleSkew(STAMP, git, REPO)).toEqual({
|
||||
desktopCommitsBehind: null,
|
||||
outOfSync: false
|
||||
})
|
||||
expect(calls).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('still detects skew on a shallow clone when the stamp is in the graph', async () => {
|
||||
const { git } = gitAnswering({
|
||||
'merge-base': { code: 0 },
|
||||
'rev-list': { stdout: '2\n' }
|
||||
})
|
||||
|
||||
expect(await detectBundleSkew(STAMP, git, REPO)).toEqual({
|
||||
desktopCommitsBehind: 2,
|
||||
outOfSync: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Real-git integration: proves the pathspec discriminates docs/e2e-only
|
||||
// commits from runtime commits, and that a disconnected stamp goes quiet, in
|
||||
// an actual repository rather than against a hand-written fake.
|
||||
const scratchRepos: string[] = []
|
||||
|
||||
afterAll(() => {
|
||||
for (const dir of scratchRepos) {
|
||||
rmSync(dir, { force: true, recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
function scratchGit(repoRoot: string) {
|
||||
return (...args: string[]) =>
|
||||
execFileSync('git', ['-c', 'user.email=skew@test', '-c', 'user.name=skew', ...args], {
|
||||
cwd: repoRoot,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
.toString()
|
||||
.trim()
|
||||
}
|
||||
|
||||
function makeScratchRepo(): { base: string; repoRoot: string } {
|
||||
const repoRoot = mkdtempSync(join(tmpdir(), 'bundle-skew-'))
|
||||
scratchRepos.push(repoRoot)
|
||||
|
||||
const git = scratchGit(repoRoot)
|
||||
|
||||
git('init', '-q', '-b', 'main')
|
||||
git('commit', '-q', '--allow-empty', '-m', 'base')
|
||||
|
||||
return { base: git('rev-parse', 'HEAD'), repoRoot }
|
||||
}
|
||||
|
||||
function writeFiles(repoRoot: string, files: string[]) {
|
||||
for (const file of files) {
|
||||
const target = join(repoRoot, file)
|
||||
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
writeFileSync(target, '')
|
||||
}
|
||||
}
|
||||
|
||||
function realGitRun(root: string): RunGit {
|
||||
return async (args, options) => {
|
||||
try {
|
||||
const stdout = execFileSync('git', args, {
|
||||
cwd: options.cwd || root,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
}).toString()
|
||||
|
||||
return { code: 0, stderr: '', stdout }
|
||||
} catch (error) {
|
||||
const e = error as { status?: number; stderr?: Buffer; stdout?: Buffer }
|
||||
|
||||
return {
|
||||
code: e.status ?? 1,
|
||||
stderr: e.stderr?.toString() ?? '',
|
||||
stdout: e.stdout?.toString() ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('detectBundleSkew against a real git repo', () => {
|
||||
it('is quiet when only docs and e2e specs changed under apps/desktop', async () => {
|
||||
const { base, repoRoot } = makeScratchRepo()
|
||||
const git = scratchGit(repoRoot)
|
||||
|
||||
writeFiles(repoRoot, ['apps/desktop/AGENTS.md', 'apps/desktop/e2e/boot.spec.ts'])
|
||||
git('add', '.')
|
||||
git('commit', '-q', '-m', 'docs and e2e only')
|
||||
|
||||
const result = await detectBundleSkew({ commit: base, source: 'local' }, realGitRun(repoRoot), repoRoot)
|
||||
|
||||
expect(result).toEqual({ desktopCommitsBehind: 0, outOfSync: false })
|
||||
})
|
||||
|
||||
it('warns when a renderer file changed under apps/desktop', async () => {
|
||||
const { base, repoRoot } = makeScratchRepo()
|
||||
const git = scratchGit(repoRoot)
|
||||
|
||||
writeFiles(repoRoot, ['apps/desktop/src/app/new-feature.tsx', 'apps/desktop/README.md'])
|
||||
git('add', '.')
|
||||
git('commit', '-q', '-m', 'renderer change')
|
||||
|
||||
const result = await detectBundleSkew({ commit: base, source: 'local' }, realGitRun(repoRoot), repoRoot)
|
||||
|
||||
expect(result).toEqual({ desktopCommitsBehind: 1, outOfSync: true })
|
||||
})
|
||||
|
||||
// The #92233 install, reproduced: the update rewrote the tree onto a fresh
|
||||
// orphan root, so the stamp resolves but is unreachable. Real git answers
|
||||
// `rev-list` with a positive count here — ancestry is the only thing that
|
||||
// keeps the banner off.
|
||||
it('is quiet when the stamp sits on a disconnected root', async () => {
|
||||
const { base, repoRoot } = makeScratchRepo()
|
||||
const git = scratchGit(repoRoot)
|
||||
|
||||
git('checkout', '-q', '--orphan', 'rewritten')
|
||||
writeFiles(repoRoot, ['apps/desktop/src/app/shell.tsx'])
|
||||
git('add', '.')
|
||||
git('commit', '-q', '-m', 'synthetic root after a ZIP-fallback update')
|
||||
|
||||
const runGit = realGitRun(repoRoot)
|
||||
|
||||
// Precondition: the raw count this function used to trust is nonzero.
|
||||
const raw = await runGit(['rev-list', '--count', `${base}..HEAD`, '--', ...RUNTIME_PATHS], { cwd: repoRoot })
|
||||
|
||||
expect(Number.parseInt(raw.stdout.trim(), 10)).toBeGreaterThan(0)
|
||||
|
||||
const result = await detectBundleSkew({ commit: base, source: 'local' }, runGit, repoRoot)
|
||||
|
||||
expect(result).toEqual({ desktopCommitsBehind: null, outOfSync: false })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user