moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { buildServer } from '../../src/index.js';
import { users, teams, teamMembers, teamResources, agents, scoutFindings } from '../../src/db/schema.js';
import { sql, eq } from 'drizzle-orm';
import { ScoutAgent } from '../../src/daemons/scout.js';
describe('Scout Agent (Task 3.18)', () => {
let server: Awaited<ReturnType<typeof buildServer>>;
let userId: string;
let teamId: string;
let scout: ScoutAgent;
beforeAll(async () => {
server = await buildServer();
// Clean up leftover test data
await server.db.execute(sql`DELETE FROM scout_findings WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'scout_%')`);
await server.db.execute(sql`DELETE FROM agents WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'scout_%')`);
await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'scouttest-%'`);
await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'scout_%'`);
// Create test user
const [user] = await server.db.insert(users).values({
clerkId: 'scout_user1',
displayName: 'Scout User',
email: 'scout_user1@test.com',
}).returning();
userId = user.id;
// Create team
const [team] = await server.db.insert(teams).values({
name: 'Scout Test Team',
slug: 'scouttest-team',
ownerId: userId,
}).returning();
teamId = team.id;
// Add membership with interests
await server.db.insert(teamMembers).values({
teamId,
userId,
role: 'owner',
interests: ['python', 'data-science'],
});
// Create an agent with tools for the user
await server.db.insert(agents).values({
userId,
teamId,
name: 'TestAgent',
tools: ['web_search', 'code_review'],
});
// Create team resources
await server.db.insert(teamResources).values([
{
teamId,
resourceType: 'skill',
name: 'Python Data Analyzer',
description: 'A skill for analyzing data with Python',
config: {},
sharedBy: userId,
},
{
teamId,
resourceType: 'prompt_template',
name: 'Code Review Template',
description: 'Template for code reviews',
config: {},
sharedBy: userId,
},
]);
scout = new ScoutAgent(server.db);
// Override auth handler
server._authHandler.fn = async function (request: FastifyRequest, reply: FastifyReply) {
const testUserId = request.headers['x-test-user-id'] as string;
if (!testUserId) {
return reply.code(401).send({ error: 'Missing x-test-user-id header' });
}
request.userId = testUserId;
request.clerkId = 'test';
};
});
afterAll(async () => {
await server.db.execute(sql`DELETE FROM scout_findings WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'scout_%')`);
await server.db.execute(sql`DELETE FROM agents WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'scout_%')`);
await server.db.execute(sql`DELETE FROM team_resources WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'scouttest-%')`);
await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'scouttest-%'`);
await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'scout_%'`);
await server.close();
});
it('scan creates findings from team resources', async () => {
const findings = await scout.scan(userId, teamId);
expect(findings.length).toBeGreaterThanOrEqual(2);
expect(findings.every((f) => f.status === 'new')).toBe(true);
expect(findings.some((f) => f.title.includes('Python Data Analyzer'))).toBe(true);
expect(findings.some((f) => f.title.includes('Code Review Template'))).toBe(true);
});
it('relevance scoring boosts based on user interests', async () => {
// The Python Data Analyzer should be boosted because user has 'python' interest
const findings = await scout.listFindings(userId);
const pythonFinding = findings.find((f) => f.title.includes('Python'));
const templateFinding = findings.find((f) => f.title.includes('Code Review'));
expect(pythonFinding).toBeTruthy();
expect(templateFinding).toBeTruthy();
// Python finding should have higher score (base 0.5 + 0.3 interest boost)
expect(pythonFinding!.relevanceScore).toBeGreaterThan(templateFinding!.relevanceScore);
});
it('adopt updates status', async () => {
const findings = await scout.listFindings(userId);
const finding = findings[0];
const updated = await scout.adopt(finding.id);
expect(updated).toBeTruthy();
expect(updated!.status).toBe('adopted');
});
it('dismiss updates status', async () => {
const findings = await scout.listFindings(userId);
const newFinding = findings.find((f) => f.status === 'new');
expect(newFinding).toBeTruthy();
const updated = await scout.dismiss(newFinding!.id);
expect(updated).toBeTruthy();
expect(updated!.status).toBe('dismissed');
});
it('dismissed finding not resurfaced in future scans', async () => {
// Clear all non-dismissed findings
const allFindings = await scout.listFindings(userId);
const dismissed = allFindings.filter((f) => f.status === 'dismissed');
expect(dismissed.length).toBeGreaterThan(0);
// Run scan again — dismissed titles should not reappear as new
const newFindings = await scout.scan(userId, teamId);
const dismissedTitles = dismissed.map((f) => f.title);
const resurfaced = newFindings.filter((f) => dismissedTitles.includes(f.title));
expect(resurfaced.length).toBe(0);
});
it('GET /api/scout/findings returns findings for user', async () => {
const response = await server.inject({
method: 'GET',
url: '/api/scout/findings',
headers: { 'x-test-user-id': userId },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(Array.isArray(body)).toBe(true);
expect(body.length).toBeGreaterThan(0);
});
it('PATCH /api/scout/findings/:id rejects invalid status', async () => {
const findings = await scout.listFindings(userId);
const finding = findings[0];
const response = await server.inject({
method: 'PATCH',
url: `/api/scout/findings/${finding.id}`,
headers: { 'x-test-user-id': userId },
payload: { status: 'invalid' },
});
expect(response.statusCode).toBe(400);
});
});