Merge remote-tracking branch 'origin/master' into worktree/fix-minimal-preset-prompt
This commit is contained in:
@@ -1,218 +0,0 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BUNDLED_PNPM_VERSION, RepositoryCache, type RepositoryInstall } from '@cordisjs/plugin-loader/repository'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const roots: string[] = []
|
||||
|
||||
/** Normalize Git's platform checkout line endings for source-content assertions. */
|
||||
const lf = (text: string): string => text.replace(/\r\n/g, '\n')
|
||||
|
||||
async function temporaryRoot(name: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), `cordis-${name}-`))
|
||||
roots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
async function fakePackage(directory: string): Promise<void> {
|
||||
const target = join(directory, 'node_modules', 'repository')
|
||||
await mkdir(target, { recursive: true })
|
||||
await writeFile(join(target, 'package.json'), '{"name":"fixture"}\n')
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs()
|
||||
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
describe('RepositoryCache', () => {
|
||||
it('single-flights and permanently reuses an exact specifier', async () => {
|
||||
const root = await temporaryRoot('repository-cache')
|
||||
const calls: string[] = []
|
||||
const install: RepositoryInstall = async (directory) => {
|
||||
calls.push(directory)
|
||||
await fakePackage(directory)
|
||||
}
|
||||
const cache = new RepositoryCache(root, { install })
|
||||
const specifier = 'github:owner/repository#0123456789abcdef'
|
||||
|
||||
const [first, concurrent] = await Promise.all([cache.resolve(specifier), cache.resolve(specifier)])
|
||||
expect(concurrent).toBe(first)
|
||||
expect(calls).toHaveLength(1)
|
||||
|
||||
const reopened = new RepositoryCache(root, { install: async () => { throw new Error('cache miss') } })
|
||||
expect(await reopened.resolve(specifier)).toBe(first)
|
||||
expect(JSON.parse(await readFile(join(first, '..', '..', 'package.json'), 'utf8'))).toMatchObject({
|
||||
packageManager: `pnpm@${BUNDLED_PNPM_VERSION}`,
|
||||
dependencies: { repository: specifier },
|
||||
})
|
||||
|
||||
const second = await cache.resolve('github:owner/repository#fedcba9876543210')
|
||||
expect(second).not.toBe(first)
|
||||
expect(calls).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('accepts the valid winner when independent cache instances race', async () => {
|
||||
const root = await temporaryRoot('repository-race')
|
||||
const bothStarted = Promise.withResolvers<undefined>()
|
||||
let starts = 0
|
||||
const install: RepositoryInstall = async (directory) => {
|
||||
await fakePackage(directory)
|
||||
starts += 1
|
||||
if (starts === 2) bothStarted.resolve(undefined)
|
||||
await bothStarted.promise
|
||||
}
|
||||
const specifier = 'github:owner/repository#race'
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
new RepositoryCache(root, { install }).resolve(specifier),
|
||||
new RepositoryCache(root, { install }).resolve(specifier),
|
||||
])
|
||||
|
||||
expect(second).toBe(first)
|
||||
expect(starts).toBe(2)
|
||||
expect(await readdir(root)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes a failed staging tree and permits an exact retry', async () => {
|
||||
const root = await temporaryRoot('repository-retry')
|
||||
let attempts = 0
|
||||
const cache = new RepositoryCache(root, { install: async (directory) => {
|
||||
attempts += 1
|
||||
if (attempts === 1) throw new Error('install failed')
|
||||
await fakePackage(directory)
|
||||
} })
|
||||
|
||||
await expect(cache.resolve('github:owner/repository#ref')).rejects.toThrow('failed to prepare repository')
|
||||
expect(await readdir(root)).toEqual([])
|
||||
await expect(cache.resolve('github:owner/repository#ref')).resolves.toContain('node_modules')
|
||||
expect(attempts).toBe(2)
|
||||
})
|
||||
|
||||
it('rejects empty or padded specifiers before touching the cache', async () => {
|
||||
const root = await temporaryRoot('repository-input')
|
||||
const cache = new RepositoryCache(root, { install: fakePackage })
|
||||
expect(() => cache.resolve('')).toThrow('non-empty unpadded string')
|
||||
expect(() => cache.resolve(' github:owner/repository#ref')).toThrow('non-empty unpadded string')
|
||||
await expect(readdir(root)).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('fails loud on a corrupt published marker instead of reinstalling it', async () => {
|
||||
const root = await temporaryRoot('repository-corrupt')
|
||||
const specifier = 'github:owner/repository#corrupt'
|
||||
const key = createHash('sha256').update(specifier).digest('hex')
|
||||
const entry = join(root, key)
|
||||
await mkdir(join(entry, 'node_modules', 'repository'), { recursive: true })
|
||||
await writeFile(join(entry, '.repository-cache.json'), '{}\n')
|
||||
const cache = new RepositoryCache(root, { install: async () => { throw new Error('must not reinstall') } })
|
||||
|
||||
await expect(cache.resolve(specifier)).rejects.toThrow('repository cache marker is invalid')
|
||||
})
|
||||
|
||||
it('isolates and prepares a .dsh-plugin Git subpath from an enclosing pnpm workspace', { timeout: 60_000 }, async () => {
|
||||
const root = await temporaryRoot('repository-pnpm')
|
||||
const repository = join(root, 'source')
|
||||
await mkdir(join(repository, '.dsh-plugin'), { recursive: true })
|
||||
await mkdir(join(repository, '.dsh-plugin', 'build-helper'), { recursive: true })
|
||||
await mkdir(join(repository, '.dsh-plugin', 'prepare-helper'), { recursive: true })
|
||||
await mkdir(join(repository, 'skills', 'fixture'), { recursive: true })
|
||||
const shadowPnpm = join(root, 'shadow-pnpm')
|
||||
await mkdir(shadowPnpm)
|
||||
await writeFile(join(shadowPnpm, 'pnpm'), '#!/bin/sh\nexit 99\n', { mode: 0o700 })
|
||||
await writeFile(join(shadowPnpm, 'pnpm.bat'), '@exit /b 99\r\n')
|
||||
await writeFile(join(repository, 'package.json'), `${JSON.stringify({
|
||||
name: 'repository-fixture',
|
||||
private: true,
|
||||
version: '1.0.0',
|
||||
packageManager: `pnpm@${BUNDLED_PNPM_VERSION}`,
|
||||
})}\n`)
|
||||
await writeFile(join(repository, 'pnpm-workspace.yaml'), 'packages: []\n')
|
||||
await writeFile(join(repository, 'pnpm-lock.yaml'), [
|
||||
"lockfileVersion: '9.0'",
|
||||
'settings:',
|
||||
' autoInstallPeers: true',
|
||||
' excludeLinksFromLockfile: false',
|
||||
'importers:',
|
||||
' .: {}',
|
||||
'',
|
||||
].join('\n'))
|
||||
await writeFile(join(repository, '.dsh-plugin', 'build-helper', 'package.json'), `${JSON.stringify({
|
||||
name: 'repository-build-helper',
|
||||
version: '1.0.0',
|
||||
bin: 'index.js',
|
||||
})}\n`)
|
||||
await writeFile(join(repository, '.dsh-plugin', 'build-helper', 'index.js'), [
|
||||
'#!/usr/bin/env node',
|
||||
"require('node:fs').writeFileSync('dependency-built.txt', 'dependency available\\n')",
|
||||
'',
|
||||
].join('\n'), { mode: 0o700 })
|
||||
await writeFile(join(repository, '.dsh-plugin', 'prepare-helper', 'package.json'), `${JSON.stringify({
|
||||
name: 'repository-prepare-helper',
|
||||
version: '1.0.0',
|
||||
bin: { 'dsh-plugin-prepare': 'index.js' },
|
||||
})}\n`)
|
||||
await writeFile(join(repository, '.dsh-plugin', 'prepare-helper', 'index.js'), [
|
||||
'#!/usr/bin/env node',
|
||||
"const { cpSync, mkdirSync, writeFileSync } = require('node:fs')",
|
||||
"mkdirSync('dsh-plugin-assets/skills', { recursive: true })",
|
||||
"cpSync('../skills', 'dsh-plugin-assets/skills/0', { recursive: true })",
|
||||
"writeFileSync('dsh-plugin.mjs', 'export function apply() {}\\n')",
|
||||
"writeFileSync('prepared.txt', `${process.env.REPOSITORY_TEST_VISIBLE ?? 'absent'}|${process.env.REPOSITORY_TEST_TOKEN ?? 'absent'}|${process.env.PNPM_CONFIG_IGNORE_WORKSPACE ?? 'absent'}\\n`)",
|
||||
"writeFileSync('environment.json', `${JSON.stringify({ path: process.env.PATH, pathExt: process.env.PATHEXT })}\\n`)",
|
||||
'',
|
||||
].join('\n'), { mode: 0o700 })
|
||||
await writeFile(join(repository, 'skills', 'fixture', 'SKILL.md'), 'repository skill source\n')
|
||||
await writeFile(join(repository, '.dsh-plugin', 'package.json'), `${JSON.stringify({
|
||||
name: 'repository-plugin-fixture',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
// The fixture owns dependency installation, not platform-specific
|
||||
// node_modules/.bin shim generation during pnpm's Git preparation.
|
||||
prepack: [
|
||||
'node ./node_modules/repository-build-helper/index.js',
|
||||
'node ./node_modules/repository-prepare-helper/index.js',
|
||||
].join(' && '),
|
||||
},
|
||||
devDependencies: {
|
||||
'repository-build-helper': 'file:./build-helper',
|
||||
'repository-prepare-helper': 'file:./prepare-helper',
|
||||
},
|
||||
dsh: { skills: ['../skills'] },
|
||||
})}\n`)
|
||||
await execFileAsync('git', ['init', '--quiet'], { cwd: repository })
|
||||
await execFileAsync('git', ['add', '.'], { cwd: repository })
|
||||
await execFileAsync('git', [
|
||||
'-c', 'user.name=Repository Fixture',
|
||||
'-c', 'user.email=repository@example.invalid',
|
||||
'commit', '--quiet', '-m', 'fixture',
|
||||
], { cwd: repository })
|
||||
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repository, encoding: 'utf8' })
|
||||
const specifier = `git+${pathToFileURL(repository).href}#${stdout.trim()}&path:/.dsh-plugin`
|
||||
vi.stubEnv('REPOSITORY_TEST_VISIBLE', 'visible')
|
||||
vi.stubEnv('REPOSITORY_TEST_TOKEN', 'hidden')
|
||||
vi.stubEnv('PNPM_HOME', shadowPnpm)
|
||||
vi.stubEnv('PATH', [shadowPnpm, ...(process.env.PATH === undefined ? [] : [process.env.PATH])].join(delimiter))
|
||||
vi.stubEnv('PATHEXT', '.BAT;.CMD;.EXE')
|
||||
|
||||
const installed = await new RepositoryCache(join(root, 'cache')).resolve(specifier)
|
||||
await expect(readFile(join(installed, 'dependency-built.txt'), 'utf8')).resolves.toBe('dependency available\n')
|
||||
await expect(readFile(join(installed, 'prepared.txt'), 'utf8')).resolves.toBe('visible|absent|true\n')
|
||||
const environment = JSON.parse(await readFile(join(installed, 'environment.json'), 'utf8')) as {
|
||||
path: string
|
||||
pathExt: string
|
||||
}
|
||||
expect(environment.path.split(delimiter)).not.toContain(shadowPnpm)
|
||||
expect(environment.pathExt.split(';')[0]?.toUpperCase()).toBe('.CMD')
|
||||
await expect(readFile(join(installed, 'dsh-plugin.mjs'), 'utf8')).resolves.toContain('export function apply')
|
||||
expect(lf(await readFile(join(installed, 'dsh-plugin-assets/skills/0/fixture/SKILL.md'), 'utf8')))
|
||||
.toBe('repository skill source\n')
|
||||
await expect(readFile(join(installed, 'package.json'), 'utf8'))
|
||||
.resolves.toContain('repository-plugin-fixture')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user