// Nightly: requires a non-core registry command./** * Integration tests for git command. * * Verifies init, add, commit, branch, checkout (with DWIM), plus local and * smart-HTTP remote clone via kernel.exec() with real WASM binaries. */ import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from 'vitest';import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';import { resolve, join } from 'node:path';import { tmpdir } from 'node:os';import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https';import { spawn, spawnSync, execSync } from 'node:child_process';import { createHash } from 'node:crypto';import { createWasmVmRuntime } from '@rivet-dev/agentos-test-harness';import { allowAll, COMMANDS_DIR, createInMemoryFileSystem, createKernel, describeIf, hasWasmBinaries,} from '@rivet-dev/agentos-test-harness';import type { Kernel } from '@rivet-dev/agentos-test-harness'; vi.setConfig({ testTimeout: 30_000 }); /** Check git binary exists in addition to base WASM binaries */const hasGit = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'git'));const hasHostGit = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0;// Smart HTTP needs Git's libcurl-backed remote helper. It is now a real second// WASM binary (git-remote-http links the overlaid mbedTLS libcurl in-process);// git-remote-https aliases to it.const hasGitHttpHelper = hasGit && existsSync(resolve(COMMANDS_DIR, 'git-remote-http'));// The real OpenSSH client (software/ssh) lights up git-over-ssh; its presence// changes how ssh:// remotes fail when unreachable.const hasSshClient = existsSync(resolve(COMMANDS_DIR, 'ssh')); const gitConfig = [ '-c safe.directory=*', '-c init.defaultBranch=main', '-c user.name=agentos', '-c user.email=agentos@example.invalid',].join(' '); function git(args: string) { return `git ${gitConfig} ${args}`;} /** Create a kernel with a world-writable in-memory filesystem */async function createGitKernel() { const vfs = createInMemoryFileSystem(); // Make root and /tmp writable by all users (WASM processes run as non-root) await (vfs as any).chmod('/', 0o1777); await vfs.mkdir('/tmp', { recursive: true }); await (vfs as any).chmod('/tmp', 0o1777); const kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); return { kernel, vfs, dispose: () => kernel.dispose() };} async function createGitKernelWithNet(loopbackExemptPorts: number[], seededCaPem?: string) { const vfs = createInMemoryFileSystem(); await (vfs as any).chmod('/', 0o1777); await vfs.mkdir('/tmp', { recursive: true }); await (vfs as any).chmod('/tmp', 0o1777); const kernel = createKernel({ filesystem: vfs, permissions: allowAll, loopbackExemptPorts, syncFilesystemOnDispose: false, }); await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); // Seed the Debian-shaped trust store the way the native VM bootstrap does, so // libcurl's compile-time default CA bundle (/etc/ssl/certs/ca-certificates.crt) // resolves in-guest for git-remote-http's mbedTLS backend. if (seededCaPem) { await vfs.mkdir('/etc/ssl/certs', { recursive: true }); await kernel.writeFile('/etc/ssl/certs/ca-certificates.crt', seededCaPem); } return { kernel, vfs, dispose: () => kernel.dispose() };} // Build a real CA and a leaf server certificate signed by it, with a SAN that// covers the 127.0.0.1 loopback endpoint the VM connects to. This lets// git-remote-http's mbedTLS backend perform genuine chain + hostname// verification, exactly like Linux git against a private CA.function makeCaSignedCert(caCommonName: string): { caPem: string; serverKey: string; serverCert: string;} { const dir = mkdtempSync(join(tmpdir(), 'git-ca-')); try { execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/ca.key" 2>/dev/null`); execSync( `openssl req -x509 -new -key "${dir}/ca.key" -days 3650 -subj "/CN=${caCommonName}" -out "${dir}/ca.crt" 2>/dev/null`, ); execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/srv.key" 2>/dev/null`); execSync(`openssl req -new -key "${dir}/srv.key" -subj "/CN=localhost" -out "${dir}/srv.csr" 2>/dev/null`); writeFileSync(`${dir}/ext.cnf`, 'subjectAltName=DNS:localhost,IP:127.0.0.1\n'); execSync( `openssl x509 -req -in "${dir}/srv.csr" -CA "${dir}/ca.crt" -CAkey "${dir}/ca.key" ` + `-CAcreateserial -days 3650 -extfile "${dir}/ext.cnf" -out "${dir}/srv.crt" 2>/dev/null`, ); return { caPem: readFileSync(`${dir}/ca.crt`, 'utf8'), serverKey: readFileSync(`${dir}/srv.key`, 'utf8'), serverCert: readFileSync(`${dir}/srv.crt`, 'utf8'), }; } finally { rmSync(dir, { recursive: true, force: true }); }} function runHostGit(args: string[], cwd?: string) { const result = spawnSync('git', args, { cwd, encoding: 'utf8', }); if (result.status !== 0) { throw new Error( `host git failed: git ${args.join(' ')}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, ); }} async function runHostGitResult(args: string[], cwd?: string) { return await new Promise<{ status: number | null; stdout: string; stderr: string }>((resolveResult, reject) => { const child = spawn('git', args, { cwd }); const stdout: Buffer[] = []; const stderr: Buffer[] = []; child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk))); child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk))); child.on('error', reject); child.on('close', (status) => { resolveResult({ status, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), }); }); });} /** Helper: run command and assert success */async function run(kernel: Kernel, cmd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { const r = await kernel.exec(cmd); if (r.exitCode !== 0) { throw new Error(`Command failed (exit ${r.exitCode}): ${cmd}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`); } return r;} async function expectGitRef(kernel: Kernel, repo: string, ref: string) { const result = await run(kernel, git(`-C ${repo} rev-parse --verify ${ref}`)); expect(result.stdout.trim()).toMatch(/^[0-9a-f]{40,64}$/);} function sidebandPacket(band: 1 | 2 | 3, payload: Uint8Array): Uint8Array { const packetLength = payload.length + 5; const header = new TextEncoder().encode(packetLength.toString(16).padStart(4, '0')); const packet = new Uint8Array(packetLength); packet.set(header, 0); packet[4] = band; packet.set(payload, 5); return packet;} function concatBytes(chunks: Uint8Array[]): Uint8Array { const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length; } return output;} // TODO(P6): requires git WASM artifact, intentionally excluded from the fast software-build gate.describeIf(hasGit, 'git command', () => { let kernel: Kernel; let vfs: any; let dispose: () => Promise<void>; afterEach(async () => { await dispose?.(); }); it('init creates .git directory structure', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); const result = await run(kernel, git('init /repo')); expect(result.stdout).toContain('Initialized empty Git repository'); expect(await vfs.exists('/repo/.git/HEAD')).toBe(true); expect(await vfs.exists('/repo/.git/objects')).toBe(true); expect(await vfs.exists('/repo/.git/refs/heads')).toBe(true); const head = new TextDecoder().decode(await vfs.readFile('/repo/.git/HEAD')); expect(head.trim()).toBe('ref: refs/heads/main'); }); it('add + commit creates objects and updates ref', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /repo')); await kernel.writeFile('/repo/hello.txt', 'hello world\n'); await run(kernel, git('-C /repo add hello.txt')); await run(kernel, git("-C /repo commit -m 'first commit'")); expect(await vfs.exists('/repo/.git/refs/heads/main')).toBe(true); }); it('hidden sideband helper repeatedly streams band 1 and forwards progress without spooling', async () => { ({ kernel, dispose } = await createGitKernel()); const encoder = new TextEncoder(); const payloadChunks = Array.from({ length: 128 }, (_, index) => encoder.encode(`pack-${index.toString().padStart(3, '0')}:${'x'.repeat(4096)}\n`)); const stream: Uint8Array[] = []; for (const [index, payload] of payloadChunks.entries()) { stream.push(sidebandPacket(1, payload)); if (index % 16 === 0) stream.push(sidebandPacket(2, encoder.encode(`progress ${index}\n`))); } stream.push(encoder.encode('0000')); const result = await kernel.exec('git sideband--helper demux parity-test 1', { stdin: concatBytes(stream), }); expect(result.exitCode, result.stderr).toBe(0); expect(result.stdout).toBe(new TextDecoder().decode(concatBytes(payloadChunks))); expect(result.stderr).toContain('progress 0'); expect(result.stderr).toContain('progress 112'); const coloredProgress = concatBytes([ sidebandPacket(2, encoder.encode('\x1b[31mred\x1b[0m\n')), encoder.encode('0000'), ]); const sanitized = await kernel.exec('git sideband--helper demux parity-test 0', { stdin: coloredProgress, }); expect(sanitized.exitCode, sanitized.stderr).toBe(0); expect(sanitized.stderr).toContain('^[[31mred^[[0m'); const ansiAllowed = await kernel.exec('git sideband--helper demux parity-test 1', { stdin: coloredProgress, }); expect(ansiAllowed.exitCode, ansiAllowed.stderr).toBe(0); expect(ansiAllowed.stderr).toContain('\x1b[31mred\x1b[0m'); const invalidPolicy = await kernel.exec('git sideband--helper demux parity-test 16', { stdin: coloredProgress, }); expect(invalidPolicy.exitCode).not.toBe(0); expect(invalidPolicy.stderr).toContain('invalid sideband control-character policy'); const muxed = await kernel.exec('git sideband--helper mux 65520 0 5', { stdin: 'receive progress\n', }); expect(muxed.exitCode, muxed.stderr).toBe(0); expect(muxed.stdout).toMatch(/^[0-9a-f]{4}\u0002receive progress\n$/); const help = await kernel.exec('git help -a'); expect(help.stdout).not.toContain('sideband--helper'); const patchText = readFileSync( resolve(import.meta.dirname, '../../../toolchain/c/patches/git/0002-wasi-synchronous-sideband-demux.patch'), 'utf8', ); const addedImplementation = patchText .split('\n') .filter((line) => line.startsWith('+') && !line.startsWith('+++')) .join('\n'); expect(addedImplementation).not.toContain('tmp_sideband_'); expect(addedImplementation).not.toMatch(/odb_mkstemp|spool_fd|spool_tempfile/); }); it('branch lists branches with current marked', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /repo')); await kernel.writeFile('/repo/file.txt', 'content\n'); await run(kernel, git('-C /repo add file.txt')); await run(kernel, git("-C /repo commit -m 'init'")); const result = await run(kernel, git('-C /repo branch')); expect(result.stdout.trim()).toBe('* main'); }); it('checkout -b creates a new branch', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /repo')); await kernel.writeFile('/repo/file.txt', 'content\n'); await run(kernel, git('-C /repo add file.txt')); await run(kernel, git("-C /repo commit -m 'init'")); await run(kernel, git('-C /repo checkout -b feature')); const result = await run(kernel, git('-C /repo branch')); const lines = result.stdout.trim().split('\n').map((l: string) => l.trim()); expect(lines).toContain('* feature'); expect(lines).toContain('main'); }); it('full quickstart scenario: init, commit, branch, clone, checkout', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); // Create origin repo await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'initial commit'")); // Check default branch let r = await run(kernel, git('-C /tmp/origin branch')); expect(r.stdout.trim()).toBe('* main'); // Create feature branch with a new file await run(kernel, git('-C /tmp/origin checkout -b feature')); await kernel.writeFile('/tmp/origin/feature.txt', 'checked out from feature\n'); await run(kernel, git('-C /tmp/origin add feature.txt')); await run(kernel, git("-C /tmp/origin commit -m 'add feature file'")); // Switch back to main await run(kernel, git('-C /tmp/origin checkout main')); // Clone await run(kernel, git('clone /tmp/origin /tmp/clone')); // Clone should only show main branch initially r = await run(kernel, git('-C /tmp/clone branch')); expect(r.stdout.trim()).toBe('* main'); // Checkout feature (DWIM from remote tracking) await run(kernel, git('-C /tmp/clone checkout feature')); // Now both branches should be listed r = await run(kernel, git('-C /tmp/clone branch')); const branches = r.stdout.trim().split('\n').map((l: string) => l.trim()); expect(branches).toContain('* feature'); expect(branches).toContain('main'); // Verify feature file exists in clone const featureContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/feature.txt')); expect(featureContent).toBe('checked out from feature\n'); // Verify README exists too const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); expect(readmeContent).toBe('# demo repo\n'); }); it('clone without an explicit destination uses the source basename', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'default destination\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'seed'")); await run(kernel, 'mkdir -p /work'); await run(kernel, git('-C /work clone /tmp/origin')); expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); expect(readmeContent).toBe('default destination\n'); }); it('clone without an explicit destination strips a trailing .git suffix', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin.git')); await kernel.writeFile('/tmp/origin.git/README.md', 'suffix destination\n'); await run(kernel, git('-C /tmp/origin.git add README.md')); await run(kernel, git("-C /tmp/origin.git commit -m 'seed'")); await run(kernel, 'mkdir -p /work'); await run(kernel, git('-C /work clone /tmp/origin.git')); expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); expect(readmeContent).toBe('suffix destination\n'); }); it('clone into an existing empty destination directory succeeds', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'empty destination\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'seed'")); await run(kernel, 'mkdir -p /tmp/clone'); await run(kernel, git('clone /tmp/origin /tmp/clone')); expect(await vfs.exists('/tmp/clone/.git/HEAD')).toBe(true); const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); expect(readmeContent).toBe('empty destination\n'); }); it('clone rejects a non-empty destination directory', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'origin\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'seed'")); await run(kernel, 'mkdir -p /tmp/clone'); await kernel.writeFile('/tmp/clone/existing.txt', 'keep me\n'); const result = await kernel.exec(git('clone /tmp/origin /tmp/clone')); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/already exists|not an empty directory|destination/i); const existing = new TextDecoder().decode(await vfs.readFile('/tmp/clone/existing.txt')); expect(existing).toBe('keep me\n'); expect(await vfs.exists('/tmp/clone/.git')).toBe(false); }); it('clone of a missing repository fails without leaving a partial destination', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); const result = await kernel.exec(git('clone /tmp/missing /tmp/clone')); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/not a git repository|missing|no such file|fatal/i); expect(await vfs.exists('/tmp/clone')).toBe(false); }); it('clone of an empty repository succeeds and leaves an empty worktree', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await run(kernel, git('clone /tmp/origin /tmp/clone')); const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); expect(head.trim()).toBe('ref: refs/heads/main'); expect(await vfs.exists('/tmp/clone/.git/config')).toBe(true); expect(await vfs.exists('/tmp/clone/.git/refs/heads/main')).toBe(false); expect(await vfs.exists('/tmp/clone/README.md')).toBe(false); }); it('clone preserves nested directory trees', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await run(kernel, 'mkdir -p /tmp/origin/src/nested'); await kernel.writeFile('/tmp/origin/src/nested/file.txt', 'nested payload\n'); await kernel.writeFile('/tmp/origin/src/root.txt', 'root payload\n'); await run(kernel, git('-C /tmp/origin add src/nested/file.txt src/root.txt')); await run(kernel, git("-C /tmp/origin commit -m 'nested tree'")); await run(kernel, git('clone /tmp/origin /tmp/clone')); const nested = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/nested/file.txt')); const root = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/root.txt')); expect(nested).toBe('nested payload\n'); expect(root).toBe('root payload\n'); }); it('clone honors the source default branch when HEAD is not main', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'main branch\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'main'")); await run(kernel, git('-C /tmp/origin checkout -b trunk')); await kernel.writeFile('/tmp/origin/trunk.txt', 'trunk branch\n'); await run(kernel, git('-C /tmp/origin add trunk.txt')); await run(kernel, git("-C /tmp/origin commit -m 'trunk'")); await run(kernel, git('clone /tmp/origin /tmp/clone')); const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); expect(head.trim()).toBe('ref: refs/heads/trunk'); expect(await vfs.exists('/tmp/clone/.git/refs/heads/trunk')).toBe(true); const trunk = new TextDecoder().decode(await vfs.readFile('/tmp/clone/trunk.txt')); expect(trunk).toBe('trunk branch\n'); }); it('clone copies nested branch refs and checkout DWIM works for branch names with slashes', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'initial commit'")); await run(kernel, git('-C /tmp/origin checkout -b feature/deep')); await kernel.writeFile('/tmp/origin/feature.txt', 'nested branch payload\n'); await run(kernel, git('-C /tmp/origin add feature.txt')); await run(kernel, git("-C /tmp/origin commit -m 'nested branch'")); await run(kernel, git('-C /tmp/origin checkout main')); Lines 1–500 of 973. Load more
// Nightly: requires a non-core registry command. /** * Integration tests for git command. * * Verifies init, add, commit, branch, checkout (with DWIM), plus local and * smart-HTTP remote clone via kernel.exec() with real WASM binaries. */ import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from 'vitest'; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { tmpdir } from 'node:os'; import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; import { spawn, spawnSync, execSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { createWasmVmRuntime } from '@rivet-dev/agentos-test-harness'; import { allowAll, COMMANDS_DIR, createInMemoryFileSystem, createKernel, describeIf, hasWasmBinaries, } from '@rivet-dev/agentos-test-harness'; import type { Kernel } from '@rivet-dev/agentos-test-harness'; vi.setConfig({ testTimeout: 30_000 }); /** Check git binary exists in addition to base WASM binaries */ const hasGit = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'git')); const hasHostGit = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0; // Smart HTTP needs Git's libcurl-backed remote helper. It is now a real second // WASM binary (git-remote-http links the overlaid mbedTLS libcurl in-process); // git-remote-https aliases to it. const hasGitHttpHelper = hasGit && existsSync(resolve(COMMANDS_DIR, 'git-remote-http')); // The real OpenSSH client (software/ssh) lights up git-over-ssh; its presence // changes how ssh:// remotes fail when unreachable. const hasSshClient = existsSync(resolve(COMMANDS_DIR, 'ssh')); const gitConfig = [ '-c safe.directory=*', '-c init.defaultBranch=main', '-c user.name=agentos', '-c user.email=agentos@example.invalid', ].join(' '); function git(args: string) { return `git ${gitConfig} ${args}`; } /** Create a kernel with a world-writable in-memory filesystem */ async function createGitKernel() { const vfs = createInMemoryFileSystem(); // Make root and /tmp writable by all users (WASM processes run as non-root) await (vfs as any).chmod('/', 0o1777); await vfs.mkdir('/tmp', { recursive: true }); await (vfs as any).chmod('/tmp', 0o1777); const kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); return { kernel, vfs, dispose: () => kernel.dispose() }; } async function createGitKernelWithNet(loopbackExemptPorts: number[], seededCaPem?: string) { const vfs = createInMemoryFileSystem(); await (vfs as any).chmod('/', 0o1777); await vfs.mkdir('/tmp', { recursive: true }); await (vfs as any).chmod('/tmp', 0o1777); const kernel = createKernel({ filesystem: vfs, permissions: allowAll, loopbackExemptPorts, syncFilesystemOnDispose: false, }); await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); // Seed the Debian-shaped trust store the way the native VM bootstrap does, so // libcurl's compile-time default CA bundle (/etc/ssl/certs/ca-certificates.crt) // resolves in-guest for git-remote-http's mbedTLS backend. if (seededCaPem) { await vfs.mkdir('/etc/ssl/certs', { recursive: true }); await kernel.writeFile('/etc/ssl/certs/ca-certificates.crt', seededCaPem); } return { kernel, vfs, dispose: () => kernel.dispose() }; } // Build a real CA and a leaf server certificate signed by it, with a SAN that // covers the 127.0.0.1 loopback endpoint the VM connects to. This lets // git-remote-http's mbedTLS backend perform genuine chain + hostname // verification, exactly like Linux git against a private CA. function makeCaSignedCert(caCommonName: string): { caPem: string; serverKey: string; serverCert: string; } { const dir = mkdtempSync(join(tmpdir(), 'git-ca-')); try { execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/ca.key" 2>/dev/null`); execSync( `openssl req -x509 -new -key "${dir}/ca.key" -days 3650 -subj "/CN=${caCommonName}" -out "${dir}/ca.crt" 2>/dev/null`, ); execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/srv.key" 2>/dev/null`); execSync(`openssl req -new -key "${dir}/srv.key" -subj "/CN=localhost" -out "${dir}/srv.csr" 2>/dev/null`); writeFileSync(`${dir}/ext.cnf`, 'subjectAltName=DNS:localhost,IP:127.0.0.1\n'); execSync( `openssl x509 -req -in "${dir}/srv.csr" -CA "${dir}/ca.crt" -CAkey "${dir}/ca.key" ` + `-CAcreateserial -days 3650 -extfile "${dir}/ext.cnf" -out "${dir}/srv.crt" 2>/dev/null`, ); return { caPem: readFileSync(`${dir}/ca.crt`, 'utf8'), serverKey: readFileSync(`${dir}/srv.key`, 'utf8'), serverCert: readFileSync(`${dir}/srv.crt`, 'utf8'), }; } finally { rmSync(dir, { recursive: true, force: true }); } } function runHostGit(args: string[], cwd?: string) { const result = spawnSync('git', args, { cwd, encoding: 'utf8', }); if (result.status !== 0) { throw new Error( `host git failed: git ${args.join(' ')}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, ); } } async function runHostGitResult(args: string[], cwd?: string) { return await new Promise<{ status: number | null; stdout: string; stderr: string }>((resolveResult, reject) => { const child = spawn('git', args, { cwd }); const stdout: Buffer[] = []; const stderr: Buffer[] = []; child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk))); child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk))); child.on('error', reject); child.on('close', (status) => { resolveResult({ status, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), }); }); }); } /** Helper: run command and assert success */ async function run(kernel: Kernel, cmd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { const r = await kernel.exec(cmd); if (r.exitCode !== 0) { throw new Error(`Command failed (exit ${r.exitCode}): ${cmd}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`); } return r; } async function expectGitRef(kernel: Kernel, repo: string, ref: string) { const result = await run(kernel, git(`-C ${repo} rev-parse --verify ${ref}`)); expect(result.stdout.trim()).toMatch(/^[0-9a-f]{40,64}$/); } function sidebandPacket(band: 1 | 2 | 3, payload: Uint8Array): Uint8Array { const packetLength = payload.length + 5; const header = new TextEncoder().encode(packetLength.toString(16).padStart(4, '0')); const packet = new Uint8Array(packetLength); packet.set(header, 0); packet[4] = band; packet.set(payload, 5); return packet; } function concatBytes(chunks: Uint8Array[]): Uint8Array { const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length; } return output; } // TODO(P6): requires git WASM artifact, intentionally excluded from the fast software-build gate. describeIf(hasGit, 'git command', () => { let kernel: Kernel; let vfs: any; let dispose: () => Promise<void>; afterEach(async () => { await dispose?.(); }); it('init creates .git directory structure', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); const result = await run(kernel, git('init /repo')); expect(result.stdout).toContain('Initialized empty Git repository'); expect(await vfs.exists('/repo/.git/HEAD')).toBe(true); expect(await vfs.exists('/repo/.git/objects')).toBe(true); expect(await vfs.exists('/repo/.git/refs/heads')).toBe(true); const head = new TextDecoder().decode(await vfs.readFile('/repo/.git/HEAD')); expect(head.trim()).toBe('ref: refs/heads/main'); }); it('add + commit creates objects and updates ref', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /repo')); await kernel.writeFile('/repo/hello.txt', 'hello world\n'); await run(kernel, git('-C /repo add hello.txt')); await run(kernel, git("-C /repo commit -m 'first commit'")); expect(await vfs.exists('/repo/.git/refs/heads/main')).toBe(true); }); it('hidden sideband helper repeatedly streams band 1 and forwards progress without spooling', async () => { ({ kernel, dispose } = await createGitKernel()); const encoder = new TextEncoder(); const payloadChunks = Array.from({ length: 128 }, (_, index) => encoder.encode(`pack-${index.toString().padStart(3, '0')}:${'x'.repeat(4096)}\n`)); const stream: Uint8Array[] = []; for (const [index, payload] of payloadChunks.entries()) { stream.push(sidebandPacket(1, payload)); if (index % 16 === 0) stream.push(sidebandPacket(2, encoder.encode(`progress ${index}\n`))); } stream.push(encoder.encode('0000')); const result = await kernel.exec('git sideband--helper demux parity-test 1', { stdin: concatBytes(stream), }); expect(result.exitCode, result.stderr).toBe(0); expect(result.stdout).toBe(new TextDecoder().decode(concatBytes(payloadChunks))); expect(result.stderr).toContain('progress 0'); expect(result.stderr).toContain('progress 112'); const coloredProgress = concatBytes([ sidebandPacket(2, encoder.encode('\x1b[31mred\x1b[0m\n')), encoder.encode('0000'), ]); const sanitized = await kernel.exec('git sideband--helper demux parity-test 0', { stdin: coloredProgress, }); expect(sanitized.exitCode, sanitized.stderr).toBe(0); expect(sanitized.stderr).toContain('^[[31mred^[[0m'); const ansiAllowed = await kernel.exec('git sideband--helper demux parity-test 1', { stdin: coloredProgress, }); expect(ansiAllowed.exitCode, ansiAllowed.stderr).toBe(0); expect(ansiAllowed.stderr).toContain('\x1b[31mred\x1b[0m'); const invalidPolicy = await kernel.exec('git sideband--helper demux parity-test 16', { stdin: coloredProgress, }); expect(invalidPolicy.exitCode).not.toBe(0); expect(invalidPolicy.stderr).toContain('invalid sideband control-character policy'); const muxed = await kernel.exec('git sideband--helper mux 65520 0 5', { stdin: 'receive progress\n', }); expect(muxed.exitCode, muxed.stderr).toBe(0); expect(muxed.stdout).toMatch(/^[0-9a-f]{4}\u0002receive progress\n$/); const help = await kernel.exec('git help -a'); expect(help.stdout).not.toContain('sideband--helper'); const patchText = readFileSync( resolve(import.meta.dirname, '../../../toolchain/c/patches/git/0002-wasi-synchronous-sideband-demux.patch'), 'utf8', ); const addedImplementation = patchText .split('\n') .filter((line) => line.startsWith('+') && !line.startsWith('+++')) .join('\n'); expect(addedImplementation).not.toContain('tmp_sideband_'); expect(addedImplementation).not.toMatch(/odb_mkstemp|spool_fd|spool_tempfile/); }); it('branch lists branches with current marked', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /repo')); await kernel.writeFile('/repo/file.txt', 'content\n'); await run(kernel, git('-C /repo add file.txt')); await run(kernel, git("-C /repo commit -m 'init'")); const result = await run(kernel, git('-C /repo branch')); expect(result.stdout.trim()).toBe('* main'); }); it('checkout -b creates a new branch', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /repo')); await kernel.writeFile('/repo/file.txt', 'content\n'); await run(kernel, git('-C /repo add file.txt')); await run(kernel, git("-C /repo commit -m 'init'")); await run(kernel, git('-C /repo checkout -b feature')); const result = await run(kernel, git('-C /repo branch')); const lines = result.stdout.trim().split('\n').map((l: string) => l.trim()); expect(lines).toContain('* feature'); expect(lines).toContain('main'); }); it('full quickstart scenario: init, commit, branch, clone, checkout', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); // Create origin repo await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'initial commit'")); // Check default branch let r = await run(kernel, git('-C /tmp/origin branch')); expect(r.stdout.trim()).toBe('* main'); // Create feature branch with a new file await run(kernel, git('-C /tmp/origin checkout -b feature')); await kernel.writeFile('/tmp/origin/feature.txt', 'checked out from feature\n'); await run(kernel, git('-C /tmp/origin add feature.txt')); await run(kernel, git("-C /tmp/origin commit -m 'add feature file'")); // Switch back to main await run(kernel, git('-C /tmp/origin checkout main')); // Clone await run(kernel, git('clone /tmp/origin /tmp/clone')); // Clone should only show main branch initially r = await run(kernel, git('-C /tmp/clone branch')); expect(r.stdout.trim()).toBe('* main'); // Checkout feature (DWIM from remote tracking) await run(kernel, git('-C /tmp/clone checkout feature')); // Now both branches should be listed r = await run(kernel, git('-C /tmp/clone branch')); const branches = r.stdout.trim().split('\n').map((l: string) => l.trim()); expect(branches).toContain('* feature'); expect(branches).toContain('main'); // Verify feature file exists in clone const featureContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/feature.txt')); expect(featureContent).toBe('checked out from feature\n'); // Verify README exists too const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); expect(readmeContent).toBe('# demo repo\n'); }); it('clone without an explicit destination uses the source basename', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'default destination\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'seed'")); await run(kernel, 'mkdir -p /work'); await run(kernel, git('-C /work clone /tmp/origin')); expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); expect(readmeContent).toBe('default destination\n'); }); it('clone without an explicit destination strips a trailing .git suffix', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin.git')); await kernel.writeFile('/tmp/origin.git/README.md', 'suffix destination\n'); await run(kernel, git('-C /tmp/origin.git add README.md')); await run(kernel, git("-C /tmp/origin.git commit -m 'seed'")); await run(kernel, 'mkdir -p /work'); await run(kernel, git('-C /work clone /tmp/origin.git')); expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); expect(readmeContent).toBe('suffix destination\n'); }); it('clone into an existing empty destination directory succeeds', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'empty destination\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'seed'")); await run(kernel, 'mkdir -p /tmp/clone'); await run(kernel, git('clone /tmp/origin /tmp/clone')); expect(await vfs.exists('/tmp/clone/.git/HEAD')).toBe(true); const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); expect(readmeContent).toBe('empty destination\n'); }); it('clone rejects a non-empty destination directory', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'origin\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'seed'")); await run(kernel, 'mkdir -p /tmp/clone'); await kernel.writeFile('/tmp/clone/existing.txt', 'keep me\n'); const result = await kernel.exec(git('clone /tmp/origin /tmp/clone')); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/already exists|not an empty directory|destination/i); const existing = new TextDecoder().decode(await vfs.readFile('/tmp/clone/existing.txt')); expect(existing).toBe('keep me\n'); expect(await vfs.exists('/tmp/clone/.git')).toBe(false); }); it('clone of a missing repository fails without leaving a partial destination', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); const result = await kernel.exec(git('clone /tmp/missing /tmp/clone')); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/not a git repository|missing|no such file|fatal/i); expect(await vfs.exists('/tmp/clone')).toBe(false); }); it('clone of an empty repository succeeds and leaves an empty worktree', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await run(kernel, git('clone /tmp/origin /tmp/clone')); const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); expect(head.trim()).toBe('ref: refs/heads/main'); expect(await vfs.exists('/tmp/clone/.git/config')).toBe(true); expect(await vfs.exists('/tmp/clone/.git/refs/heads/main')).toBe(false); expect(await vfs.exists('/tmp/clone/README.md')).toBe(false); }); it('clone preserves nested directory trees', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await run(kernel, 'mkdir -p /tmp/origin/src/nested'); await kernel.writeFile('/tmp/origin/src/nested/file.txt', 'nested payload\n'); await kernel.writeFile('/tmp/origin/src/root.txt', 'root payload\n'); await run(kernel, git('-C /tmp/origin add src/nested/file.txt src/root.txt')); await run(kernel, git("-C /tmp/origin commit -m 'nested tree'")); await run(kernel, git('clone /tmp/origin /tmp/clone')); const nested = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/nested/file.txt')); const root = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/root.txt')); expect(nested).toBe('nested payload\n'); expect(root).toBe('root payload\n'); }); it('clone honors the source default branch when HEAD is not main', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', 'main branch\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'main'")); await run(kernel, git('-C /tmp/origin checkout -b trunk')); await kernel.writeFile('/tmp/origin/trunk.txt', 'trunk branch\n'); await run(kernel, git('-C /tmp/origin add trunk.txt')); await run(kernel, git("-C /tmp/origin commit -m 'trunk'")); await run(kernel, git('clone /tmp/origin /tmp/clone')); const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); expect(head.trim()).toBe('ref: refs/heads/trunk'); expect(await vfs.exists('/tmp/clone/.git/refs/heads/trunk')).toBe(true); const trunk = new TextDecoder().decode(await vfs.readFile('/tmp/clone/trunk.txt')); expect(trunk).toBe('trunk branch\n'); }); it('clone copies nested branch refs and checkout DWIM works for branch names with slashes', async () => { ({ kernel, vfs, dispose } = await createGitKernel()); await run(kernel, git('init /tmp/origin')); await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); await run(kernel, git('-C /tmp/origin add README.md')); await run(kernel, git("-C /tmp/origin commit -m 'initial commit'")); await run(kernel, git('-C /tmp/origin checkout -b feature/deep')); await kernel.writeFile('/tmp/origin/feature.txt', 'nested branch payload\n'); await run(kernel, git('-C /tmp/origin add feature.txt')); await run(kernel, git("-C /tmp/origin commit -m 'nested branch'")); await run(kernel, git('-C /tmp/origin checkout main'));