test(api): integration tests for MS22 knowledge layer modules (MS22-TEST-001) (#594)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #594.
This commit is contained in:
198
apps/api/src/agent-memory/agent-memory.integration.spec.ts
Normal file
198
apps/api/src/agent-memory/agent-memory.integration.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { beforeAll, beforeEach, describe, expect, it, afterAll } from "vitest";
|
||||||
|
import { randomUUID as uuid } from "crypto";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { NotFoundException } from "@nestjs/common";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { AgentMemoryService } from "./agent-memory.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
|
||||||
|
const shouldRunDbIntegrationTests =
|
||||||
|
process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL);
|
||||||
|
const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip;
|
||||||
|
|
||||||
|
async function createWorkspace(
|
||||||
|
prisma: PrismaClient,
|
||||||
|
label: string
|
||||||
|
): Promise<{ workspaceId: string; ownerId: string }> {
|
||||||
|
const workspace = await prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
name: `${label} ${Date.now()}`,
|
||||||
|
owner: {
|
||||||
|
create: {
|
||||||
|
email: `${label.toLowerCase().replace(/\s+/g, "-")}-${Date.now()}@example.com`,
|
||||||
|
name: `${label} Owner`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
ownerId: workspace.ownerId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describeFn("AgentMemoryService Integration", () => {
|
||||||
|
let moduleRef: TestingModule;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
let service: AgentMemoryService;
|
||||||
|
let setupComplete = false;
|
||||||
|
|
||||||
|
let workspaceAId: string;
|
||||||
|
let workspaceAOwnerId: string;
|
||||||
|
let workspaceBId: string;
|
||||||
|
let workspaceBOwnerId: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
await prisma.$connect();
|
||||||
|
|
||||||
|
const workspaceA = await createWorkspace(prisma, "Agent Memory Integration A");
|
||||||
|
workspaceAId = workspaceA.workspaceId;
|
||||||
|
workspaceAOwnerId = workspaceA.ownerId;
|
||||||
|
|
||||||
|
const workspaceB = await createWorkspace(prisma, "Agent Memory Integration B");
|
||||||
|
workspaceBId = workspaceB.workspaceId;
|
||||||
|
workspaceBOwnerId = workspaceB.ownerId;
|
||||||
|
|
||||||
|
moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AgentMemoryService,
|
||||||
|
{
|
||||||
|
provide: PrismaService,
|
||||||
|
useValue: prisma,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = moduleRef.get<AgentMemoryService>(AgentMemoryService);
|
||||||
|
setupComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.agentMemory.deleteMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: {
|
||||||
|
in: [workspaceAId, workspaceBId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (!prisma) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceIds = [workspaceAId, workspaceBId].filter(
|
||||||
|
(id): id is string => typeof id === "string"
|
||||||
|
);
|
||||||
|
const ownerIds = [workspaceAOwnerId, workspaceBOwnerId].filter(
|
||||||
|
(id): id is string => typeof id === "string"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (workspaceIds.length > 0) {
|
||||||
|
await prisma.agentMemory.deleteMany({
|
||||||
|
where: {
|
||||||
|
workspaceId: {
|
||||||
|
in: workspaceIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.workspace.deleteMany({ where: { id: { in: workspaceIds } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerIds.length > 0) {
|
||||||
|
await prisma.user.deleteMany({ where: { id: { in: ownerIds } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleRef) {
|
||||||
|
await moduleRef.close();
|
||||||
|
}
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upserts and lists memory entries", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = `agent-${uuid()}`;
|
||||||
|
|
||||||
|
const entry = await service.upsert(workspaceAId, agentId, "session-context", {
|
||||||
|
value: { intent: "create-tests", depth: "integration" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(entry.workspaceId).toBe(workspaceAId);
|
||||||
|
expect(entry.agentId).toBe(agentId);
|
||||||
|
expect(entry.key).toBe("session-context");
|
||||||
|
|
||||||
|
const listed = await service.findAll(workspaceAId, agentId);
|
||||||
|
|
||||||
|
expect(listed).toHaveLength(1);
|
||||||
|
expect(listed[0]?.id).toBe(entry.id);
|
||||||
|
expect(listed[0]?.value).toMatchObject({ intent: "create-tests" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates existing key via upsert without creating duplicates", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = `agent-${uuid()}`;
|
||||||
|
|
||||||
|
const first = await service.upsert(workspaceAId, agentId, "preferences", {
|
||||||
|
value: { model: "fast" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const second = await service.upsert(workspaceAId, agentId, "preferences", {
|
||||||
|
value: { model: "accurate" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(second.id).toBe(first.id);
|
||||||
|
expect(second.value).toMatchObject({ model: "accurate" });
|
||||||
|
|
||||||
|
const rowCount = await prisma.agentMemory.count({
|
||||||
|
where: {
|
||||||
|
workspaceId: workspaceAId,
|
||||||
|
agentId,
|
||||||
|
key: "preferences",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rowCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists keys in sorted order and isolates by workspace", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = `agent-${uuid()}`;
|
||||||
|
|
||||||
|
await service.upsert(workspaceAId, agentId, "beta", { value: { v: 2 } });
|
||||||
|
await service.upsert(workspaceAId, agentId, "alpha", { value: { v: 1 } });
|
||||||
|
await service.upsert(workspaceBId, agentId, "alpha", { value: { v: 99 } });
|
||||||
|
|
||||||
|
const workspaceAEntries = await service.findAll(workspaceAId, agentId);
|
||||||
|
const workspaceBEntries = await service.findAll(workspaceBId, agentId);
|
||||||
|
|
||||||
|
expect(workspaceAEntries.map((row) => row.key)).toEqual(["alpha", "beta"]);
|
||||||
|
expect(workspaceBEntries).toHaveLength(1);
|
||||||
|
expect(workspaceBEntries[0]?.value).toMatchObject({ v: 99 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws NotFoundException when requesting unknown key", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(service.findOne(workspaceAId, `agent-${uuid()}`, "missing")).rejects.toThrow(
|
||||||
|
NotFoundException
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { beforeAll, beforeEach, describe, expect, it, afterAll, vi } from "vitest";
|
||||||
|
import { randomUUID as uuid } from "crypto";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { ConflictException } from "@nestjs/common";
|
||||||
|
import { PrismaClient, Prisma } from "@prisma/client";
|
||||||
|
import { EMBEDDING_DIMENSION } from "@mosaic/shared";
|
||||||
|
import { ConversationArchiveService } from "./conversation-archive.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { EmbeddingService } from "../knowledge/services/embedding.service";
|
||||||
|
|
||||||
|
const shouldRunDbIntegrationTests =
|
||||||
|
process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL);
|
||||||
|
const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip;
|
||||||
|
|
||||||
|
function vector(value: number): number[] {
|
||||||
|
return Array.from({ length: EMBEDDING_DIMENSION }, () => value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toVectorLiteral(input: number[]): string {
|
||||||
|
return `[${input.join(",")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describeFn("ConversationArchiveService Integration", () => {
|
||||||
|
let moduleRef: TestingModule;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
let service: ConversationArchiveService;
|
||||||
|
let workspaceId: string;
|
||||||
|
let ownerId: string;
|
||||||
|
let setupComplete = false;
|
||||||
|
|
||||||
|
const embeddingServiceMock = {
|
||||||
|
isConfigured: vi.fn(),
|
||||||
|
generateEmbedding: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
await prisma.$connect();
|
||||||
|
|
||||||
|
const workspace = await prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
name: `Conversation Archive Integration ${Date.now()}`,
|
||||||
|
owner: {
|
||||||
|
create: {
|
||||||
|
email: `conversation-archive-integration-${Date.now()}@example.com`,
|
||||||
|
name: "Conversation Archive Integration Owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceId = workspace.id;
|
||||||
|
ownerId = workspace.ownerId;
|
||||||
|
|
||||||
|
moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ConversationArchiveService,
|
||||||
|
{
|
||||||
|
provide: PrismaService,
|
||||||
|
useValue: prisma,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: EmbeddingService,
|
||||||
|
useValue: embeddingServiceMock,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = moduleRef.get<ConversationArchiveService>(ConversationArchiveService);
|
||||||
|
setupComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
embeddingServiceMock.isConfigured.mockReturnValue(false);
|
||||||
|
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.conversationArchive.deleteMany({ where: { workspaceId } });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (!prisma) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaceId) {
|
||||||
|
await prisma.conversationArchive.deleteMany({ where: { workspaceId } });
|
||||||
|
await prisma.workspace.deleteMany({ where: { id: workspaceId } });
|
||||||
|
}
|
||||||
|
if (ownerId) {
|
||||||
|
await prisma.user.deleteMany({ where: { id: ownerId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleRef) {
|
||||||
|
await moduleRef.close();
|
||||||
|
}
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ingests a conversation log", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = `session-${uuid()}`;
|
||||||
|
|
||||||
|
const result = await service.ingest(workspaceId, {
|
||||||
|
sessionId,
|
||||||
|
agentId: "agent-conversation-ingest",
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: "Can you summarize deployment issues?" },
|
||||||
|
{ role: "assistant", content: "Yes, three retries timed out in staging." },
|
||||||
|
],
|
||||||
|
summary: "Deployment retry failures discussed",
|
||||||
|
startedAt: "2026-02-28T21:00:00.000Z",
|
||||||
|
endedAt: "2026-02-28T21:05:00.000Z",
|
||||||
|
metadata: { source: "integration-test" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.id).toBeDefined();
|
||||||
|
|
||||||
|
const stored = await prisma.conversationArchive.findUnique({
|
||||||
|
where: {
|
||||||
|
id: result.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stored).toBeTruthy();
|
||||||
|
expect(stored?.workspaceId).toBe(workspaceId);
|
||||||
|
expect(stored?.sessionId).toBe(sessionId);
|
||||||
|
expect(stored?.messageCount).toBe(2);
|
||||||
|
expect(stored?.summary).toBe("Deployment retry failures discussed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects duplicate session ingest per workspace", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = `session-${uuid()}`;
|
||||||
|
const dto = {
|
||||||
|
sessionId,
|
||||||
|
agentId: "agent-conversation-duplicate",
|
||||||
|
messages: [{ role: "user", content: "hello" }],
|
||||||
|
summary: "simple conversation",
|
||||||
|
startedAt: "2026-02-28T22:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.ingest(workspaceId, dto);
|
||||||
|
|
||||||
|
await expect(service.ingest(workspaceId, dto)).rejects.toThrow(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects semantic search when embeddings are disabled", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddingServiceMock.isConfigured.mockReturnValue(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.search(workspaceId, {
|
||||||
|
query: "deployment retries",
|
||||||
|
})
|
||||||
|
).rejects.toThrow(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("searches archived conversations by vector similarity", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const near = vector(0.02);
|
||||||
|
const far = vector(0.85);
|
||||||
|
|
||||||
|
const matching = await prisma.conversationArchive.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
sessionId: `session-search-${uuid()}`,
|
||||||
|
agentId: "agent-conversation-search-a",
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: "What caused deployment retries?" },
|
||||||
|
{ role: "assistant", content: "A connection pool timeout." },
|
||||||
|
] as unknown as Prisma.InputJsonValue,
|
||||||
|
messageCount: 2,
|
||||||
|
summary: "Deployment retries caused by connection pool timeout",
|
||||||
|
startedAt: new Date("2026-02-28T23:00:00.000Z"),
|
||||||
|
metadata: { channel: "cli" } as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonMatching = await prisma.conversationArchive.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
sessionId: `session-search-${uuid()}`,
|
||||||
|
agentId: "agent-conversation-search-b",
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: "How is billing configured?" },
|
||||||
|
] as unknown as Prisma.InputJsonValue,
|
||||||
|
messageCount: 1,
|
||||||
|
summary: "Billing and quotas conversation",
|
||||||
|
startedAt: new Date("2026-02-28T23:10:00.000Z"),
|
||||||
|
metadata: { channel: "cli" } as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE conversation_archives
|
||||||
|
SET embedding = ${toVectorLiteral(near)}::vector(${EMBEDDING_DIMENSION})
|
||||||
|
WHERE id = ${matching.id}::uuid
|
||||||
|
`;
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE conversation_archives
|
||||||
|
SET embedding = ${toVectorLiteral(far)}::vector(${EMBEDDING_DIMENSION})
|
||||||
|
WHERE id = ${nonMatching.id}::uuid
|
||||||
|
`;
|
||||||
|
|
||||||
|
embeddingServiceMock.isConfigured.mockReturnValue(true);
|
||||||
|
embeddingServiceMock.generateEmbedding.mockResolvedValue(near);
|
||||||
|
|
||||||
|
const result = await service.search(workspaceId, {
|
||||||
|
query: "deployment retries timeout",
|
||||||
|
agentId: "agent-conversation-search-a",
|
||||||
|
similarityThreshold: 0,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = result.data as Array<{ id: string; agent_id: string; similarity: number }>;
|
||||||
|
|
||||||
|
expect(result.pagination.total).toBe(1);
|
||||||
|
expect(rows).toHaveLength(1);
|
||||||
|
expect(rows[0]?.id).toBe(matching.id);
|
||||||
|
expect(rows[0]?.agent_id).toBe("agent-conversation-search-a");
|
||||||
|
expect(rows[0]?.similarity).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
226
apps/api/src/findings/findings.integration.spec.ts
Normal file
226
apps/api/src/findings/findings.integration.spec.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { beforeAll, beforeEach, describe, expect, it, afterAll, vi } from "vitest";
|
||||||
|
import { randomUUID as uuid } from "crypto";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { BadRequestException, NotFoundException } from "@nestjs/common";
|
||||||
|
import { PrismaClient, Prisma } from "@prisma/client";
|
||||||
|
import { FindingsService } from "./findings.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { EmbeddingService } from "../knowledge/services/embedding.service";
|
||||||
|
|
||||||
|
const shouldRunDbIntegrationTests =
|
||||||
|
process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL);
|
||||||
|
const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip;
|
||||||
|
|
||||||
|
const EMBEDDING_DIMENSION = 1536;
|
||||||
|
|
||||||
|
function vector(value: number): number[] {
|
||||||
|
return Array.from({ length: EMBEDDING_DIMENSION }, () => value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toVectorLiteral(input: number[]): string {
|
||||||
|
return `[${input.join(",")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describeFn("FindingsService Integration", () => {
|
||||||
|
let moduleRef: TestingModule;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
let service: FindingsService;
|
||||||
|
let workspaceId: string;
|
||||||
|
let ownerId: string;
|
||||||
|
let setupComplete = false;
|
||||||
|
|
||||||
|
const embeddingServiceMock = {
|
||||||
|
isConfigured: vi.fn(),
|
||||||
|
generateEmbedding: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
await prisma.$connect();
|
||||||
|
|
||||||
|
const workspace = await prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
name: `Findings Integration ${Date.now()}`,
|
||||||
|
owner: {
|
||||||
|
create: {
|
||||||
|
email: `findings-integration-${Date.now()}@example.com`,
|
||||||
|
name: "Findings Integration Owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceId = workspace.id;
|
||||||
|
ownerId = workspace.ownerId;
|
||||||
|
|
||||||
|
moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
FindingsService,
|
||||||
|
{
|
||||||
|
provide: PrismaService,
|
||||||
|
useValue: prisma,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: EmbeddingService,
|
||||||
|
useValue: embeddingServiceMock,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = moduleRef.get<FindingsService>(FindingsService);
|
||||||
|
setupComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
embeddingServiceMock.isConfigured.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (!prisma) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaceId) {
|
||||||
|
await prisma.finding.deleteMany({ where: { workspaceId } });
|
||||||
|
await prisma.workspace.deleteMany({ where: { id: workspaceId } });
|
||||||
|
}
|
||||||
|
if (ownerId) {
|
||||||
|
await prisma.user.deleteMany({ where: { id: ownerId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleRef) {
|
||||||
|
await moduleRef.close();
|
||||||
|
}
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates, lists, fetches, and deletes findings", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await service.create(workspaceId, {
|
||||||
|
agentId: "agent-findings-crud",
|
||||||
|
type: "security",
|
||||||
|
title: "Unescaped SQL fragment",
|
||||||
|
data: { severity: "high" },
|
||||||
|
summary: "Potential injection risk in dynamic query path.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(created.id).toBeDefined();
|
||||||
|
expect(created.workspaceId).toBe(workspaceId);
|
||||||
|
expect(created.taskId).toBeNull();
|
||||||
|
|
||||||
|
const listed = await service.findAll(workspaceId, {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
agentId: "agent-findings-crud",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listed.meta.total).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(listed.data.some((row) => row.id === created.id)).toBe(true);
|
||||||
|
|
||||||
|
const found = await service.findOne(created.id, workspaceId);
|
||||||
|
expect(found.id).toBe(created.id);
|
||||||
|
expect(found.title).toBe("Unescaped SQL fragment");
|
||||||
|
|
||||||
|
await expect(service.findOne(created.id, uuid())).rejects.toThrow(NotFoundException);
|
||||||
|
|
||||||
|
await expect(service.remove(created.id, workspaceId)).resolves.toEqual({
|
||||||
|
message: "Finding deleted successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(service.findOne(created.id, workspaceId)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects create when taskId does not exist in workspace", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.create(workspaceId, {
|
||||||
|
taskId: uuid(),
|
||||||
|
agentId: "agent-findings-missing-task",
|
||||||
|
type: "bug",
|
||||||
|
title: "Invalid task id",
|
||||||
|
data: { source: "integration-test" },
|
||||||
|
summary: "Should fail when task relation is not found.",
|
||||||
|
})
|
||||||
|
).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects vector search when embeddings are disabled", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddingServiceMock.isConfigured.mockReturnValue(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.search(workspaceId, {
|
||||||
|
query: "security issue",
|
||||||
|
})
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("searches findings by vector similarity with filters", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const near = vector(0.01);
|
||||||
|
const far = vector(0.9);
|
||||||
|
|
||||||
|
const matchedFinding = await prisma.finding.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
agentId: "agent-findings-search-a",
|
||||||
|
type: "incident",
|
||||||
|
title: "Authentication bypass",
|
||||||
|
data: { score: 0.9 } as Prisma.InputJsonValue,
|
||||||
|
summary: "Bypass risk found in login checks.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherFinding = await prisma.finding.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
agentId: "agent-findings-search-b",
|
||||||
|
type: "incident",
|
||||||
|
title: "Retry timeout",
|
||||||
|
data: { score: 0.2 } as Prisma.InputJsonValue,
|
||||||
|
summary: "Timeout issue in downstream retries.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE findings
|
||||||
|
SET embedding = ${toVectorLiteral(near)}::vector(1536)
|
||||||
|
WHERE id = ${matchedFinding.id}::uuid
|
||||||
|
`;
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE findings
|
||||||
|
SET embedding = ${toVectorLiteral(far)}::vector(1536)
|
||||||
|
WHERE id = ${otherFinding.id}::uuid
|
||||||
|
`;
|
||||||
|
|
||||||
|
embeddingServiceMock.isConfigured.mockReturnValue(true);
|
||||||
|
embeddingServiceMock.generateEmbedding.mockResolvedValue(near);
|
||||||
|
|
||||||
|
const result = await service.search(workspaceId, {
|
||||||
|
query: "authentication bypass risk",
|
||||||
|
agentId: "agent-findings-search-a",
|
||||||
|
limit: 10,
|
||||||
|
similarityThreshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.query).toBe("authentication bypass risk");
|
||||||
|
expect(result.meta.total).toBe(1);
|
||||||
|
expect(result.data).toHaveLength(1);
|
||||||
|
expect(result.data[0]?.id).toBe(matchedFinding.id);
|
||||||
|
expect(result.data[0]?.agentId).toBe("agent-findings-search-a");
|
||||||
|
expect(result.data.find((row) => row.id === otherFinding.id)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
162
apps/api/src/tasks/tasks.assigned-agent.integration.spec.ts
Normal file
162
apps/api/src/tasks/tasks.assigned-agent.integration.spec.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { beforeAll, beforeEach, describe, expect, it, afterAll, vi } from "vitest";
|
||||||
|
import { randomUUID as uuid } from "crypto";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { TasksService } from "./tasks.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { ActivityService } from "../activity/activity.service";
|
||||||
|
|
||||||
|
const shouldRunDbIntegrationTests =
|
||||||
|
process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL);
|
||||||
|
const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeFn("TasksService assignedAgent Integration", () => {
|
||||||
|
let moduleRef: TestingModule;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
let service: TasksService;
|
||||||
|
let workspaceId: string;
|
||||||
|
let ownerId: string;
|
||||||
|
let setupComplete = false;
|
||||||
|
|
||||||
|
const activityServiceMock = {
|
||||||
|
logTaskCreated: vi.fn().mockResolvedValue(undefined),
|
||||||
|
logTaskUpdated: vi.fn().mockResolvedValue(undefined),
|
||||||
|
logTaskDeleted: vi.fn().mockResolvedValue(undefined),
|
||||||
|
logTaskCompleted: vi.fn().mockResolvedValue(undefined),
|
||||||
|
logTaskAssigned: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
prisma = new PrismaClient();
|
||||||
|
await prisma.$connect();
|
||||||
|
|
||||||
|
const workspace = await prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
name: `Tasks Assigned Agent Integration ${Date.now()}`,
|
||||||
|
owner: {
|
||||||
|
create: {
|
||||||
|
email: `tasks-assigned-agent-integration-${Date.now()}@example.com`,
|
||||||
|
name: "Tasks Assigned Agent Integration Owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceId = workspace.id;
|
||||||
|
ownerId = workspace.ownerId;
|
||||||
|
|
||||||
|
moduleRef = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
TasksService,
|
||||||
|
{
|
||||||
|
provide: PrismaService,
|
||||||
|
useValue: prisma,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ActivityService,
|
||||||
|
useValue: activityServiceMock,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = moduleRef.get<TasksService>(TasksService);
|
||||||
|
setupComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.task.deleteMany({ where: { workspaceId } });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (!prisma) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaceId) {
|
||||||
|
await prisma.task.deleteMany({ where: { workspaceId } });
|
||||||
|
await prisma.workspace.deleteMany({ where: { id: workspaceId } });
|
||||||
|
}
|
||||||
|
if (ownerId) {
|
||||||
|
await prisma.user.deleteMany({ where: { id: ownerId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleRef) {
|
||||||
|
await moduleRef.close();
|
||||||
|
}
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists assignedAgent on create", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await service.create(workspaceId, ownerId, {
|
||||||
|
title: `Assigned agent create ${uuid()}`,
|
||||||
|
assignedAgent: "fleet-worker-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(task.assignedAgent).toBe("fleet-worker-1");
|
||||||
|
|
||||||
|
const stored = await prisma.task.findUnique({
|
||||||
|
where: {
|
||||||
|
id: task.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
assignedAgent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stored).toMatchObject({
|
||||||
|
id: task.id,
|
||||||
|
assignedAgent: "fleet-worker-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const listed = await service.findAll({ workspaceId, page: 1, limit: 10 }, ownerId);
|
||||||
|
const listedTask = listed.data.find((row) => row.id === task.id);
|
||||||
|
|
||||||
|
expect(listedTask?.assignedAgent).toBe("fleet-worker-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates and clears assignedAgent", async () => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await service.create(workspaceId, ownerId, {
|
||||||
|
title: `Assigned agent update ${uuid()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(created.assignedAgent).toBeNull();
|
||||||
|
|
||||||
|
const updated = await service.update(created.id, workspaceId, ownerId, {
|
||||||
|
assignedAgent: "fleet-worker-2",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated.assignedAgent).toBe("fleet-worker-2");
|
||||||
|
|
||||||
|
const cleared = await service.update(created.id, workspaceId, ownerId, {
|
||||||
|
assignedAgent: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cleared.assignedAgent).toBeNull();
|
||||||
|
|
||||||
|
const stored = await prisma.task.findUnique({
|
||||||
|
where: {
|
||||||
|
id: created.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
assignedAgent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stored?.assignedAgent).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -69,5 +69,5 @@ Remaining estimate: ~143K tokens (Codex budget).
|
|||||||
| MS22-API-003 | not-started | p0-knowledge | Task API: expose assigned_agent in CRUD | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-003 | MS22-TEST-001 | — | — | — | 8K | — | Extend existing TaskModule |
|
| MS22-API-003 | not-started | p0-knowledge | Task API: expose assigned_agent in CRUD | TASKS:P0 | api | feat/ms22-task-agent | MS22-DB-003 | MS22-TEST-001 | — | — | — | 8K | — | Extend existing TaskModule |
|
||||||
| MS22-TEST-001 | not-started | p0-knowledge | Integration tests: Findings + AgentMemory + ConvArchive | TASKS:P0 | api | test/ms22-integration | MS22-API-001,MS22-API-002,MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | E2E with live postgres |
|
| MS22-TEST-001 | not-started | p0-knowledge | Integration tests: Findings + AgentMemory + ConvArchive | TASKS:P0 | api | test/ms22-integration | MS22-API-001,MS22-API-002,MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | E2E with live postgres |
|
||||||
| MS22-SKILL-001 | not-started | p0-knowledge | OpenClaw mosaic skill (agents read/write findings/memory) | TASKS:P0 | stack | feat/ms22-openclaw-skill | MS22-API-001,MS22-API-002 | MS22-VER-P0 | — | — | — | 15K | — | Skill in ~/.agents/skills/mosaic/ |
|
| MS22-SKILL-001 | not-started | p0-knowledge | OpenClaw mosaic skill (agents read/write findings/memory) | TASKS:P0 | stack | feat/ms22-openclaw-skill | MS22-API-001,MS22-API-002 | MS22-VER-P0 | — | — | — | 15K | — | Skill in ~/.agents/skills/mosaic/ |
|
||||||
| MS22-INGEST-001 | not-started | p0-knowledge | Session log ingestion pipeline (OpenClaw logs → ConvArchive) | TASKS:P0 | stack | feat/ms22-ingest | MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | Script to batch-ingest existing logs |
|
| MS22-INGEST-001 | done | p0-knowledge | Session log ingestion pipeline (OpenClaw logs → ConvArchive) | TASKS:P0 | stack | feat/ms22-ingest | MS22-API-004 | MS22-VER-P0 | — | — | — | 20K | — | Script to batch-ingest existing logs |
|
||||||
| MS22-VER-P0 | not-started | p0-knowledge | Phase 0 verification: all modules deployed + smoke tested | TASKS:P0 | stack | — | MS22-TEST-001,MS22-SKILL-001,MS22-INGEST-001,MS22-API-003 | — | — | — | — | 5K | — | |
|
| MS22-VER-P0 | not-started | p0-knowledge | Phase 0 verification: all modules deployed + smoke tested | TASKS:P0 | stack | — | MS22-TEST-001,MS22-SKILL-001,MS22-INGEST-001,MS22-API-003 | — | — | — | — | 5K | — | |
|
||||||
|
|||||||
Reference in New Issue
Block a user