mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
498 lines
18 KiB
TypeScript
498 lines
18 KiB
TypeScript
import { execFile } from 'node:child_process'
|
|
import {
|
|
mkdtemp,
|
|
mkdir,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
symlink,
|
|
truncate,
|
|
writeFile
|
|
} from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { promisify } from 'node:util'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
installPluginFromLocalPath,
|
|
installPluginFromGit,
|
|
PLUGIN_LOCKFILE_MAX_BYTES,
|
|
readPluginLockfile,
|
|
removeInstalledPlugin
|
|
} from './plugin-install'
|
|
import { PLUGIN_MANIFEST_MAX_BYTES } from './plugin-manifest-file'
|
|
import { readPluginCurrentPointer } from './plugin-current-pointer'
|
|
import { writePluginLockfile } from './plugin-install-lockfile-store'
|
|
|
|
const roots: string[] = []
|
|
const execFileAsync = promisify(execFile)
|
|
|
|
async function tempRoot(prefix: string): Promise<string> {
|
|
const root = await mkdtemp(join(tmpdir(), prefix))
|
|
roots.push(root)
|
|
return root
|
|
}
|
|
|
|
async function writePluginSource(
|
|
root: string,
|
|
options: { id?: string; panelEntry?: string; includePanel?: boolean } = {}
|
|
): Promise<void> {
|
|
const panelEntry = options.panelEntry ?? 'panel.html'
|
|
await writeFile(
|
|
join(root, 'orca-plugin.json'),
|
|
JSON.stringify({
|
|
manifestVersion: 1,
|
|
id: options.id ?? 'demo',
|
|
publisher: 'orca-samples',
|
|
name: 'Demo',
|
|
version: '1.0.0',
|
|
engines: { orca: '>=1.0.0' },
|
|
pluginApi: 1,
|
|
contributes: {
|
|
panels: [{ id: 'panel', title: 'Panel', entry: panelEntry }],
|
|
commands: [],
|
|
events: []
|
|
},
|
|
capabilities: []
|
|
})
|
|
)
|
|
if (options.includePanel !== false) {
|
|
await writeFile(join(root, panelEntry), '<h1>Panel</h1>')
|
|
}
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
|
|
})
|
|
|
|
describe('installPluginFromLocalPath', () => {
|
|
it('refuses to allocate an oversized install lockfile', async () => {
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
const lockPath = join(pluginsDir, 'plugins.lock.json')
|
|
await writeFile(lockPath, '')
|
|
await truncate(lockPath, PLUGIN_LOCKFILE_MAX_BYTES + 1)
|
|
|
|
await expect(readPluginLockfile(pluginsDir)).resolves.toEqual({ version: 1, plugins: {} })
|
|
})
|
|
|
|
it('verifies copied content and writes a rollback-compatible consent field', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
|
|
const result = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(result.ok).toBe(true)
|
|
if (!result.ok) {
|
|
return
|
|
}
|
|
await expect(
|
|
readFile(join(pluginsDir, result.pluginKey, result.contentHash, 'panel.html'), 'utf8')
|
|
).resolves.toBe('<h1>Panel</h1>')
|
|
const lock = JSON.parse(await readFile(join(pluginsDir, 'plugins.lock.json'), 'utf8')) as {
|
|
plugins: Record<string, Record<string, unknown>>
|
|
}
|
|
expect(lock.plugins[result.pluginKey]).toMatchObject({
|
|
capabilityHash: result.consentFingerprint
|
|
})
|
|
expect(lock.plugins[result.pluginKey]).not.toHaveProperty('consentFingerprint')
|
|
})
|
|
|
|
it('skips root Git metadata before copying while still enforcing plugin limits', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
const gitDir = join(sourcePath, '.git')
|
|
await mkdir(gitDir)
|
|
await writeFile(join(gitDir, 'large.pack'), '')
|
|
await truncate(join(gitDir, 'large.pack'), 50 * 1024 * 1024 + 1)
|
|
|
|
const result = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(result).toMatchObject({ ok: true })
|
|
if (result.ok) {
|
|
await expect(
|
|
readFile(join(pluginsDir, result.pluginKey, result.contentHash, '.git', 'large.pack'))
|
|
).rejects.toMatchObject({ code: 'ENOENT' })
|
|
}
|
|
})
|
|
|
|
it('restores the previous current pointer when lockfile publication fails', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
const first = await installPluginFromLocalPath({ pluginsDir, sourcePath, hostVersion: '1.4.0' })
|
|
expect(first.ok).toBe(true)
|
|
if (!first.ok) {
|
|
return
|
|
}
|
|
await writeFile(join(sourcePath, 'panel.html'), '<h1>Updated</h1>')
|
|
await rm(join(pluginsDir, 'plugins.lock.json'))
|
|
await mkdir(join(pluginsDir, 'plugins.lock.json'))
|
|
|
|
const failed = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(failed).toMatchObject({ ok: false })
|
|
await expect(readPluginCurrentPointer(join(pluginsDir, first.pluginKey))).resolves.toBe(
|
|
first.contentHash
|
|
)
|
|
})
|
|
|
|
it('does not replace provenance when a same-content reinstall fails to publish', async () => {
|
|
const firstSource = await tempRoot('orca-plugin-source-')
|
|
const secondSource = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(firstSource)
|
|
await writePluginSource(secondSource)
|
|
const first = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath: firstSource,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(first.ok).toBe(true)
|
|
if (!first.ok) {
|
|
return
|
|
}
|
|
const acceptedLock = await readFile(join(pluginsDir, 'plugins.lock.json'), 'utf8')
|
|
await rm(join(pluginsDir, 'plugins.lock.json'))
|
|
await mkdir(join(pluginsDir, 'plugins.lock.json'))
|
|
|
|
const failed = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath: secondSource,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(failed).toMatchObject({ ok: false })
|
|
|
|
await rm(join(pluginsDir, 'plugins.lock.json'), { recursive: true })
|
|
await writeFile(join(pluginsDir, 'plugins.lock.json'), acceptedLock)
|
|
const recovered = await readPluginLockfile(pluginsDir)
|
|
expect(recovered.plugins[first.pluginKey]?.source).toEqual({
|
|
kind: 'local-path',
|
|
path: firstSource
|
|
})
|
|
})
|
|
|
|
it('preserves legacy lock provenance during a same-content reinstall', async () => {
|
|
const firstSource = await tempRoot('orca-plugin-source-')
|
|
const secondSource = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(firstSource)
|
|
await writePluginSource(secondSource)
|
|
const first = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath: firstSource,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(first.ok).toBe(true)
|
|
if (!first.ok) {
|
|
return
|
|
}
|
|
await rm(join(pluginsDir, first.pluginKey, '.install-provenance', `${first.contentHash}.json`))
|
|
const reinstalled = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath: secondSource,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(reinstalled).toMatchObject({ ok: true })
|
|
|
|
// Recovery from the newly backfilled provenance must retain the accepted
|
|
// legacy source rather than the same-byte reinstall's alternate source.
|
|
await rm(join(pluginsDir, 'plugins.lock.json'))
|
|
const recovered = await readPluginLockfile(pluginsDir)
|
|
expect(recovered.plugins[first.pluginKey]?.source).toEqual({
|
|
kind: 'local-path',
|
|
path: firstSource
|
|
})
|
|
})
|
|
|
|
it('retains only the current and immediately previous content versions', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
const hashes: string[] = []
|
|
for (const content of ['one', 'two', 'three']) {
|
|
await writeFile(join(sourcePath, 'panel.html'), `<h1>${content}</h1>`)
|
|
const result = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) {
|
|
hashes.push(result.contentHash)
|
|
}
|
|
}
|
|
|
|
const versionDirs = (
|
|
await readdir(join(pluginsDir, 'orca-samples.demo'), {
|
|
withFileTypes: true
|
|
})
|
|
)
|
|
.filter((entry) => entry.isDirectory() && /^[0-9a-f]{64}$/.test(entry.name))
|
|
.map((entry) => entry.name)
|
|
.sort()
|
|
expect(versionDirs).toEqual(hashes.slice(-2).sort())
|
|
})
|
|
|
|
it('keeps the rollback version when current content is reinstalled', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
const hashes: string[] = []
|
|
for (const content of ['one', 'two', 'two']) {
|
|
await writeFile(join(sourcePath, 'panel.html'), `<h1>${content}</h1>`)
|
|
const result = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(result.ok).toBe(true)
|
|
if (result.ok) {
|
|
hashes.push(result.contentHash)
|
|
}
|
|
}
|
|
|
|
const versionDirs = (
|
|
await readdir(join(pluginsDir, 'orca-samples.demo'), { withFileTypes: true })
|
|
)
|
|
.filter((entry) => entry.isDirectory() && /^[0-9a-f]{64}$/.test(entry.name))
|
|
.map((entry) => entry.name)
|
|
.sort()
|
|
expect(versionDirs).toEqual([...new Set(hashes)].sort())
|
|
})
|
|
|
|
it('repairs a pointer-new lock-old interrupted publication from provenance', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
const first = await installPluginFromLocalPath({ pluginsDir, sourcePath, hostVersion: '1.4.0' })
|
|
expect(first.ok).toBe(true)
|
|
const oldLock = await readFile(join(pluginsDir, 'plugins.lock.json'), 'utf8')
|
|
await writeFile(join(sourcePath, 'panel.html'), '<h1>new current</h1>')
|
|
const second = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(second.ok).toBe(true)
|
|
if (!first.ok || !second.ok) {
|
|
return
|
|
}
|
|
|
|
await writeFile(join(pluginsDir, 'plugins.lock.json'), oldLock)
|
|
const repaired = await readPluginLockfile(pluginsDir)
|
|
|
|
expect(repaired.plugins[second.pluginKey]?.contentHash).toBe(second.contentHash)
|
|
const persisted = JSON.parse(await readFile(join(pluginsDir, 'plugins.lock.json'), 'utf8')) as {
|
|
plugins: Record<string, { contentHash?: string }>
|
|
}
|
|
expect(persisted.plugins[second.pluginKey]?.contentHash).toBe(second.contentHash)
|
|
})
|
|
|
|
it('rejects a manifest whose declared panel artifact is missing', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath, { includePanel: false })
|
|
|
|
const result = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(result).toMatchObject({ ok: false })
|
|
})
|
|
|
|
it('rejects an oversized manifest without reading an unbounded JSON payload', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
const manifestPath = join(sourcePath, 'orca-plugin.json')
|
|
await writeFile(manifestPath, '')
|
|
await truncate(manifestPath, PLUGIN_MANIFEST_MAX_BYTES + 1)
|
|
|
|
const result = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(result).toMatchObject({ ok: false, error: expect.stringContaining('exceeds') })
|
|
})
|
|
|
|
it('serializes concurrent installs so lockfile entries are not lost', async () => {
|
|
const firstSource = await tempRoot('orca-plugin-source-')
|
|
const secondSource = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(firstSource, { id: 'first' })
|
|
await writePluginSource(secondSource, { id: 'second' })
|
|
|
|
const results = await Promise.all([
|
|
installPluginFromLocalPath({ pluginsDir, sourcePath: firstSource, hostVersion: '1.4.0' }),
|
|
installPluginFromLocalPath({ pluginsDir, sourcePath: secondSource, hostVersion: '1.4.0' })
|
|
])
|
|
expect(results.every((result) => result.ok)).toBe(true)
|
|
const lock = JSON.parse(await readFile(join(pluginsDir, 'plugins.lock.json'), 'utf8')) as {
|
|
plugins: Record<string, unknown>
|
|
}
|
|
expect(Object.keys(lock.plugins).sort()).toEqual(['orca-samples.first', 'orca-samples.second'])
|
|
})
|
|
|
|
it('serializes concurrent lockfile publications without temporary-file collisions', async () => {
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
const lock = { version: 1 as const, plugins: {} }
|
|
|
|
await expect(
|
|
Promise.all([
|
|
writePluginLockfile(pluginsDir, lock),
|
|
writePluginLockfile(pluginsDir, lock),
|
|
writePluginLockfile(pluginsDir, lock)
|
|
])
|
|
).resolves.toHaveLength(3)
|
|
await expect(readPluginLockfile(pluginsDir)).resolves.toEqual(lock)
|
|
})
|
|
|
|
it('refuses to repoint at a tampered existing content directory', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
const first = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
expect(first.ok).toBe(true)
|
|
if (!first.ok) {
|
|
return
|
|
}
|
|
await writeFile(
|
|
join(pluginsDir, first.pluginKey, first.contentHash, 'panel.html'),
|
|
'<h1>Tampered</h1>'
|
|
)
|
|
|
|
const second = await installPluginFromLocalPath({
|
|
pluginsDir,
|
|
sourcePath,
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(second).toMatchObject({
|
|
ok: false,
|
|
error: expect.stringContaining('integrity verification')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('installPluginFromGit', () => {
|
|
it('uses system Git, resolves the requested ref, and installs its exact bytes', async () => {
|
|
const sourcePath = await tempRoot('orca-plugin-git-source-')
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
await writePluginSource(sourcePath)
|
|
await execFileAsync('git', ['init', '--quiet'], { cwd: sourcePath })
|
|
await execFileAsync('git', ['config', 'user.email', 'plugins@example.invalid'], {
|
|
cwd: sourcePath
|
|
})
|
|
await execFileAsync('git', ['config', 'user.name', 'Plugin Test'], { cwd: sourcePath })
|
|
await execFileAsync('git', ['add', '.'], { cwd: sourcePath })
|
|
await execFileAsync('git', ['commit', '--quiet', '-m', 'fixture'], { cwd: sourcePath })
|
|
await execFileAsync('git', ['tag', 'v1.0.0'], { cwd: sourcePath })
|
|
const { stdout: commitStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
cwd: sourcePath
|
|
})
|
|
|
|
const configKeys = ['GIT_CONFIG_COUNT', 'GIT_CONFIG_KEY_0', 'GIT_CONFIG_VALUE_0'] as const
|
|
const previous = Object.fromEntries(configKeys.map((key) => [key, process.env[key]]))
|
|
process.env.GIT_CONFIG_COUNT = '1'
|
|
process.env.GIT_CONFIG_KEY_0 = `url.${pathToFileURL(sourcePath).href}.insteadOf`
|
|
process.env.GIT_CONFIG_VALUE_0 = 'https://plugin.test/demo.git'
|
|
try {
|
|
const result = await installPluginFromGit({
|
|
pluginsDir,
|
|
url: 'https://plugin.test/demo.git',
|
|
ref: 'v1.0.0',
|
|
hostVersion: '1.4.0'
|
|
})
|
|
|
|
expect(result).toMatchObject({ ok: true, resolvedCommit: commitStdout.trim() })
|
|
if (result.ok) {
|
|
await expect(
|
|
readFile(join(pluginsDir, result.pluginKey, result.contentHash, 'panel.html'), 'utf8')
|
|
).resolves.toBe('<h1>Panel</h1>')
|
|
}
|
|
} finally {
|
|
for (const key of configKeys) {
|
|
const value = previous[key]
|
|
if (value === undefined) {
|
|
delete process.env[key]
|
|
} else {
|
|
process.env[key] = value
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('removeInstalledPlugin', () => {
|
|
it('rejects an unqualified or traversing key before removing anything', async () => {
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
const pluginsDataDir = await tempRoot('orca-plugin-data-')
|
|
const outside = join(await tempRoot('orca-plugin-outside-'), 'keep.txt')
|
|
await writeFile(outside, 'keep')
|
|
|
|
await expect(
|
|
removeInstalledPlugin({ pluginsDir, pluginsDataDir, pluginKey: '../outside' })
|
|
).rejects.toThrow('invalid qualified plugin key')
|
|
await expect(readFile(outside, 'utf8')).resolves.toBe('keep')
|
|
})
|
|
|
|
it('rejects a resolved uninstall target outside its root', async () => {
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
const pluginsDataDir = await tempRoot('orca-plugin-data-')
|
|
const outside = await tempRoot('orca-plugin-outside-')
|
|
const marker = join(outside, 'keep.txt')
|
|
await writeFile(marker, 'keep')
|
|
await symlink(
|
|
outside,
|
|
join(pluginsDir, 'orca-samples.demo'),
|
|
process.platform === 'win32' ? 'junction' : 'dir'
|
|
)
|
|
|
|
await expect(
|
|
removeInstalledPlugin({
|
|
pluginsDir,
|
|
pluginsDataDir,
|
|
pluginKey: 'orca-samples.demo'
|
|
})
|
|
).rejects.toThrow('outside')
|
|
await expect(readFile(marker, 'utf8')).resolves.toBe('keep')
|
|
})
|
|
|
|
it('removes qualified install and data directories', async () => {
|
|
const pluginsDir = await tempRoot('orca-plugin-installs-')
|
|
const pluginsDataDir = await tempRoot('orca-plugin-data-')
|
|
const key = 'orca-samples.demo'
|
|
await mkdir(join(pluginsDir, key))
|
|
await mkdir(join(pluginsDataDir, key))
|
|
await writeFile(join(pluginsDir, key, 'content'), 'installed')
|
|
await writeFile(join(pluginsDataDir, key, 'storage.json'), '{}')
|
|
|
|
await removeInstalledPlugin({ pluginsDir, pluginsDataDir, pluginKey: key })
|
|
|
|
await expect(readFile(join(pluginsDir, key, 'content'))).rejects.toThrow()
|
|
await expect(readFile(join(pluginsDataDir, key, 'storage.json'))).rejects.toThrow()
|
|
})
|
|
})
|