mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 16:03:45 +00:00
* build(xterm): restore the patch regeneration harness and gate it in CI docs/reference/ime-architecture.md says "Never hand-edit the bundles in the patch" and links to docs/reference/xterm-patch-regeneration.md. That doc does not exist, and neither does the harness it describes. Both landed in29117bf776and were deleted by17cfc968cf, a revert of the composition-ownership change, which swept up a build tool and a CI gate as collateral. The rule survived; its enforcement did not. Every xterm patch since has had to hand-edit minified bundles to comply with the surrounding architecture, because everything resolves to lib/xterm.mjs at runtime and under vitest, so a src-only edit is inert. The shipped bundles were therefore not the output of any build, and this restores them to build output. Comparing identifier multisets against a pristine build of the pinned commit finds hand-written names a minifier never emits ($rl, $hp, $tid), const in an otherwise let-only esbuild bundle, !! where the source reads Boolean(), an escaped LRM where esbuild emits the literal, and a return block esbuild collapses to void(...). Every remaining token difference is a minifier local reallocating. The old source patch could not be reused. It described the reverted composition-ownership architecture, so restoring it would have re-applied an abandoned design on top of dropping three accumulated fixes. It is re-derived from the shipped patch instead, and the derivation is a fixed point. Two deliberate departures from the deleted version. Sourcemaps are included rather than deleted, because a live test reads lib/*.map and asserts the mapped version matches the runtime version. The source-patch superset carve-out is gone, so a source hunk the shipped patch cannot name now fails loudly instead of being carved out silently. The doc's claim that the webgl and serialize addons reproduce byte for byte was half wrong. Their ESM output does reproduce at the pinned commit, but both also publish CJS that the root package script never builds, so folding either in needs a build step this harness lacks. Recorded as a blocker rather than a confident sentence. xterm_patch_sync runs the regenerator in --check mode, so a patch that does not match a rebuild of the pinned upstream now fails PR CI. The -diff -text attribute is required, not cosmetic: pnpm hashes the patch byte-for-byte, so a CRLF checkout breaks the install outright. Not verified: the CI job has not run on a real runner, the addon CJS bundles are unreproduced, and the generator is untested on Windows and Linux. * build(xterm): make the regenerator runnable on Windows and drop dead paths Readiness review on the restore found one blocking gap and two cheap cleanups. None of them change the emitted patch, which is byte-identical before and after. The generator could not run on Windows at all. Three sites called npm through execFileSync with shell:false, but npm ships as npm.cmd there, execFile applies no PATHEXT, and since CVE-2024-27980 it refuses a .cmd target without a shell. That matters because this harness arms a blocking gate whose documented remedy is --write, so a Windows contributor who tripped the gate had no remedy except hand-editing a 7MB minified bundle, which is the practice the gate exists to abolish. Four sibling scripts in config/scripts already handle this; the fix follows them and lands in run(), so the manifest-driven build step is covered too. git and tar are real executables in System32 and keep resolving without a shell, which avoids quoting exposure on paths with spaces. deleteGeneratedSourcemaps was unreachable, since the policy is include. Deleting it left "delete" as a legal policy value that nothing honoured, so a manifest asking for it would have silently shipped sourcemaps that do not match the bundle. The enum is narrowed and an unrecognised policy now throws rather than falling through. generatedHunks moved into the test file rather than being dropped; its partition assertion, that generated and source hunks reconstruct the whole patch, is worth keeping. The -text attribute now covers all five patch files. pnpm hashes each of them byte-for-byte, so the CRLF hazard the xterm patch was protected from applies equally to node-pty and the three addons. All five were already LF in the object DB, so this pins existing behaviour. -diff stays scoped to the xterm patch, since the others are readable. The doc's claim that the addons reproduce byte for byte is now dated and marked a one-off measurement rather than an invariant, because nothing re-runs it. Effective lines fall from 591 to 568 against the 600 budget. Still the largest file in config/scripts, and adding a second package to the manifest would need a split first.
464 lines
17 KiB
JavaScript
464 lines
17 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
|
import { createHash } from 'node:crypto'
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
CHECKOUT_DIFF_FLAGS,
|
|
PNPM_DIFF_FLAGS,
|
|
assertBuildStepsAllowed,
|
|
assertPublishedCommit,
|
|
assertSourceDerivationsAgree,
|
|
assertSourcemapPolicy,
|
|
firstDifferenceIndex,
|
|
formatCheckFailure,
|
|
lockfilePatchHashIsStale,
|
|
normalizePnpmDiff,
|
|
patchHash,
|
|
pnpmDiffEnvironment,
|
|
readLockfilePatchHash,
|
|
readLockfileResolutionHashes,
|
|
sourceHunks,
|
|
splitPatchEntries,
|
|
stampVersionSource,
|
|
updateLockfilePatchHash
|
|
} from './regenerate-xterm-patches.mjs'
|
|
|
|
// Only the tests need to slice the generated half out of a patch; the generator
|
|
// reads `generatedPaths` directly where it compares against the pristine build.
|
|
function generatedHunks(patchText, generatedPaths) {
|
|
return splitPatchEntries(patchText)
|
|
.filter((entry) => generatedPaths.some((prefix) => entry.path.startsWith(prefix)))
|
|
.map((entry) => entry.text)
|
|
.join('')
|
|
}
|
|
|
|
const REPO_ROOT = path.resolve(import.meta.dirname, '..', '..')
|
|
const MANIFEST_PATH = path.join(REPO_ROOT, 'config', 'patches', 'xterm-upstream.json')
|
|
const temporaryDirectories = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories
|
|
.splice(0)
|
|
.map((directory) => rm(directory, { recursive: true, force: true }))
|
|
)
|
|
})
|
|
|
|
async function createDirectory() {
|
|
const directory = await mkdtemp(path.join(tmpdir(), 'orca-xterm-patch-'))
|
|
temporaryDirectories.push(directory)
|
|
return directory
|
|
}
|
|
|
|
async function writeTree(root, files) {
|
|
for (const [relative, contents] of Object.entries(files)) {
|
|
const target = path.join(root, relative)
|
|
await mkdir(path.dirname(target), { recursive: true })
|
|
await writeFile(target, contents)
|
|
}
|
|
}
|
|
|
|
/** The three exported diff pieces, composed the way the generator composes them. */
|
|
function diffFolders(folderA, folderB) {
|
|
let stdout
|
|
try {
|
|
stdout = execFileSync('git', [...PNPM_DIFF_FLAGS, folderA, folderB], {
|
|
encoding: 'utf8',
|
|
env: pnpmDiffEnvironment(),
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
} catch (error) {
|
|
if (error.status !== 1) {
|
|
throw error
|
|
}
|
|
stdout = error.stdout
|
|
}
|
|
return normalizePnpmDiff(stdout, folderA, folderB)
|
|
}
|
|
|
|
const PRISTINE = {
|
|
'src/Widget.ts': 'export function widget(): number {\n return 1\n}\n',
|
|
'src/Other.ts': 'export const other = 0\n',
|
|
'lib/widget.js': 'function widget(){return 1}\n',
|
|
'lib/widget.js.map': '{"version":3,"sources":["../src/Widget.ts"],"mappings":"AAAA"}\n',
|
|
'package.json': '{\n "name": "@scope/widget"\n}\n'
|
|
}
|
|
|
|
const PATCHED = {
|
|
...PRISTINE,
|
|
'src/Widget.ts': 'export function widget(): number {\n return 2\n}\n',
|
|
'lib/widget.js': 'function widget(){return 2}\n',
|
|
'lib/widget.js.map': '{"version":3,"sources":["../src/Widget.ts"],"mappings":"AAAC"}\n'
|
|
}
|
|
|
|
describe('pnpm diff format', () => {
|
|
it('keeps the exact git flags pnpm uses, so patches survive `pnpm patch-commit`', () => {
|
|
expect(PNPM_DIFF_FLAGS).toEqual([
|
|
'-c',
|
|
'core.safecrlf=false',
|
|
'diff',
|
|
'--src-prefix=a/',
|
|
'--dst-prefix=b/',
|
|
'--ignore-cr-at-eol',
|
|
'--irreversible-delete',
|
|
'--full-index',
|
|
'--no-index',
|
|
'--text',
|
|
'--no-ext-diff',
|
|
'--no-color'
|
|
])
|
|
})
|
|
|
|
it('blanks the config-bearing environment variables', () => {
|
|
const environment = pnpmDiffEnvironment({ PATH: '/usr/bin', HOME: '/Users/someone' })
|
|
expect(environment).toMatchObject({
|
|
PATH: '/usr/bin',
|
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
HOME: '',
|
|
XDG_CONFIG_HOME: '',
|
|
USERPROFILE: ''
|
|
})
|
|
})
|
|
|
|
it('strips both scratch folder prefixes from headers and index lines', async () => {
|
|
const root = await createDirectory()
|
|
const folderA = path.join(root, 'pristine')
|
|
const folderB = path.join(root, 'patched')
|
|
await writeTree(folderA, PRISTINE)
|
|
await writeTree(folderB, PATCHED)
|
|
|
|
const patch = diffFolders(folderA, folderB)
|
|
|
|
expect(patch).not.toContain(root)
|
|
expect(patch).toContain('diff --git a/lib/widget.js b/lib/widget.js')
|
|
expect(patch).toContain('--- a/src/Widget.ts')
|
|
expect(patch).toContain('+++ b/src/Widget.ts')
|
|
})
|
|
|
|
it('drops a trailing no-newline marker and .DS_Store entries', () => {
|
|
const withMarker = 'diff --git a/x b/x\n@@ -1 +1 @@\n-a\n+b\n\\ No newline at end of file\n'
|
|
expect(normalizePnpmDiff(withMarker, '/a', '/b')).toBe(
|
|
'diff --git a/x b/x\n@@ -1 +1 @@\n-a\n+b\n'
|
|
)
|
|
|
|
const withJunk = [
|
|
'diff --git a/.DS_Store b/.DS_Store\n',
|
|
'index 000..111\n',
|
|
'Binary files differ\n',
|
|
'diff --git a/lib/x.js b/lib/x.js\n',
|
|
'@@ -1 +1 @@\n-a\n+b\n'
|
|
].join('')
|
|
expect(normalizePnpmDiff(withJunk, '/a', '/b')).toBe(
|
|
'diff --git a/lib/x.js b/lib/x.js\n@@ -1 +1 @@\n-a\n+b\n'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('patch entry splitting', () => {
|
|
it('separates hand-edited source hunks from generated bundle hunks', async () => {
|
|
const root = await createDirectory()
|
|
const folderA = path.join(root, 'pristine')
|
|
const folderB = path.join(root, 'patched')
|
|
await writeTree(folderA, PRISTINE)
|
|
await writeTree(folderB, PATCHED)
|
|
const patch = diffFolders(folderA, folderB)
|
|
|
|
expect(splitPatchEntries(patch).map((entry) => entry.path)).toEqual([
|
|
'lib/widget.js',
|
|
'lib/widget.js.map',
|
|
'src/Widget.ts'
|
|
])
|
|
expect(splitPatchEntries(sourceHunks(patch)).map((entry) => entry.path)).toEqual([
|
|
'src/Widget.ts'
|
|
])
|
|
expect(splitPatchEntries(generatedHunks(patch, ['lib/'])).map((entry) => entry.path)).toEqual([
|
|
'lib/widget.js',
|
|
'lib/widget.js.map'
|
|
])
|
|
})
|
|
|
|
it('rejects renames rather than emitting a header it cannot round-trip', () => {
|
|
expect(() => splitPatchEntries('diff --git a/old.ts b/new.ts\n')).toThrow(
|
|
/renames are not supported/
|
|
)
|
|
})
|
|
|
|
it('concatenating the two halves reproduces the whole patch', async () => {
|
|
const root = await createDirectory()
|
|
const folderA = path.join(root, 'pristine')
|
|
const folderB = path.join(root, 'patched')
|
|
await writeTree(folderA, PRISTINE)
|
|
await writeTree(folderB, PATCHED)
|
|
const patch = diffFolders(folderA, folderB)
|
|
|
|
expect(generatedHunks(patch, ['lib/']) + sourceHunks(patch)).toBe(patch)
|
|
})
|
|
})
|
|
|
|
describe('round-trip stability', () => {
|
|
it('re-diffing an applied patch yields the identical patch', async () => {
|
|
const root = await createDirectory()
|
|
const folderA = path.join(root, 'pristine')
|
|
const folderB = path.join(root, 'patched')
|
|
await writeTree(folderA, PRISTINE)
|
|
await writeTree(folderB, PATCHED)
|
|
const patch = diffFolders(folderA, folderB)
|
|
|
|
const replay = path.join(root, 'replay')
|
|
await writeTree(replay, PRISTINE)
|
|
const patchFile = path.join(root, 'round-trip.patch')
|
|
await writeFile(patchFile, patch)
|
|
execFileSync('git', ['apply', '-p1', '--whitespace=nowarn', patchFile], { cwd: replay })
|
|
|
|
expect(await readFile(path.join(replay, 'lib/widget.js'), 'utf8')).toBe(
|
|
PATCHED['lib/widget.js']
|
|
)
|
|
expect(diffFolders(folderA, replay)).toBe(patch)
|
|
})
|
|
|
|
it('applying only the source half leaves the bundle untouched', async () => {
|
|
const root = await createDirectory()
|
|
const folderA = path.join(root, 'pristine')
|
|
const folderB = path.join(root, 'patched')
|
|
await writeTree(folderA, PRISTINE)
|
|
await writeTree(folderB, PATCHED)
|
|
const patchFile = path.join(root, 'src.patch')
|
|
await writeFile(patchFile, sourceHunks(diffFolders(folderA, folderB)))
|
|
|
|
const replay = path.join(root, 'replay')
|
|
await writeTree(replay, PRISTINE)
|
|
execFileSync('git', ['apply', '-p1', '--whitespace=nowarn', patchFile], { cwd: replay })
|
|
|
|
expect(await readFile(path.join(replay, 'src/Widget.ts'), 'utf8')).toBe(
|
|
PATCHED['src/Widget.ts']
|
|
)
|
|
expect(await readFile(path.join(replay, 'lib/widget.js'), 'utf8')).toBe(
|
|
PRISTINE['lib/widget.js']
|
|
)
|
|
})
|
|
})
|
|
|
|
// `--write` rewrites the source patch from the emitted patch, so a hunk the
|
|
// emitted patch cannot name would delete itself on the next run.
|
|
describe('source derivation agreement', () => {
|
|
const publishedEntry = [
|
|
'diff --git a/src/browser/Types.ts b/src/browser/Types.ts',
|
|
'index 1111111..2222222 100644',
|
|
'--- a/src/browser/Types.ts',
|
|
'+++ b/src/browser/Types.ts',
|
|
'@@ -1 +1,2 @@',
|
|
' interface ICompositionHelper {',
|
|
'+ handleCompositionInput(data: string): boolean;',
|
|
''
|
|
].join('\n')
|
|
const unpublishedEntry = [
|
|
'diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts',
|
|
'index 3333333..4444444 100644',
|
|
'--- a/src/browser/TestUtils.test.ts',
|
|
'+++ b/src/browser/TestUtils.test.ts',
|
|
'@@ -1 +1,2 @@',
|
|
' class MockCompositionHelper {',
|
|
'+ public handleCompositionInput(): boolean { return false; }',
|
|
''
|
|
].join('\n')
|
|
it('fails when the source patch carries a file the emitted patch cannot', () => {
|
|
expect(() =>
|
|
assertSourceDerivationsAgree(publishedEntry + unpublishedEntry, publishedEntry)
|
|
).toThrow(/disagree on a source file/)
|
|
})
|
|
|
|
it('fails when the two derivations disagree on a file', () => {
|
|
expect(() =>
|
|
assertSourceDerivationsAgree(publishedEntry.replace('boolean;', 'void;'), publishedEntry)
|
|
).toThrow(/disagree on a source file/)
|
|
})
|
|
|
|
it('diffs the checkout with pnpm formatting so the two halves stay comparable', () => {
|
|
expect(CHECKOUT_DIFF_FLAGS).toEqual(PNPM_DIFF_FLAGS.filter((flag) => flag !== '--no-index'))
|
|
expect(CHECKOUT_DIFF_FLAGS).toContain('--full-index')
|
|
expect(CHECKOUT_DIFF_FLAGS).not.toContain('--no-index')
|
|
})
|
|
})
|
|
|
|
describe('manifest guards', () => {
|
|
const packageEntry = { name: '@xterm/xterm', version: '6.1.0-beta.287' }
|
|
const commit = '53a98a720ae4a973e384fa2440880d09537132f3'
|
|
|
|
it('accepts a tarball that names the pinned commit', () => {
|
|
const published = { version: '6.1.0-beta.287', commit }
|
|
expect(() => assertPublishedCommit(published, packageEntry, commit)).not.toThrow()
|
|
})
|
|
|
|
it('fails when a version bump moved the upstream commit', () => {
|
|
const published = { version: '6.1.0-beta.287', commit: 'f'.repeat(40) }
|
|
expect(() => assertPublishedCommit(published, packageEntry, commit)).toThrow(
|
|
/was published from commit[\s\S]*Update upstream\.commit/
|
|
)
|
|
})
|
|
|
|
it('fails when the registry serves a different version than the manifest pins', () => {
|
|
const published = { version: '6.1.0-beta.288', commit }
|
|
expect(() => assertPublishedCommit(published, packageEntry, commit)).toThrow(/registry served/)
|
|
})
|
|
|
|
it('fails when the tarball carries no commit stamp at all', () => {
|
|
expect(() =>
|
|
assertPublishedCommit({ version: '6.1.0-beta.287' }, packageEntry, commit)
|
|
).toThrow(/\(absent\)/)
|
|
})
|
|
|
|
it('refuses a build step that would de-minify the bundle', () => {
|
|
const manifest = {
|
|
forbiddenBuildScripts: { why: 'dev esbuild', scripts: ['setup'] },
|
|
packages: [
|
|
{
|
|
name: '@xterm/xterm',
|
|
build: [
|
|
{ cwd: '.', command: 'npm', args: ['run', 'setup'] },
|
|
{ cwd: '.', command: 'npm', args: ['run', 'package'] }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
expect(() => assertBuildStepsAllowed(manifest)).toThrow(/`npm run setup` is forbidden/)
|
|
})
|
|
|
|
it('refuses a sourcemap policy it does not implement', () => {
|
|
expect(assertSourcemapPolicy({ sourcemaps: { policy: 'include' } })).toBe('include')
|
|
// `delete` named a code path that no longer exists; accepting it would ship
|
|
// maps that do not match the bundle.
|
|
expect(() => assertSourcemapPolicy({ sourcemaps: { policy: 'delete' } })).toThrow(
|
|
/must be one of include, got "delete"/
|
|
)
|
|
expect(() => assertSourcemapPolicy({ sourcemaps: { policy: 'exclude' } })).toThrow(
|
|
/must be one of include, got "exclude"/
|
|
)
|
|
expect(() => assertSourcemapPolicy({})).toThrow(/got undefined/)
|
|
})
|
|
|
|
it('stamps the published version into the version source', () => {
|
|
const source = "export const XTERM_VERSION = '6.0.0';\n"
|
|
expect(stampVersionSource(source, '6.1.0-beta.287')).toBe(
|
|
"export const XTERM_VERSION = '6.1.0-beta.287';\n"
|
|
)
|
|
expect(() => stampVersionSource('export const OTHER = 1\n', '6.1.0')).toThrow(/XTERM_VERSION/)
|
|
})
|
|
})
|
|
|
|
describe('lockfile coupling', () => {
|
|
const lockfile = [
|
|
'patchedDependencies:',
|
|
" '@xterm/xterm@6.1.0-beta.287':",
|
|
` hash: ${'0'.repeat(64)}`,
|
|
' path: config/patches/@xterm__xterm@6.1.0-beta.287.patch',
|
|
' node-pty@1.1.0:',
|
|
` hash: ${'1'.repeat(64)}`,
|
|
' path: config/patches/node-pty@1.1.0.patch',
|
|
'snapshots:',
|
|
` '@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=${'0'.repeat(64)}))':`,
|
|
` '@xterm/xterm': 6.1.0-beta.287(patch_hash=${'0'.repeat(64)})`,
|
|
` node-pty@1.1.0(patch_hash=${'1'.repeat(64)}):`,
|
|
''
|
|
].join('\n')
|
|
|
|
it('hashes the patch the way pnpm keys the store directory', () => {
|
|
expect(patchHash('diff --git a/x b/x\n')).toBe(
|
|
createHash('sha256').update('diff --git a/x b/x\n').digest('hex')
|
|
)
|
|
})
|
|
|
|
it('reads quoted and unquoted package keys', () => {
|
|
expect(readLockfilePatchHash(lockfile, '@xterm/xterm@6.1.0-beta.287')).toBe('0'.repeat(64))
|
|
expect(readLockfilePatchHash(lockfile, 'node-pty@1.1.0')).toBe('1'.repeat(64))
|
|
})
|
|
|
|
it('rewrites only the targeted entry', () => {
|
|
const updated = updateLockfilePatchHash(lockfile, '@xterm/xterm@6.1.0-beta.287', 'a'.repeat(64))
|
|
expect(readLockfilePatchHash(updated, '@xterm/xterm@6.1.0-beta.287')).toBe('a'.repeat(64))
|
|
expect(readLockfilePatchHash(updated, 'node-pty@1.1.0')).toBe('1'.repeat(64))
|
|
expect(updated.split('\n')).toHaveLength(lockfile.split('\n').length)
|
|
})
|
|
|
|
// pnpm repeats the hash in every resolution key. Rewriting only patchedDependencies
|
|
// installs fine on a warm store and drifts on a cold one, so it fails in CI only.
|
|
it('rewrites the resolution keys as well as patchedDependencies', () => {
|
|
const key = '@xterm/xterm@6.1.0-beta.287'
|
|
expect(readLockfileResolutionHashes(lockfile, key)).toEqual(['0'.repeat(64), '0'.repeat(64)])
|
|
|
|
const updated = updateLockfilePatchHash(lockfile, key, 'a'.repeat(64))
|
|
|
|
expect(readLockfileResolutionHashes(updated, key)).toEqual(['a'.repeat(64), 'a'.repeat(64)])
|
|
expect(readLockfileResolutionHashes(updated, 'node-pty@1.1.0')).toEqual(['1'.repeat(64)])
|
|
expect(updated).not.toContain('0'.repeat(64))
|
|
})
|
|
|
|
it('reports a lockfile stale in its resolution keys alone', () => {
|
|
const key = '@xterm/xterm@6.1.0-beta.287'
|
|
const halfUpdated = lockfile.replace(`hash: ${'0'.repeat(64)}`, `hash: ${'a'.repeat(64)}`)
|
|
|
|
expect(readLockfilePatchHash(halfUpdated, key)).toBe('a'.repeat(64))
|
|
expect(lockfilePatchHashIsStale(halfUpdated, key, 'a'.repeat(64))).toBe(true)
|
|
expect(lockfilePatchHashIsStale(lockfile, key, '0'.repeat(64))).toBe(false)
|
|
})
|
|
|
|
it('fails loudly when the package is not patched at all', () => {
|
|
expect(() => readLockfilePatchHash(lockfile, '@xterm/addon-webgl@0.20.0-beta.286')).toThrow(
|
|
/no patchedDependencies entry/
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('check-mode reporting', () => {
|
|
it('points at the source patch instead of the bundle', () => {
|
|
const message = formatCheckFailure({
|
|
name: '@xterm/xterm',
|
|
patchPath: 'config/patches/@xterm__xterm@6.1.0-beta.287.patch',
|
|
committed: 'diff --git a/lib/x.js b/lib/x.js\n@@ -1 +1 @@\n-a\n+b\n',
|
|
regenerated: 'diff --git a/lib/x.js b/lib/x.js\n@@ -1 +1 @@\n-a\n+c\n'
|
|
})
|
|
expect(message).toContain('Do not edit them')
|
|
expect(message).toContain('--write')
|
|
expect(message).toContain('docs/reference/xterm-patch-regeneration.md')
|
|
expect(message).toContain('files [lib/x.js]')
|
|
})
|
|
|
|
it('locates the first differing character', () => {
|
|
expect(firstDifferenceIndex('abc', 'abd')).toBe(2)
|
|
expect(firstDifferenceIndex('abc', 'abc')).toBe(-1)
|
|
expect(firstDifferenceIndex('abc', 'abcd')).toBe(3)
|
|
})
|
|
})
|
|
|
|
// These run without network or a build, so ordinary `pnpm test` catches the two
|
|
// desyncs that would otherwise only surface in the heavy xterm_patch_sync job.
|
|
describe('committed xterm patch artifacts', () => {
|
|
it('records the lockfile hash pnpm derives from the patch file', async () => {
|
|
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
|
|
const lockfile = await readFile(path.join(REPO_ROOT, 'pnpm-lock.yaml'), 'utf8')
|
|
for (const packageEntry of manifest.packages) {
|
|
const patch = await readFile(path.join(REPO_ROOT, packageEntry.patch), 'utf8')
|
|
const key = `${packageEntry.name}@${packageEntry.version}`
|
|
expect(readLockfilePatchHash(lockfile, key)).toBe(patchHash(patch))
|
|
}
|
|
})
|
|
|
|
it('keeps the source patch and the full patch equal on every source file', async () => {
|
|
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
|
|
for (const packageEntry of manifest.packages) {
|
|
const patch = await readFile(path.join(REPO_ROOT, packageEntry.patch), 'utf8')
|
|
const source = await readFile(path.join(REPO_ROOT, packageEntry.sourcePatch), 'utf8')
|
|
expect(sourceHunks(source)).toBe(sourceHunks(patch))
|
|
expect(generatedHunks(patch, packageEntry.generatedPaths)).not.toBe('')
|
|
}
|
|
})
|
|
|
|
it('pins a full upstream commit and a buildable package entry', async () => {
|
|
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
|
|
expect(manifest.upstream.commit).toMatch(/^[0-9a-f]{40}$/)
|
|
expect(manifest.packages.length).toBeGreaterThan(0)
|
|
expect(() => assertBuildStepsAllowed(manifest)).not.toThrow()
|
|
})
|
|
})
|