import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, mkdir, symlink } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import repositoryModule from '../src/main/repository-service.cjs'; const { RepositoryService } = repositoryModule; function status(head = 'a'.repeat(40)) { return { head, shortHead: head.slice(0, 7), clean: true, counts: { changed: 0, conflicts: 0 }, branch: { head: 'main', upstream: 'origin/main', ahead: 0, behind: 0 } }; } const remote = { id: 1, name: 'Portfolio', full_name: 'Jens/Portfolio', owner: { login: 'Jens' }, private: true, default_branch: 'main', html_url: 'https://gitea.example/Jens/Portfolio', clone_url: 'https://gitea.example/Jens/Portfolio.git', ssh_url: 'git@gitea.example:Jens/Portfolio.git' }; function service() { return new RepositoryService({ data: { preferences: { preferredCloneProtocol: 'ssh' }, favorites: [] } }, {}, {}); } test('a synchronized commit is deployable when the server is unknown or older', () => { const current = status(); const unknown = service().decorate(remote, { localPath: 'C:/Projects/Portfolio', status: current }, [ { id: 'prod', branch: 'main', state: { liveSha: null, healthy: null } } ]); assert.equal(unknown.readyToDeploy, true); const older = service().decorate(remote, { localPath: 'C:/Projects/Portfolio', status: current }, [ { id: 'prod', branch: 'main', state: { liveSha: 'b'.repeat(40), healthy: true } } ]); assert.equal(older.readyToDeploy, true); }); test('a healthy commit already live on the server is not offered for deployment again', () => { const current = status(); const repository = service().decorate(remote, { localPath: 'C:/Projects/Portfolio', status: current }, [ { id: 'prod', branch: 'main', state: { liveSha: current.head, healthy: true } } ]); assert.equal(repository.readyToDeploy, false); }); test('an unhealthy live commit remains eligible for a controlled redeploy', () => { const current = status(); const repository = service().decorate(remote, { localPath: 'C:/Projects/Portfolio', status: current }, [ { id: 'prod', branch: 'main', state: { liveSha: current.head, healthy: false } } ]); assert.equal(repository.readyToDeploy, true); }); test('repository discovery is bounded, skips generated trees and ignores inaccessible roots', async (context) => { const root = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-repositories-')); context.after(() => import('node:fs/promises').then(({ rm }) => rm(root, { recursive: true, force: true }))); await mkdir(path.join(root, 'group', 'app', '.git'), { recursive: true }); await mkdir(path.join(root, 'node_modules', 'ignored', '.git'), { recursive: true }); await mkdir(path.join(root, 'too', 'deep', 'repository', '.git'), { recursive: true }); try { await symlink(path.join(root, 'group'), path.join(root, 'linked'), 'junction'); } catch {} const instance = service(); const found = await instance.discoverInRoot(root, 2); assert.deepEqual(found, [await import('node:fs/promises').then(({ realpath }) => realpath(path.join(root, 'group', 'app')))]); assert.deepEqual(await instance.discoverInRoot(path.join(root, 'missing')), []); const all = await instance.discoverAll([root, root, '', null]); assert.equal(all.length, 2); assert.equal(new Set(all).size, 2); assert.ok(all.includes(found[0])); }); test('local descriptors preserve Git failures and watch paths are defensive copies', async () => { const instance = new RepositoryService({ data: {} }, { status: async (localPath) => { if (localPath.endsWith('bad')) throw new Error('not a repository'); return { ...status(), root: `${localPath}/canonical`, remoteUrl: remote.clone_url }; } }, {}); const descriptors = await instance.getLocalDescriptors(['good', 'bad']); assert.equal(descriptors[0].localPath, 'good/canonical'); assert.equal(descriptors[1].error, 'not a repository'); instance.lastKnownLocalPaths = ['one']; const watched = instance.getWatchPaths(); watched.push('two'); assert.deepEqual(instance.getWatchPaths(), ['one']); }); test('refresh links explicit and remote-matched repositories and retains unmatched locals', async () => { const diagnostics = []; const store = { data: { gitea: { baseUrl: 'https://gitea.example' }, workspaceRoots: ['root'], repositoryMappings: { 'jens/portfolio': 'C:/explicit' }, preferences: { preferredCloneProtocol: 'https' }, favorites: ['jens/portfolio'] }, getToken: () => 'token', getDeploymentProfiles: (name) => name === remote.full_name ? [{ id: 'prod', branch: 'main' }] : [], getDeploymentState: () => ({ liveSha: null, healthy: null }) }; const secondRemote = { ...remote, id: 2, name: 'Other', full_name: 'Jens/Other', clone_url: 'https://gitea.example/Jens/Other.git' }; const instance = new RepositoryService(store, { status: async (localPath) => ({ ...status(localPath.includes('unmatched') ? 'b'.repeat(40) : 'a'.repeat(40)), root: localPath, remoteUrl: localPath.includes('matched') ? secondRemote.clone_url : remote.clone_url }) }, { listRepositories: async () => [remote, secondRemote] }, { debug: async (...args) => diagnostics.push(args) }); instance.discoverAll = async () => ['C:/matched', 'C:/unmatched']; const repositories = await instance.refresh(); const explicit = repositories.find((item) => item.fullName === remote.full_name); const matched = repositories.find((item) => item.fullName === secondRemote.full_name); const unmatched = repositories.find((item) => item.linkState === 'unmatched-local'); assert.equal(explicit.localPath, 'C:/explicit'); assert.equal(explicit.favorite, true); assert.equal(explicit.preferredCloneUrl, remote.clone_url); assert.equal(matched.localPath, 'C:/matched'); assert.equal(unmatched.localPath, 'C:/unmatched'); assert.deepEqual(instance.getWatchPaths().sort(), ['C:/explicit', 'C:/matched', 'C:/unmatched'].sort()); assert.equal(diagnostics[0][0], 'repositories.refresh.completed'); }); test('refresh remains local-only without configured Gitea credentials', async () => { const store = { data: { gitea: { baseUrl: '' }, workspaceRoots: [], repositoryMappings: {}, preferences: { preferredCloneProtocol: 'ssh' }, favorites: [] }, getToken: () => '', getDeploymentProfiles: () => [], getDeploymentState: () => null }; const instance = new RepositoryService(store, {}, { listRepositories: async () => { throw new Error('must not call'); } }); instance.discoverAll = async () => []; assert.deepEqual(await instance.refresh(), []); }); test('decoration reports conflicts, behind branches, errors and remote-only repositories', () => { const instance = service(); const conflicted = instance.decorate(remote, { localPath: 'repo', status: { ...status(), counts: { changed: 1, conflicts: 2 }, branch: { ...status().branch, behind: 3 } } }, []); assert.equal(conflicted.attentionReason, 'Merge conflict'); assert.equal(conflicted.readyToDeploy, false); const behind = instance.decorate(remote, { localPath: 'repo', status: { ...status(), branch: { ...status().branch, behind: 1 } } }, []); assert.equal(behind.attentionReason, '1 commit behind remote'); const broken = instance.decorate(remote, { localPath: 'repo', status: null, error: 'broken checkout' }, []); assert.equal(broken.attentionReason, 'broken checkout'); const remoteOnly = instance.decorate({ ...remote, ssh_url: '', clone_url: '' }, null, []); assert.equal(remoteOnly.linkState, 'remote-only'); assert.equal(remoteOnly.preferredCloneUrl, ''); });