diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index 6a95b2861a..bb6ab4ec16 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -55,6 +55,24 @@ function git(fixture: Fixture, cwd: string, args: string[]): string { return result.stdout.trim() } +/** Remove the line terminator Git appends to a printed path, as the installer does. */ +function stripGitLineTerminator(output: string): string { + const withoutLineFeed = output.endsWith('\n') ? output.slice(0, -1) : output + return process.platform === 'win32' && withoutLineFeed.endsWith('\r') + ? withoutLineFeed.slice(0, -1) + : withoutLineFeed +} + +// `git()` trims, which also drops a worktree directory's own trailing whitespace. Paths that must +// equal the installer's own reading keep only the line terminator, so a trailing space survives. +function gitPath(fixture: Fixture, cwd: string, args: string[]): string { + const result = gitResult(fixture, cwd, args) + if (result.status !== 0) { + throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`) + } + return stripGitLineTerminator(result.stdout) +} + function write(path: string, content: string, mode?: number): void { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, content, mode === undefined ? undefined : { mode }) @@ -161,12 +179,12 @@ function gitDirectory(fixture: Fixture, root: string): string { // The installer resolves the common directory against Git's own top-level path, and the host // spells that path differently from the fixture directory this file created: Git canonicalizes // Windows 8.3 short names and the macOS `/var` symlink, while `mkdtempSync` returns the temp -// directory's own spelling. Resolving the relative common directory against Git's top-level -// instead of the fixture directory gives the same string the installer computes, so an injected -// failure on this path reaches the installer's own lock instead of missing it. +// directory's own spelling. Reading both values the way the installer reads them (`gitPath`, not +// the trimming `git()`) keeps the relative common directory resolved to the same string the +// installer computes, so an injected failure on this path reaches the installer's own lock. function commonDirectory(fixture: Fixture): string { - const root = git(fixture, fixture.main, ['rev-parse', '--show-toplevel']) - const output = git(fixture, fixture.main, ['rev-parse', '--git-common-dir']) + const root = gitPath(fixture, fixture.main, ['rev-parse', '--show-toplevel']) + const output = gitPath(fixture, fixture.main, ['rev-parse', '--git-common-dir']) return isAbsolute(output) ? output : resolve(root, output) } @@ -397,6 +415,15 @@ syncBuiltinESMExports() await expectInjectedLockAccessFailure(fixture, { operation: 'openSync', code: 'EPERM', expires: false }) }) + // A worktree directory can end in a space on POSIX hosts; Git prints that path verbatim while a + // trimming read would drop the space and name a different directory, so the injection only fires + // when the expected path is read the installer's way. Windows cannot create such a directory. + it.skipIf(process.platform === 'win32')('handles an injected lock failure in a worktree whose path ends with a space', async () => { + await expectInjectedLockAccessFailure(createFixture({ main: 'main ' }), { + operation: 'openSync', code: 'EPERM', expires: false, + }) + }) + it('waits for a concurrent installer to finish publishing its lock record', async () => { const fixture = createFixture() const lockPath = installLockPath(fixture)