Files
stack/apps/api/src/knowledge/services/link-resolution.service.spec.ts
T
Jason WoltjeandClaude Sonnet 4.5 12abdfe81d feat(#93): implement agent spawn via federation
Implements FED-010: Agent Spawn via Federation feature that enables
spawning and managing Claude agents on remote federated Mosaic Stack
instances via COMMAND message type.

Features:
- Federation agent command types (spawn, status, kill)
- FederationAgentService for handling agent operations
- Integration with orchestrator's agent spawner/lifecycle services
- API endpoints for spawning, querying status, and killing agents
- Full command routing through federation COMMAND infrastructure
- Comprehensive test coverage (12/12 tests passing)

Architecture:
- Hub → Spoke: Spawn agents on remote instances
- Command flow: FederationController → FederationAgentService →
  CommandService → Remote Orchestrator
- Response handling: Remote orchestrator returns agent status/results
- Security: Connection validation, signature verification

Files created:
- apps/api/src/federation/types/federation-agent.types.ts
- apps/api/src/federation/federation-agent.service.ts
- apps/api/src/federation/federation-agent.service.spec.ts

Files modified:
- apps/api/src/federation/command.service.ts (agent command routing)
- apps/api/src/federation/federation.controller.ts (agent endpoints)
- apps/api/src/federation/federation.module.ts (service registration)
- apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint)
- apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration)

Testing:
- 12/12 tests passing for FederationAgentService
- All command service tests passing
- TypeScript compilation successful
- Linting passed

Refs #93

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-02-03 14:37:06 -06:00

