Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
52a6c9020This file is pinned to the selected release. Download the original to open it in your design tool.
import { execFileSync } from 'node:child_process'
import { mkdtemp, mkdir, rm, realpath } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { agentProject, canonicalRepository } from './agentProject.js'
const roots: string[] = []
afterEach(async () => { await Promise.all(roots.splice(0).map(path => rm(path, { recursive: true, force: true }))) })
describe('owning-machine project metadata', () => {
it('canonicalizes transports and strips credentials and URL tokens', () => {
expect(canonicalRepository('git@github.com:Org/App.git')).toBe('github.com/org/app')
expect(canonicalRepository('https://user:secret@github.com/Org/App.git?token=secret')).toBe('github.com/org/app')
expect(canonicalRepository('ssh://git@github.com:22/Org/App.git')).toBe('github.com/org/app')
expect(canonicalRepository('ssh://git@example.com:2222/Org/App.git')).toBe('example.com:2222/Org/App')
expect(canonicalRepository('/private/checkouts/app')).toBeNull()
expect(canonicalRepository('file:///private/checkouts/app')).toBeNull()
expect(canonicalRepository('ssh://git@example.com/CaseSensitive.git')).toBe('example.com/CaseSensitive')
})
it('reads the current checkout and branch, including branch changes after cache expiry', async () => {
const root = await mkdtemp(join(tmpdir(), 'harness-v2-project-')); roots.push(root)
const git = (...args: string[]) => execFileSync('git', ['-C', root, ...args], { stdio: 'pipe' })
git('init', '-b', 'main')
git('remote', 'add', 'origin', 'git@github.com:Org/App.git')
await mkdir(join(root, 'nested'))
const cwd = join(root, 'nested')
expect(await agentProject(cwd, 100)).toMatchObject({ cwd, root: await realpath(root), remote: 'github.com/org/app', branch: 'main' })
git('symbolic-ref', 'HEAD', 'refs/heads/feature/real-branch')
expect(await agentProject(cwd, 20_000)).toMatchObject({ branch: 'feature/real-branch' })
})
it('names a linked worktree for its repository rather than its folder', async () => {
const root = await mkdtemp(join(tmpdir(), 'harness-v2-worktree-')); roots.push(root)
const repo = join(root, 'app')
const git = (...args: string[]) => execFileSync('git', ['-C', repo, ...args], { stdio: 'pipe' })
await mkdir(repo)
git('init', '-b', 'main')
git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '--allow-empty', '-m', 'initial')
const linked = join(root, 'worktrees', 'app', 'claude-0922-1136')
git('worktree', 'add', '-b', 'harness/claude-0922-1136', linked)
expect(await agentProject(linked)).toMatchObject({ name: 'app', root: await realpath(linked), branch: 'harness/claude-0922-1136', worktree: true })
expect(await agentProject(repo)).toMatchObject({ name: 'app', branch: 'main' })
expect(await agentProject(repo)).not.toHaveProperty('worktree')
expect(await agentProject(linked)).not.toHaveProperty('branchPending')
git('config', 'branch.harness/claude-0922-1136.harness', 'placeholder')
expect(await agentProject(linked, Date.now() + 20_000)).toMatchObject({ branchPending: true })
git('checkout', '--quiet', '--detach')
expect((await agentProject(repo, Date.now() + 20_000))?.branch).toMatch(/^Detached [0-9a-f]{7,}$/)
})
it('keeps a non-repository project branchless and rejects absent cwd', async () => {
const root = await mkdtemp(join(tmpdir(), 'harness-v2-folder-')); roots.push(root)
expect(await agentProject(root)).toMatchObject({ cwd: root, root: null, remote: null, branch: null })
expect(await agentProject(null)).toBeNull()
expect(await agentProject('relative')).toBeNull()
expect(await agentProject('/tmp/unsafe\n')).toBeNull()
})
})