Files
orca/src/shared/plugin-git-download-binary-compatibility.test-cases.ts
T
Neil 751e974f88 feat(plugins): download sources without checkout and pin catalog releases in a v2 manifest/catalog (#30636)
* plugins: download Git sources without checkout, within a protocol allowlist and size cap

Plugin and marketplace sources were a real clone/checkout, so user hooks,
smudge filters and core.hooksPath ran, insteadOf rules could reach any
transport, and nothing bounded the download. Now a --no-checkout depth-1
clone (or exact-commit fetch) runs with an empty hooks path, only the https
and ssh transports, and Orca writes the commit's exact blobs from ls-tree +
cat-file --batch. Symlinks, submodules, .git aliases, colliding portable
paths and SHA-256 repositories are refused. A size watcher stops the whole
Git process tree once the pack outgrows an approximate cap.

The exact argv lives in shared code and joins the real-Git compatibility
matrix, so the Git 2.25 lane runs it. Tests reach fixtures over an SSH
shim, since file transports are now refused.

* plugins: manifest v2 declares an execution profile

data < instructional < sandboxed-ui < trusted-worker. A v2 manifest is
strict and fails when it carries contributions beyond its profile; v1
manifests keep loading with the least profile inferred from what they
declare.

* plugins: pin exact catalog releases in orca-marketplace.v2.json

A v2 catalog entry carries the release it vouches for: commit, content
hash, version, profile, minimum Orca and contribution types. Preview and
install fetch that commit directly and refuse bytes that differ. The v1
file stays strict and frozen for shipped clients; v2 entries strip unknown
keys, and entries this client cannot install are dropped at parse time.

Catalog fetches read only the index blobs under their own download cap,
so a catalog repository is not held to one plugin's limits. Release-bearing
snapshots are cached under a new key, so an older build never reads them
and simply refetches.

readMarketplaceJson and parseMarketplaceFile from #30612 are replaced by
parsePluginMarketplaceIndex: catalogs are now read as Git blobs, so the
on-disk JSON read has no caller left.

* test(plugins): serve marketplace e2e fixtures over SSH

Plugin downloads now refuse the file transport, even through an insteadOf
rewrite, so the fresh-profile marketplace journey reaches its fixture
repositories through the GIT_SSH_COMMAND shim the unit tests use.

* test(plugins): give the Git compatibility plugin source its own identity

* test(plugins): record the plugin fixture's executable bit in the Git index on every platform
2026-10-28 13:36:49 -07:00

67 lines
2.9 KiB
TypeScript

import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import {
pluginGitCloneArgs,
pluginGitConfigArgs,
pluginGitFetchCommitArgs,
pluginGitTreeArgs
} from './plugins/plugin-git-arguments'
export function registerPluginGitDownloadBinaryCompatibilityCase(
runGit: (
args: string[],
env?: NodeJS.ProcessEnv,
stdin?: string
) => Promise<{ stdout: string; stderr: string }>,
getRepoPath: () => string
): void {
// Why pin this: plugin downloads pass every `-c` key and flag below to the user's Git, and a
// local path counts as the file transport, so the protocol policy is observable offline.
it('downloads a plugin commit with the exact no-checkout arguments', async () => {
const source = 'compat-plugin-source'
await mkdir(join(getRepoPath(), source, 'bin'), { recursive: true })
await writeFile(join(getRepoPath(), source, 'bin', 'run'), 'plugin\n', { mode: 0o755 })
await runGit(['init', '-q', source])
await runGit(['-C', source, 'add', '--all'])
// Why: Windows ignores the 0o755 file mode, so record the executable bit in the index directly.
await runGit(['-C', source, 'update-index', '--chmod=+x', 'bin/run'])
// Why inline identity: a fresh repository does not inherit the fixture repository's config.
const identity = [
'-c',
'user.name=Compatibility Test',
'-c',
'user.email=compatibility@example.invalid'
]
await runGit(['-C', source, ...identity, 'commit', '-qm', 'plugin'])
await runGit(['-C', source, 'tag', 'v1'])
const commit = (await runGit(['-C', source, 'rev-parse', 'HEAD'])).stdout.trim()
const config = pluginGitConfigArgs('compat-no-hooks')
const allowLocal = ['-c', 'protocol.file.allow=always']
await expect(
runGit([...config, ...pluginGitCloneArgs(source, 'v1', 'refused')])
).rejects.toThrow("transport 'file' not allowed")
await runGit([...config, ...allowLocal, ...pluginGitCloneArgs(source, 'v1', 'compat-clone')])
await runGit([...config, 'init', '--quiet', '--template=', 'compat-fetch'])
await runGit(['-C', 'compat-fetch', ...config, 'remote', 'add', 'origin', `../${source}`])
await runGit([
'-C',
'compat-fetch',
...config,
...allowLocal,
...pluginGitFetchCommitArgs(commit)
])
for (const repository of ['compat-clone', 'compat-fetch']) {
const tree = await runGit(['-C', repository, ...pluginGitTreeArgs(commit)])
const [directory, file] = tree.stdout.split('\0')
expect(directory).toMatch(/^040000 tree [0-9a-f]{40} +-\tbin$/)
expect(file).toMatch(/^100755 blob [0-9a-f]{40} +7\tbin\/run$/)
const object = file?.slice(12, 52) ?? ''
const blob = await runGit(['-C', repository, 'cat-file', '--batch'], {}, `${object}\n`)
expect(blob.stdout).toBe(`${object} blob 7\nplugin\n\n`)
}
})
}