Files
ForgeFlow/tests/repository-service.test.mjs
T

159 lines
7.5 KiB
JavaScript

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, '');
});