Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for: - projects-meta-mcp v2.25.0 (TypeScript/Node) - wiki-graph v0.3.1 (TypeScript/Node) - interns-mcp v0.3.3 (Python/FastMCP) .gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc) bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
188 lines
5.7 KiB
TypeScript
188 lines
5.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { mkdtemp, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import {
|
|
detectDomain,
|
|
detectProjectIdentity,
|
|
parseRemoteUrl,
|
|
resolveSourceProject,
|
|
} from '../../src/lib/domain-detector.js';
|
|
|
|
async function tmp(): Promise<string> {
|
|
return mkdtemp(join(tmpdir(), 'dd-'));
|
|
}
|
|
|
|
describe('detectDomain', () => {
|
|
it('returns node when package.json present', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'package.json'), '{}');
|
|
expect(await detectDomain(d)).toBe('node');
|
|
});
|
|
|
|
it('returns embedded for platformio.ini', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'platformio.ini'), '[env]');
|
|
expect(await detectDomain(d)).toBe('embedded');
|
|
});
|
|
|
|
it('returns embedded for .ino', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'sketch.ino'), 'void setup() {}');
|
|
expect(await detectDomain(d)).toBe('embedded');
|
|
});
|
|
|
|
it('returns embedded for ARM CMakeLists', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'CMakeLists.txt'), 'set(CMAKE_C_COMPILER arm-none-eabi-gcc)');
|
|
expect(await detectDomain(d)).toBe('embedded');
|
|
});
|
|
|
|
it('returns web for vite config', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'vite.config.ts'), 'export default {}');
|
|
expect(await detectDomain(d)).toBe('web');
|
|
});
|
|
|
|
it('returns web for static index.html without package.json', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'index.html'), '<html></html>');
|
|
expect(await detectDomain(d)).toBe('web');
|
|
});
|
|
|
|
it('returns unknown for empty dir', async () => {
|
|
const d = await tmp();
|
|
expect(await detectDomain(d)).toBe('unknown');
|
|
});
|
|
|
|
it('package.json wins over index.html', async () => {
|
|
const d = await tmp();
|
|
await writeFile(join(d, 'package.json'), '{}');
|
|
await writeFile(join(d, 'index.html'), '<html></html>');
|
|
expect(await detectDomain(d)).toBe('node');
|
|
});
|
|
});
|
|
|
|
describe('parseRemoteUrl', () => {
|
|
it('parses https Gitea URL with .git suffix', () => {
|
|
expect(parseRemoteUrl('https://git.kzntsv.site/OpeItcLoc03/common.git')).toEqual({
|
|
owner: 'OpeItcLoc03',
|
|
repo: 'common',
|
|
});
|
|
});
|
|
|
|
it('parses https Gitea URL without .git suffix', () => {
|
|
expect(parseRemoteUrl('https://git.kzntsv.site/victor/books')).toEqual({
|
|
owner: 'victor',
|
|
repo: 'books',
|
|
});
|
|
});
|
|
|
|
it('parses https URL with port', () => {
|
|
expect(parseRemoteUrl('https://git.kzntsv.site:443/victor/books.git')).toEqual({
|
|
owner: 'victor',
|
|
repo: 'books',
|
|
});
|
|
});
|
|
|
|
it('parses ssh URL', () => {
|
|
expect(parseRemoteUrl('git@git.kzntsv.site:victor/books.git')).toEqual({
|
|
owner: 'victor',
|
|
repo: 'books',
|
|
});
|
|
});
|
|
|
|
it('parses ssh URL without .git suffix', () => {
|
|
expect(parseRemoteUrl('git@github.com:owner/repo')).toEqual({
|
|
owner: 'owner',
|
|
repo: 'repo',
|
|
});
|
|
});
|
|
|
|
it('strips trailing slash before parsing', () => {
|
|
expect(parseRemoteUrl('https://git.kzntsv.site/victor/books/')).toEqual({
|
|
owner: 'victor',
|
|
repo: 'books',
|
|
});
|
|
});
|
|
|
|
it('returns null for empty string', () => {
|
|
expect(parseRemoteUrl('')).toBeNull();
|
|
});
|
|
|
|
it('returns null for non-URL local path', () => {
|
|
expect(parseRemoteUrl('/path/to/local/repo')).toBeNull();
|
|
});
|
|
|
|
it('returns null for URL without owner/repo path', () => {
|
|
expect(parseRemoteUrl('https://example.com/single')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('detectProjectIdentity', () => {
|
|
it('returns parsed identity when git remote returns Gitea https URL', async () => {
|
|
const fakeExec = async () => ({
|
|
stdout: 'https://git.kzntsv.site/OpeItcLoc03/common.git\n',
|
|
stderr: '',
|
|
});
|
|
expect(await detectProjectIdentity('/cwd', fakeExec)).toEqual({
|
|
owner: 'OpeItcLoc03',
|
|
repo: 'common',
|
|
});
|
|
});
|
|
|
|
it('returns parsed identity when git remote returns ssh URL', async () => {
|
|
const fakeExec = async () => ({
|
|
stdout: 'git@git.kzntsv.site:victor/books.git\n',
|
|
stderr: '',
|
|
});
|
|
expect(await detectProjectIdentity('/cwd', fakeExec)).toEqual({
|
|
owner: 'victor',
|
|
repo: 'books',
|
|
});
|
|
});
|
|
|
|
it('returns null when git command fails (no remote / not a repo)', async () => {
|
|
const fakeExec = async () => {
|
|
throw new Error('fatal: not a git repository');
|
|
};
|
|
expect(await detectProjectIdentity('/cwd', fakeExec)).toBeNull();
|
|
});
|
|
|
|
it('returns null when remote URL is unparseable', async () => {
|
|
const fakeExec = async () => ({ stdout: 'garbage-not-a-url\n', stderr: '' });
|
|
expect(await detectProjectIdentity('/cwd', fakeExec)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('resolveSourceProject', () => {
|
|
it('returns qualified <owner>/<repo> with fellBack=false when remote resolves', async () => {
|
|
const fakeExec = async () => ({
|
|
stdout: 'https://git.kzntsv.site/victor/books.git\n',
|
|
stderr: '',
|
|
});
|
|
expect(await resolveSourceProject('/some/cwd/path/books', fakeExec)).toEqual({
|
|
sourceProject: 'victor/books',
|
|
fellBack: false,
|
|
});
|
|
});
|
|
|
|
it('falls back to basename(cwd) with fellBack=true when remote command fails', async () => {
|
|
const fakeExec = async () => {
|
|
throw new Error('fatal: not a git repository');
|
|
};
|
|
expect(await resolveSourceProject('/home/user/projects/.workshop', fakeExec)).toEqual({
|
|
sourceProject: '.workshop',
|
|
fellBack: true,
|
|
});
|
|
});
|
|
|
|
it('falls back to basename(cwd) with fellBack=true when remote URL is unparseable', async () => {
|
|
const fakeExec = async () => ({ stdout: 'garbage-not-a-url\n', stderr: '' });
|
|
expect(await resolveSourceProject('/tmp/loose-folder', fakeExec)).toEqual({
|
|
sourceProject: 'loose-folder',
|
|
fellBack: true,
|
|
});
|
|
});
|
|
});
|