537 lines
17 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { LinkResolutionService } from "./link-resolution.service";
import { PrismaService } from "../../prisma/prisma.service";
describe("LinkResolutionService", () => {
let service: LinkResolutionService;
let prisma: PrismaService;
const workspaceId = "workspace-123";
const mockEntries = [
{
id: "entry-1",
workspaceId,
slug: "typescript-guide",
title: "TypeScript Guide",
content: "...",
contentHtml: "...",
summary: null,
status: "PUBLISHED",
visibility: "WORKSPACE",
createdAt: new Date(),
updatedAt: new Date(),
createdBy: "user-1",
updatedBy: "user-1",
},
{
id: "entry-2",
workspaceId,
slug: "react-hooks",
title: "React Hooks",
content: "...",
contentHtml: "...",
summary: null,
status: "PUBLISHED",
visibility: "WORKSPACE",
createdAt: new Date(),
updatedAt: new Date(),
createdBy: "user-1",
updatedBy: "user-1",
},
{
id: "entry-3",
workspaceId,
slug: "react-hooks-advanced",
title: "React Hooks Advanced",
content: "...",
contentHtml: "...",
summary: null,
status: "PUBLISHED",
visibility: "WORKSPACE",
createdAt: new Date(),
updatedAt: new Date(),
createdBy: "user-1",
updatedBy: "user-1",
},
];
const mockPrismaService = {
knowledgeEntry: {
findUnique: vi.fn(),
findFirst: vi.fn(),
findMany: vi.fn(),
},
knowledgeLink: {
findMany: vi.fn(),
},
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
LinkResolutionService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
],
}).compile();
service = module.get<LinkResolutionService>(LinkResolutionService);
prisma = module.get<PrismaService>(PrismaService);
vi.clearAllMocks();
});
describe("resolveLink", () => {
describe("Exact title match", () => {
it("should resolve link by exact title match", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);
const result = await service.resolveLink(workspaceId, "TypeScript Guide");
expect(result).toBe("entry-1");
expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledWith({
where: {
workspaceId,
title: "TypeScript Guide",
},
select: {
id: true,
},
});
});
it("should be case-sensitive for exact title match", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);
const result = await service.resolveLink(workspaceId, "typescript guide");
expect(result).toBeNull();
});
});
describe("Slug match", () => {
it("should resolve link by slug", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(mockEntries[0]);
const result = await service.resolveLink(workspaceId, "typescript-guide");
expect(result).toBe("entry-1");
expect(mockPrismaService.knowledgeEntry.findUnique).toHaveBeenCalledWith({
where: {
workspaceId_slug: {
workspaceId,
slug: "typescript-guide",
},
},
select: {
id: true,
},
});
});
it("should prioritize exact title match over slug match", async () => {
// If exact title matches, slug should not be checked
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);
const result = await service.resolveLink(workspaceId, "TypeScript Guide");
expect(result).toBe("entry-1");
expect(mockPrismaService.knowledgeEntry.findUnique).not.toHaveBeenCalled();
});
});
describe("Fuzzy title match", () => {
it("should resolve link by case-insensitive fuzzy match", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([mockEntries[0]]);
const result = await service.resolveLink(workspaceId, "typescript guide");
expect(result).toBe("entry-1");
expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith({
where: {
workspaceId,
title: {
mode: "insensitive",
equals: "typescript guide",
},
},
select: {
id: true,
title: true,
},
});
});
it("should return null when fuzzy match finds multiple results", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([
mockEntries[1],
mockEntries[2],
]);
const result = await service.resolveLink(workspaceId, "react hooks");
expect(result).toBeNull();
});
it("should return null when no match is found", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);
const result = await service.resolveLink(workspaceId, "Non-existent Entry");
expect(result).toBeNull();
});
});
describe("Workspace scoping", () => {
it("should only resolve links within the specified workspace", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);
await service.resolveLink("different-workspace", "TypeScript Guide");
expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
workspaceId: "different-workspace",
}),
})
);
});
});
describe("Edge cases", () => {
it("should handle empty string input", async () => {
const result = await service.resolveLink(workspaceId, "");
expect(result).toBeNull();
expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
});
it("should handle null input", async () => {
const result = await service.resolveLink(workspaceId, null as any);
expect(result).toBeNull();
expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
});
it("should handle whitespace-only input", async () => {
const result = await service.resolveLink(workspaceId, " ");
expect(result).toBeNull();
expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
});
it("should trim whitespace from target before resolving", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);
const result = await service.resolveLink(workspaceId, " TypeScript Guide ");
expect(result).toBe("entry-1");
expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
title: "TypeScript Guide",
}),
})
);
});
});
});
describe("resolveLinks", () => {
it("should resolve multiple links in batch", async () => {
// First link: "TypeScript Guide" -> exact title match
// Second link: "react-hooks" -> slug match
mockPrismaService.knowledgeEntry.findFirst.mockImplementation(async ({ where }: any) => {
if (where.title === "TypeScript Guide") {
return mockEntries[0];
}
return null;
});
mockPrismaService.knowledgeEntry.findUnique.mockImplementation(async ({ where }: any) => {
if (where.workspaceId_slug?.slug === "react-hooks") {
return mockEntries[1];
}
return null;
});
mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([]);
const targets = ["TypeScript Guide", "react-hooks"];
const result = await service.resolveLinks(workspaceId, targets);
expect(result).toEqual({
"TypeScript Guide": "entry-1",
"react-hooks": "entry-2",
});
});
it("should handle empty array", async () => {
const result = await service.resolveLinks(workspaceId, []);
expect(result).toEqual({});
expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
});
it("should handle unresolved links", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValue(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([]);
const result = await service.resolveLinks(workspaceId, [
"Non-existent",
"Another-Non-existent",
]);
expect(result).toEqual({
"Non-existent": null,
"Another-Non-existent": null,
});
});
it("should deduplicate targets", async () => {
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(mockEntries[0]);
const result = await service.resolveLinks(workspaceId, [
"TypeScript Guide",
"TypeScript Guide",
]);
expect(result).toEqual({
"TypeScript Guide": "entry-1",
});
// Should only be called once for the deduplicated target
expect(mockPrismaService.knowledgeEntry.findFirst).toHaveBeenCalledTimes(1);
});
});
describe("getAmbiguousMatches", () => {
it("should return multiple entries that match case-insensitively", async () => {
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([
{ id: "entry-2", title: "React Hooks" },
{ id: "entry-3", title: "React Hooks Advanced" },
]);
const result = await service.getAmbiguousMatches(workspaceId, "react hooks");
expect(result).toHaveLength(2);
expect(result).toEqual([
{ id: "entry-2", title: "React Hooks" },
{ id: "entry-3", title: "React Hooks Advanced" },
]);
});
it("should return empty array when no matches found", async () => {
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);
const result = await service.getAmbiguousMatches(workspaceId, "Non-existent");
expect(result).toEqual([]);
});
it("should return single entry if only one match", async () => {
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([
{ id: "entry-1", title: "TypeScript Guide" },
]);
const result = await service.getAmbiguousMatches(workspaceId, "typescript guide");
expect(result).toHaveLength(1);
});
});
describe("resolveLinksFromContent", () => {
it("should parse and resolve wiki links from content", async () => {
const content = "Check out [[TypeScript Guide]] and [[React Hooks]] for more info.";
// Mock resolveLink for each target
mockPrismaService.knowledgeEntry.findFirst
.mockResolvedValueOnce({ id: "entry-1" }) // TypeScript Guide
.mockResolvedValueOnce({ id: "entry-2" }); // React Hooks
const result = await service.resolveLinksFromContent(content, workspaceId);
expect(result).toHaveLength(2);
expect(result[0].link.target).toBe("TypeScript Guide");
expect(result[0].entryId).toBe("entry-1");
expect(result[1].link.target).toBe("React Hooks");
expect(result[1].entryId).toBe("entry-2");
});
it("should return null entryId for unresolved links", async () => {
const content = "See [[Non-existent Page]] for details.";
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);
const result = await service.resolveLinksFromContent(content, workspaceId);
expect(result).toHaveLength(1);
expect(result[0].link.target).toBe("Non-existent Page");
expect(result[0].entryId).toBeNull();
});
it("should return empty array for content with no wiki links", async () => {
const content = "This content has no wiki links.";
const result = await service.resolveLinksFromContent(content, workspaceId);
expect(result).toEqual([]);
expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
});
it("should handle content with display text syntax", async () => {
const content = "Read the [[typescript-guide|TS Guide]] first.";
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce({
id: "entry-1",
});
const result = await service.resolveLinksFromContent(content, workspaceId);
expect(result).toHaveLength(1);
expect(result[0].link.target).toBe("typescript-guide");
expect(result[0].link.displayText).toBe("TS Guide");
expect(result[0].entryId).toBe("entry-1");
});
it("should preserve link position information", async () => {
const content = "Start [[Page One]] middle [[Page Two]] end.";
mockPrismaService.knowledgeEntry.findFirst
.mockResolvedValueOnce({ id: "entry-1" })
.mockResolvedValueOnce({ id: "entry-2" });
const result = await service.resolveLinksFromContent(content, workspaceId);
expect(result).toHaveLength(2);
expect(result[0].link.start).toBe(6);
expect(result[0].link.end).toBe(18);
expect(result[1].link.start).toBe(26);
expect(result[1].link.end).toBe(38);
});
});
describe("getBacklinks", () => {
it("should return all entries that link to the target entry", async () => {
const targetEntryId = "entry-target";
const mockBacklinks = [
{
id: "link-1",
sourceId: "entry-1",
targetId: targetEntryId,
linkText: "Target Page",
displayText: "Target Page",
positionStart: 10,
positionEnd: 25,
resolved: true,
context: null,
createdAt: new Date(),
source: {
id: "entry-1",
title: "TypeScript Guide",
slug: "typescript-guide",
},
},
{
id: "link-2",
sourceId: "entry-2",
targetId: targetEntryId,
linkText: "Target Page",
displayText: "See Target",
positionStart: 50,
positionEnd: 70,
resolved: true,
context: null,
createdAt: new Date(),
source: {
id: "entry-2",
title: "React Hooks",
slug: "react-hooks",
},
},
];
mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce(mockBacklinks);
const result = await service.getBacklinks(targetEntryId);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
sourceId: "entry-1",
sourceTitle: "TypeScript Guide",
sourceSlug: "typescript-guide",
linkText: "Target Page",
displayText: "Target Page",
});
expect(result[1]).toEqual({
sourceId: "entry-2",
sourceTitle: "React Hooks",
sourceSlug: "react-hooks",
linkText: "Target Page",
displayText: "See Target",
});
expect(mockPrismaService.knowledgeLink.findMany).toHaveBeenCalledWith({
where: {
targetId: targetEntryId,
resolved: true,
},
include: {
source: {
select: {
id: true,
title: true,
slug: true,
},
},
},
orderBy: {
createdAt: "desc",
},
});
});
it("should return empty array when no backlinks exist", async () => {
mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce([]);
const result = await service.getBacklinks("entry-with-no-backlinks");
expect(result).toEqual([]);
});
it("should only return resolved backlinks", async () => {
const targetEntryId = "entry-target";
mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce([]);
await service.getBacklinks(targetEntryId);
expect(mockPrismaService.knowledgeLink.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
resolved: true,
}),
})
);
});
});
});