Files
aiturk-hermes-ide/apps/desktop/electron/bundle-skew.test.ts
T

321 lines
10 KiB
TypeScript

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 })
})
})