Files
factory/lib/projects-meta-mcp/tests/lib/resolve-target.test.ts
vitya ca669d96e1 feat(lib): bundle MCP servers from .common/lib into factory/lib/
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>
2026-06-11 13:17:29 +03:00

214 lines
6.9 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { mkdtemp } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { writeCache, type CacheFile } from '../../src/lib/cache.js';
import { parseTargetProject, resolveTarget } from '../../src/lib/resolve-target.js';
describe('parseTargetProject', () => {
it('accepts the agenda literal', () => {
const r = parseTargetProject('agenda');
expect(r).toEqual({ agenda: true });
});
it('accepts qualified <owner>/<repo>', () => {
const r = parseTargetProject('victor/books');
expect(r).toEqual({ agenda: false, owner: 'victor', repo: 'books' });
});
it('rejects bare repo name with hint', () => {
const r = parseTargetProject('books');
expect(r).toEqual({ error: expect.stringContaining('must be qualified') });
const err = (r as { error: string }).error;
expect(err).toContain('use `<owner>/<repo>`');
expect(err).toContain('victor/books');
expect(err).toContain('Got: `books`');
});
it('rejects the legacy _meta literal (replaced by `agenda` in 2.0)', () => {
const r = parseTargetProject('_meta');
expect(r).toEqual({ error: expect.stringContaining('must be qualified') });
expect((r as { error: string }).error).toContain('Got: `_meta`');
});
it('rejects empty string', () => {
const r = parseTargetProject('');
expect('error' in r).toBe(true);
});
it('rejects leading slash', () => {
const r = parseTargetProject('/books');
expect('error' in r).toBe(true);
});
it('rejects trailing slash', () => {
const r = parseTargetProject('victor/');
expect('error' in r).toBe(true);
});
it('rejects more than one slash', () => {
const r = parseTargetProject('a/b/c');
expect('error' in r).toBe(true);
});
it('rejects whitespace inside', () => {
const r = parseTargetProject('victor /books');
expect('error' in r).toBe(true);
});
it('rejects empty owner segment', () => {
const r = parseTargetProject('//books');
expect('error' in r).toBe(true);
});
});
describe('resolveTarget', () => {
let cacheFile: string;
const baseFixture: CacheFile = {
schemaVersion: 3,
synced_at: '2026-05-08T00:00:00Z',
synced_from: 'https://g',
machine: 'm',
projects: [
{
name: 'victor/books',
default_branch: 'main',
fetched_at: '2026-05-08T00:00:00Z',
active_tasks: [],
all_tasks_count: 0,
raw: '',
},
{
name: 'OpeItcLoc03/common',
default_branch: 'master',
fetched_at: '2026-05-08T00:00:00Z',
active_tasks: [],
all_tasks_count: 0,
raw: '',
},
],
errors: [],
};
beforeEach(async () => {
const dir = await mkdtemp(join(tmpdir(), 'resolve-target-'));
cacheFile = join(dir, 'cache.json');
await writeCache(cacheFile, baseFixture);
});
it('returns owner+repo+branch for qualified target in cache', async () => {
const r = await resolveTarget(cacheFile, 'victor/books', 'agenda', 'OpeItcLoc03');
expect(r).toEqual({ owner: 'victor', repo: 'books', branch: 'main', isAgenda: false });
});
it('routes agenda to fallbackOwner + agendaRepo', async () => {
await writeCache(cacheFile, {
...baseFixture,
projects: [
...baseFixture.projects,
{
name: 'agenda',
default_branch: 'main',
fetched_at: '2026-05-08T00:00:00Z',
active_tasks: [],
all_tasks_count: 0,
raw: '',
},
],
});
const r = await resolveTarget(cacheFile, 'agenda', 'agenda', 'OpeItcLoc03');
expect(r).toEqual({ owner: 'OpeItcLoc03', repo: 'agenda', branch: 'main', isAgenda: true });
});
it('rejects bare name with hint message', async () => {
const r = await resolveTarget(cacheFile, 'books', 'agenda', 'OpeItcLoc03');
expect('error' in r).toBe(true);
expect((r as { error: string }).error).toContain('must be qualified');
});
it('errors on cache miss for qualified target', async () => {
const r = await resolveTarget(cacheFile, 'ghost/repo', 'agenda', 'OpeItcLoc03');
expect('error' in r).toBe(true);
expect((r as { error: string }).error).toContain('not in cache');
});
it('qualified-unknown owner returns cache-miss error, not shape-error', async () => {
const r = await resolveTarget(cacheFile, 'ekassir/books', 'agenda', 'OpeItcLoc03');
expect('error' in r).toBe(true);
const err = (r as { error: string }).error;
expect(err).not.toContain('must be qualified');
expect(err).toContain('not in cache');
expect(err).toContain('ekassir/books');
});
it('errors on missing agenda entry in cache', async () => {
const r = await resolveTarget(cacheFile, 'agenda', 'agenda', 'OpeItcLoc03');
expect('error' in r).toBe(true);
expect((r as { error: string }).error).toContain('agenda not in cache');
});
it('errors when agenda is local-only', async () => {
await writeCache(cacheFile, {
...baseFixture,
projects: [
...baseFixture.projects,
{
name: 'agenda',
default_branch: 'local',
fetched_at: '2026-05-08T00:00:00Z',
active_tasks: [],
all_tasks_count: 0,
raw: '',
},
],
});
const r = await resolveTarget(cacheFile, 'agenda', 'agenda', 'OpeItcLoc03');
expect('error' in r).toBe(true);
expect((r as { error: string }).error).toContain('Gitea repo "agenda" not found');
});
it('errors when cache missing entirely', async () => {
const r = await resolveTarget('/nonexistent/cache.json', 'victor/books', 'agenda', 'OpeItcLoc03');
expect('error' in r).toBe(true);
expect((r as { error: string }).error).toContain('cache missing');
});
it('routes agenda to agendaOwner override (qualified agenda_tasks_repo)', async () => {
await writeCache(cacheFile, {
...baseFixture,
projects: [
...baseFixture.projects,
{
name: 'agenda',
default_branch: 'main',
fetched_at: '2026-05-08T00:00:00Z',
active_tasks: [],
all_tasks_count: 0,
raw: '',
},
],
});
const r = await resolveTarget(cacheFile, 'agenda', 'agenda', 'OpeItcLoc03', 'victor');
expect(r).toEqual({ owner: 'victor', repo: 'agenda', branch: 'main', isAgenda: true });
});
it('falls back to fallbackOwner when agendaOwner is undefined (bare agenda_tasks_repo)', async () => {
await writeCache(cacheFile, {
...baseFixture,
projects: [
...baseFixture.projects,
{
name: 'agenda',
default_branch: 'main',
fetched_at: '2026-05-08T00:00:00Z',
active_tasks: [],
all_tasks_count: 0,
raw: '',
},
],
});
const r = await resolveTarget(cacheFile, 'agenda', 'agenda', 'OpeItcLoc03', undefined);
expect(r).toEqual({ owner: 'OpeItcLoc03', repo: 'agenda', branch: 'main', isAgenda: true });
});
});