import test from 'node:test'; import assert from 'node:assert/strict'; import os from 'node:os'; import path from 'node:path'; import fs from 'node:fs/promises'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import cloneTargetModule from '../src/shared/clone-target.cjs'; import gitModule from '../src/main/git-service.cjs'; const exec = promisify(execFile); const { cloneDirectoryName, resolveCloneTarget } = cloneTargetModule; const { GitService } = gitModule; test('derives a safe repository folder name from HTTPS and SSH clone URLs', () => { assert.equal(cloneDirectoryName('https://gitea.example.test/jens/ForgeFlow.git'), 'ForgeFlow'); assert.equal(cloneDirectoryName('git@gitea.example.test:jens/my-app.git'), 'my-app'); assert.equal(cloneDirectoryName('ssh://git@gitea.example.test/jens/app.git?ref=main'), 'app'); }); test('resolves the automatic clone target inside the configured project root', () => { const root = path.join(os.tmpdir(), 'forgeflow-projects'); const plan = resolveCloneTarget(root, 'https://gitea.example.test/jens/portfolio.git'); assert.equal(plan.root, path.resolve(root)); assert.equal(plan.target, path.join(path.resolve(root), 'portfolio')); assert.equal(plan.directoryName, 'portfolio'); }); test('clone target inspection accepts missing and empty destinations', async (t) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-target-')); t.after(() => fs.rm(root, { recursive: true, force: true })); const service = new GitService(); const remote = 'https://gitea.example.test/jens/app.git'; const missing = await service.inspectCloneTarget(remote, path.join(root, 'missing-app')); assert.equal(missing.state, 'missing'); const emptyPath = path.join(root, 'empty-app'); await fs.mkdir(emptyPath); const empty = await service.inspectCloneTarget(remote, emptyPath); assert.equal(empty.state, 'empty'); }); test('clone target inspection reuses an existing checkout with the same origin', async (t) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-reuse-')); t.after(() => fs.rm(root, { recursive: true, force: true })); const target = path.join(root, 'app'); await fs.mkdir(target); await exec('git', ['init'], { cwd: target, encoding: 'utf8' }); await exec('git', ['remote', 'add', 'origin', 'git@gitea.example.test:jens/app.git'], { cwd: target, encoding: 'utf8' }); const service = new GitService(); const assessment = await service.inspectCloneTarget('https://gitea.example.test/jens/app.git', target); assert.equal(assessment.state, 'matching-repository'); }); test('clone target inspection blocks a different repository and ordinary files', async (t) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-conflict-')); t.after(() => fs.rm(root, { recursive: true, force: true })); const service = new GitService(); const otherRepository = path.join(root, 'repository'); await fs.mkdir(otherRepository); await exec('git', ['init'], { cwd: otherRepository, encoding: 'utf8' }); await exec('git', ['remote', 'add', 'origin', 'https://gitea.example.test/jens/other.git'], { cwd: otherRepository, encoding: 'utf8' }); await assert.rejects( service.inspectCloneTarget('https://gitea.example.test/jens/app.git', otherRepository), (error) => error.code === 'CLONE_TARGET_DIFFERENT_REPOSITORY' ); const ordinaryFolder = path.join(root, 'ordinary'); await fs.mkdir(ordinaryFolder); await fs.writeFile(path.join(ordinaryFolder, 'notes.txt'), 'do not overwrite\n'); await assert.rejects( service.inspectCloneTarget('https://gitea.example.test/jens/app.git', ordinaryFolder), (error) => error.code === 'CLONE_TARGET_NOT_EMPTY' ); }); test('clone target inspection blocks a file at the automatic destination', async (t) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-file-')); t.after(() => fs.rm(root, { recursive: true, force: true })); const target = path.join(root, 'app'); await fs.writeFile(target, 'not a directory'); const service = new GitService(); await assert.rejects( service.inspectCloneTarget('https://gitea.example.test/jens/app.git', target), (error) => error.code === 'CLONE_TARGET_NOT_DIRECTORY' ); });