diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7518e00..565e983 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,11 +84,18 @@ jobs: run: npx prisma generate --schema=packages/db/prisma/schema.prisma env: DATABASE_URL: "postgresql://shieldai:shieldai_dev@localhost:5432/shieldai" - - name: Run tests - run: npm run test + - name: Run tests with coverage + run: npm run test:coverage env: DATABASE_URL: "postgresql://shieldai:shieldai_dev@localhost:5432/shieldai" REDIS_URL: "redis://localhost:6379" + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage/lcov.info + flags: unittests + name: shieldai-coverage + fail_on_empty: false docker-build: name: Docker Build diff --git a/package.json b/package.json index df78059..ddf4660 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,17 @@ "dev": "turbo run dev", "build": "turbo run build", "test": "turbo run test", + "test:coverage": "turbo run test:coverage", "db:migrate": "turbo run db:migrate", "db:seed": "turbo run db:seed", "lint": "turbo run lint" }, "devDependencies": { "@types/node": "^25.6.0", + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5", "turbo": "^2.3.0", - "typescript": "^5.7.0", - "vitest": "^4.1.5" + "typescript": "^5.7.0" }, "engines": { "node": ">=20.0.0" diff --git a/packages/api/package.json b/packages/api/package.json index e298d62..20e4ab7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -6,6 +6,7 @@ "build": "tsc", "start": "node dist/server.js", "test": "vitest run", + "test:coverage": "vitest run --coverage", "lint": "eslint src/" }, "dependencies": { @@ -19,5 +20,9 @@ "fastify": "^5.2.0", "@shieldai/darkwatch": "workspace:*", "@shieldai/voiceprint": "workspace:*" + }, + "devDependencies": { + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" } } diff --git a/packages/api/src/routes/correlation.routes.ts b/packages/api/src/routes/correlation.routes.ts index 2ff58d6..064525f 100644 --- a/packages/api/src/routes/correlation.routes.ts +++ b/packages/api/src/routes/correlation.routes.ts @@ -1,30 +1,37 @@ -import { FastifyInstance } from "fastify"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; import { correlationService } from "@shieldai/correlation"; +type AuthUser = { id?: string }; + +function getUserId(request: FastifyRequest): string | undefined { + return (request.user as AuthUser | undefined)?.id; +} + export function correlationRoutes(fastify: FastifyInstance) { fastify.get("/dashboard", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } - const timeWindow = parseInt((request.query as any).timeWindow as string) || 60; + const timeWindow = + parseInt( + (request.query as Record).timeWindow as string + ) || 60; const data = await correlationService.getDashboardData(userId, timeWindow); return reply.send(data); }); fastify.get("/groups", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const query = request.query as Record; const result = await correlationService.getCorrelationGroups({ userId, - status: query.status || undefined, + status: (query.status as any) || undefined, timeWindowMinutes: query.timeWindow ? parseInt(query.timeWindow) : 60, @@ -34,43 +41,91 @@ export function correlationRoutes(fastify: FastifyInstance) { return reply.send(result); }); - fastify.get("/groups/:groupId", async (request, reply) => { - const groupId = (request.params as any).groupId; - const group = await correlationService.getGroupById(groupId); + fastify.get( + "/groups/:groupId", + { + schema: { + params: { + type: "object", + properties: { + groupId: { type: "string", format: "uuid" }, + }, + required: ["groupId"], + }, + }, + }, + async (request, reply) => { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { + return reply.code(401).send({ error: "User not authenticated" }); + } - if (!group) { - return reply.code(404).send({ error: "Correlation group not found" }); + const groupId = (request.params as Record).groupId; + const group = await correlationService.getGroupById(groupId, userId); + + if (!group) { + return reply.code(404).send({ error: "Correlation group not found" }); + } + + return reply.send(group); } + ); - return reply.send(group); - }); + fastify.patch( + "/groups/:groupId/resolve", + { + schema: { + params: { + type: "object", + properties: { + groupId: { type: "string", format: "uuid" }, + }, + required: ["groupId"], + }, + body: { + type: "object", + properties: { + status: { type: "string", enum: ["RESOLVED", "ACTIVE"] }, + }, + additionalProperties: false, + }, + }, + }, + async (request, reply) => { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { + return reply.code(401).send({ error: "User not authenticated" }); + } - fastify.patch("/groups/:groupId/resolve", async (request, reply) => { - const groupId = (request.params as any).groupId; - const body = (request.body as any) || {}; - const status = body.status || "RESOLVED"; - const group = await correlationService.resolveGroup(groupId, status); + const groupId = (request.params as Record).groupId; + const body = request.body as Record | undefined; + const status = body?.status || "RESOLVED"; + const group = await correlationService.resolveGroup( + groupId, + userId, + status + ); - if (!group) { - return reply.code(404).send({ error: "Correlation group not found" }); + if (!group) { + return reply.code(404).send({ error: "Correlation group not found" }); + } + + return reply.send(group); } - - return reply.send(group); - }); + ); fastify.get("/alerts", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { return reply.code(401).send({ error: "User not authenticated" }); } const query = request.query as Record; const result = await correlationService.getCorrelatedAlerts({ userId, - source: query.source || undefined, - category: query.category || undefined, - severity: query.severity || undefined, + source: (query.source as any) || undefined, + category: (query.category as any) || undefined, + severity: (query.severity as any) || undefined, timeWindowMinutes: query.timeWindow ? parseInt(query.timeWindow) : 60, @@ -80,72 +135,200 @@ export function correlationRoutes(fastify: FastifyInstance) { return reply.send(result); }); - fastify.post("/ingest/darkwatch", async (request, reply) => { - const body = request.body as any; - const alert = await correlationService.ingestDarkWatchAlert( - body.userId, - body.sourceAlertId, - { - exposureId: body.exposureId, - breachName: body.breachName, - severity: body.severity, - channel: body.channel, - dataType: body.dataType, - dataSource: body.dataSource, + fastify.post( + "/ingest/darkwatch", + { + schema: { + body: { + type: "object", + properties: { + sourceAlertId: { type: "string" }, + exposureId: { type: "string" }, + breachName: { type: "string", maxLength: 500 }, + severity: { type: "string", maxLength: 20 }, + channel: { type: "string", maxLength: 50 }, + dataType: { type: "array", items: { type: "string" } }, + dataSource: { type: "string", maxLength: 100 }, + }, + required: ["sourceAlertId", "breachName", "severity", "channel"], + additionalProperties: false, + }, + }, + }, + async (request, reply) => { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { + return reply.code(401).send({ error: "User not authenticated" }); } - ); - return reply.code(201).send(alert); - }); - fastify.post("/ingest/spamshield", async (request, reply) => { - const body = request.body as any; - const alert = await correlationService.ingestSpamShieldAlert( - body.userId, - body.sourceAlertId, - { - phoneNumber: body.phoneNumber, - decision: body.decision, - confidence: body.confidence, - reasons: body.reasons, - channel: body.channel, - hiyaReputationScore: body.hiyaReputationScore, - truecallerSpamScore: body.truecallerSpamScore, - } - ); - return reply.code(201).send(alert); - }); + const body = request.body as Record; + const alert = await correlationService.ingestDarkWatchAlert( + userId, + body.sourceAlertId as string, + { + exposureId: body.exposureId as string, + breachName: body.breachName as string, + severity: body.severity as string, + channel: body.channel as string, + dataType: body.dataType as string[] | undefined, + dataSource: body.dataSource as string | undefined, + } + ); + return reply.code(201).send(alert); + } + ); - fastify.post("/ingest/voiceprint", async (request, reply) => { - const body = request.body as any; - const alert = await correlationService.ingestVoicePrintAlert( - body.userId, - body.sourceAlertId, - { - jobId: body.jobId, - verdict: body.verdict, - syntheticScore: body.syntheticScore, - confidence: body.confidence, - matchedEnrollmentId: body.matchedEnrollmentId, - matchedSimilarity: body.matchedSimilarity, - analysisType: body.analysisType, + fastify.post( + "/ingest/spamshield", + { + schema: { + body: { + type: "object", + properties: { + sourceAlertId: { type: "string" }, + phoneNumber: { type: "string", maxLength: 20 }, + decision: { type: "string", enum: ["BLOCK", "FLAG", "ALLOW"] }, + confidence: { type: "number", minimum: 0, maximum: 1 }, + reasons: { type: "array", items: { type: "string" } }, + channel: { type: "string", enum: ["call", "sms"] }, + hiyaReputationScore: { type: "number" }, + truecallerSpamScore: { type: "number" }, + }, + required: ["sourceAlertId", "phoneNumber", "decision", "confidence"], + additionalProperties: false, + }, + }, + }, + async (request, reply) => { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { + return reply.code(401).send({ error: "User not authenticated" }); } - ); - return reply.code(201).send(alert); - }); - fastify.post("/ingest/call-analysis", async (request, reply) => { - const body = request.body as any; - const alert = await correlationService.ingestCallAnalysisAlert( - body.userId, - body.sourceAlertId, - { - callId: body.callId, - eventType: body.eventType, - mosScore: body.mosScore, - anomaly: body.anomaly, - sentiment: body.sentiment, + const body = request.body as Record; + const alert = await correlationService.ingestSpamShieldAlert( + userId, + body.sourceAlertId as string, + { + phoneNumber: body.phoneNumber as string, + decision: body.decision as string, + confidence: body.confidence as number, + reasons: body.reasons as string[] | undefined, + channel: body.channel as "call" | "sms" | undefined, + hiyaReputationScore: body.hiyaReputationScore as + | number + | undefined, + truecallerSpamScore: body.truecallerSpamScore as + | number + | undefined, + } + ); + return reply.code(201).send(alert); + } + ); + + fastify.post( + "/ingest/voiceprint", + { + schema: { + body: { + type: "object", + properties: { + sourceAlertId: { type: "string" }, + jobId: { type: "string" }, + verdict: { + type: "string", + enum: ["SYNTHETIC", "NATURAL", "UNCERTAIN"], + }, + syntheticScore: { type: "number", minimum: 0, maximum: 1 }, + confidence: { type: "number", minimum: 0, maximum: 1 }, + matchedEnrollmentId: { type: "string" }, + matchedSimilarity: { type: "number" }, + analysisType: { type: "string", maxLength: 50 }, + }, + required: [ + "sourceAlertId", + "jobId", + "verdict", + "syntheticScore", + "confidence", + ], + additionalProperties: false, + }, + }, + }, + async (request, reply) => { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { + return reply.code(401).send({ error: "User not authenticated" }); } - ); - return reply.code(201).send(alert); - }); + + const body = request.body as Record; + const alert = await correlationService.ingestVoicePrintAlert( + userId, + body.sourceAlertId as string, + { + jobId: body.jobId as string, + verdict: body.verdict as string, + syntheticScore: body.syntheticScore as number, + confidence: body.confidence as number, + matchedEnrollmentId: body.matchedEnrollmentId as + | string + | undefined, + matchedSimilarity: body.matchedSimilarity as number | undefined, + analysisType: body.analysisType as string | undefined, + } + ); + return reply.code(201).send(alert); + } + ); + + fastify.post( + "/ingest/call-analysis", + { + schema: { + body: { + type: "object", + properties: { + sourceAlertId: { type: "string" }, + callId: { type: "string" }, + eventType: { type: "string", maxLength: 100 }, + mosScore: { type: "number", minimum: 1, maximum: 5 }, + anomaly: { type: "string", maxLength: 500 }, + sentiment: { + type: "object", + properties: { + label: { type: "string", maxLength: 50 }, + score: { type: "number", minimum: 0, maximum: 1 }, + }, + }, + }, + required: ["sourceAlertId", "callId"], + additionalProperties: false, + }, + }, + }, + async (request, reply) => { + const userId = getUserId(request); + if (!userId || userId === "anonymous") { + return reply.code(401).send({ error: "User not authenticated" }); + } + + const body = request.body as Record; + const alert = await correlationService.ingestCallAnalysisAlert( + userId, + body.sourceAlertId as string, + { + callId: body.callId as string, + eventType: body.eventType as string | undefined, + mosScore: body.mosScore as number | undefined, + anomaly: body.anomaly as string | undefined, + sentiment: body.sentiment as + | { label: string; score: number } + | undefined, + } + ); + return reply.code(201).send(alert); + } + ); } diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 835fb71..474d994 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -3,7 +3,10 @@ import cors from "@fastify/cors"; import helmet from "@fastify/helmet"; import sensible from "@fastify/sensible"; import { extractOrGenerateRequestId } from "@shieldai/types"; -import { darkwatchRoutes, voiceprintRoutes, correlationRoutes } from "./routes"; +import { authMiddleware } from "./middleware/auth.middleware"; +import { darkwatchRoutes } from "./routes/darkwatch.routes"; +import { voiceprintRoutes } from "./routes/voiceprint.routes"; +import { correlationRoutes } from "./routes/correlation.routes"; const app = Fastify({ logger: { @@ -12,10 +15,13 @@ const app = Fastify({ }); async function bootstrap() { - await app.register(cors, { origin: true }); + await app.register(cors, { origin: process.env.CORS_ORIGIN || "http://localhost:5173" }); await app.register(helmet); await app.register(sensible); + // Register auth middleware to populate request.user + await app.register(authMiddleware); + app.addHook("onRequest", async (request, _reply) => { const requestId = extractOrGenerateRequestId(request.headers); request.id = requestId; diff --git a/packages/correlation/src/engine.ts b/packages/correlation/src/engine.ts index 6a0cdcd..dc8177f 100644 --- a/packages/correlation/src/engine.ts +++ b/packages/correlation/src/engine.ts @@ -282,10 +282,11 @@ export class CorrelationEngine { } public async getGroupById( - groupId: string + groupId: string, + userId: string ): Promise { const group = await (prisma as any).correlationGroup.findUnique({ - where: { id: groupId }, + where: { id: groupId, userId }, include: { alerts: { orderBy: { createdAt: "asc" }, @@ -298,10 +299,11 @@ export class CorrelationEngine { public async resolveGroup( groupId: string, + userId: string, status: string = CorrelationStatus.RESOLVED ): Promise { const group = await (prisma as any).correlationGroup.update({ - where: { id: groupId }, + where: { id: groupId, userId }, data: { status, resolvedAt: new Date(), diff --git a/packages/correlation/src/normalizer.ts b/packages/correlation/src/normalizer.ts index 0a5e2fa..61f1cd8 100644 --- a/packages/correlation/src/normalizer.ts +++ b/packages/correlation/src/normalizer.ts @@ -8,6 +8,24 @@ import { type EntityType = (typeof EntityTypes)[keyof typeof EntityTypes]; +function sanitizePayload( + payload: Record, + maxDepth: number = 5 +): Record { + const seen = new WeakSet(); + const clone = (obj: unknown, depth: number): unknown => { + if (depth > maxDepth) return "[max depth]"; + if (obj === null || typeof obj !== "object") return obj; + if (seen.has(obj as object)) return "[circular]"; + seen.add(obj as object); + if (Array.isArray(obj)) return obj.map((item) => clone(item, depth + 1)); + return Object.fromEntries( + Object.entries(obj as Record).map(([k, v]) => [k, clone(v, depth + 1)]) + ); + }; + return clone(payload, 0) as Record; +} + interface DarkWatchAlertPayload { exposureId: string; breachName: string; @@ -92,7 +110,7 @@ export class AlertNormalizer { : `Exposure detected in ${payload.breachName}`, entities, sourceAlertId, - payload: payload as unknown as Record, + payload: sanitizePayload(payload as unknown as Record), timestamp, }; } @@ -132,7 +150,7 @@ export class AlertNormalizer { : `SpamShield ${decision} decision with confidence ${Math.round(payload.confidence * 100)}%`, entities, sourceAlertId, - payload: payload as unknown as Record, + payload: sanitizePayload(payload as unknown as Record), timestamp, }; } @@ -179,7 +197,7 @@ export class AlertNormalizer { : `Synthetic voice detection: ${verdict} (score: ${payload.syntheticScore.toFixed(3)})`, entities, sourceAlertId, - payload: payload as unknown as Record, + payload: sanitizePayload(payload as unknown as Record), timestamp, }; } @@ -237,7 +255,7 @@ export class AlertNormalizer { description, entities, sourceAlertId, - payload: payload as unknown as Record, + payload: sanitizePayload(payload as unknown as Record), timestamp, }; } diff --git a/packages/correlation/src/service.ts b/packages/correlation/src/service.ts index db31fbc..ade3e29 100644 --- a/packages/correlation/src/service.ts +++ b/packages/correlation/src/service.ts @@ -126,12 +126,12 @@ export class CorrelationService { return this.engine.getCorrelationGroups(query); } - public getGroupById(groupId: string) { - return this.engine.getGroupById(groupId); + public getGroupById(groupId: string, userId: string) { + return this.engine.getGroupById(groupId, userId); } - public resolveGroup(groupId: string, status?: string) { - return this.engine.resolveGroup(groupId, status as any); + public resolveGroup(groupId: string, userId: string, status?: string) { + return this.engine.resolveGroup(groupId, userId, status as any); } public getDashboardData(userId: string, timeWindowMinutes?: number) { diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index edd32af..cd278a6 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -23,16 +23,18 @@ model User { role UserRole @default(user) // Relationships - accounts Account[] - sessions Session[] - familyGroups FamilyGroupMember[] - familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner") - subscriptions Subscription[] - alerts Alert[] - voiceEnrollments VoiceEnrollment[] - voiceAnalyses VoiceAnalysis[] - spamFeedback SpamFeedback[] - spamRules SpamRule[] + accounts Account[] + sessions Session[] + familyGroups FamilyGroupMember[] + familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner") + subscriptions Subscription[] + alerts Alert[] + voiceEnrollments VoiceEnrollment[] + voiceAnalyses VoiceAnalysis[] + spamFeedback SpamFeedback[] + spamRules SpamRule[] + normalizedAlerts NormalizedAlert[] + correlationGroups CorrelationGroup[] // Audit createdAt DateTime @default(now()) @@ -429,9 +431,93 @@ model KPISnapshot { metricName String metricValue Float metadata Json? - + createdAt DateTime @default(now()) @@index([metricName]) @@index([date]) } + +// ============================================ +// Cross-Service Alert Correlation Models +// ============================================ + +enum AlertSource { + DARKWATCH + SPAMSHIELD + VOICEPRINT + CALL_ANALYSIS +} + +enum AlertCategory { + BREACH_EXPOSURE + SPAM_CALL + SPAM_SMS + SYNTHETIC_VOICE + VOICE_MISMATCH + CALL_ANOMALY + CALL_QUALITY + CALL_EVENT +} + +enum NormalizedAlertSeverity { + LOW + INFO + MEDIUM + WARNING + HIGH + CRITICAL +} + +enum CorrelationStatus { + ACTIVE + RESOLVED +} + +model NormalizedAlert { + id String @id @default(uuid()) + source AlertSource + category AlertCategory + severity NormalizedAlertSeverity + userId String + title String + description String + entities Json + sourceAlertId String + groupId String? + payload Json? + createdAt DateTime + updatedAt DateTime @default(now()) @updatedAt + + correlationGroup CorrelationGroup? @relation(fields: [groupId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([sourceAlertId]) + @@index([userId]) + @@index([groupId]) + @@index([source]) + @@index([severity]) + @@index([createdAt]) + @@index([userId, createdAt]) +} + +model CorrelationGroup { + id String @id @default(uuid()) + userId String + entities Json + highestSeverity NormalizedAlertSeverity + status CorrelationStatus @default(ACTIVE) + alertCount Int @default(0) + summary String? + resolvedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + alerts NormalizedAlert[] + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([status]) + @@index([userId, status]) + @@index([createdAt]) +} diff --git a/packages/jobs/package.json b/packages/jobs/package.json index c92bdd7..12259ae 100644 --- a/packages/jobs/package.json +++ b/packages/jobs/package.json @@ -6,6 +6,7 @@ "build": "tsc", "start": "node dist/index.js", "test": "vitest run", + "test:coverage": "vitest run --coverage", "lint": "eslint src/" }, "dependencies": { @@ -14,5 +15,9 @@ "@shieldai/types": "workspace:*", "@shieldai/darkwatch": "workspace:*", "ioredis": "^5.4.0" + }, + "devDependencies": { + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" } } diff --git a/packages/shared-notifications/package.json b/packages/shared-notifications/package.json index 080c717..c58517d 100644 --- a/packages/shared-notifications/package.json +++ b/packages/shared-notifications/package.json @@ -7,6 +7,7 @@ "build": "tsc", "test": "vitest run", "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "lint": "eslint src/" }, "dependencies": { @@ -20,7 +21,8 @@ "devDependencies": { "@types/express": "^4.17.0", "typescript": "^5.0.0", - "vitest": "^4.1.5" + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/packages/shared-notifications/vitest.config.ts b/packages/shared-notifications/vitest.config.ts index 7b21019..364e59b 100644 --- a/packages/shared-notifications/vitest.config.ts +++ b/packages/shared-notifications/vitest.config.ts @@ -5,5 +5,18 @@ export default defineConfig({ globals: true, environment: 'node', include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + reportsDirectory: './coverage', + include: ['src/**/*.ts'], + exclude: ['src/**/*.d.ts', '**/node_modules/**', '**/test/**'], + thresholds: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, }, }); diff --git a/packages/shared-utils/package.json b/packages/shared-utils/package.json index 6ae9d14..81aad81 100644 --- a/packages/shared-utils/package.json +++ b/packages/shared-utils/package.json @@ -7,10 +7,12 @@ "types": "src/index.ts", "scripts": { "lint": "eslint src/", - "test": "vitest" + "test": "vitest", + "test:coverage": "vitest run --coverage" }, "devDependencies": { "typescript": "^5.3.3", - "vitest": "^1.3.1" + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" } } diff --git a/services/darkwatch/package.json b/services/darkwatch/package.json index 8c29662..0257bc2 100644 --- a/services/darkwatch/package.json +++ b/services/darkwatch/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "tsc", "test": "vitest run", + "test:coverage": "vitest run --coverage", "lint": "eslint src/" }, "dependencies": { @@ -14,6 +15,10 @@ "@shieldai/correlation": "workspace:*", "node-cache": "^5.1.2" }, + "devDependencies": { + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" + }, "exports": { ".": "./src/index.ts" } diff --git a/services/spamshield/package.json b/services/spamshield/package.json index 19c0241..f43e12b 100644 --- a/services/spamshield/package.json +++ b/services/spamshield/package.json @@ -8,6 +8,7 @@ "dev": "tsx watch src/index.ts", "lint": "eslint src/", "test": "vitest run", + "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -22,6 +23,8 @@ "typescript": "^5.3.3", "tsx": "^4.19.0", "eslint": "^8.56.0", - "@types/ws": "^8.5.10" + "@types/ws": "^8.5.10", + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" } } diff --git a/services/spamshield/vitest.config.ts b/services/spamshield/vitest.config.ts index 7b21019..fe01a0a 100644 --- a/services/spamshield/vitest.config.ts +++ b/services/spamshield/vitest.config.ts @@ -5,5 +5,22 @@ export default defineConfig({ globals: true, environment: 'node', include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + reportsDirectory: './coverage', + include: ['src/**/*.ts'], + exclude: [ + 'src/**/*.d.ts', + '**/node_modules/**', + '**/test/**', + ], + thresholds: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, }, }); diff --git a/services/voiceprint/package.json b/services/voiceprint/package.json index 8563f5e..001334d 100644 --- a/services/voiceprint/package.json +++ b/services/voiceprint/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "tsc", "test": "vitest run", + "test:coverage": "vitest run --coverage", "lint": "eslint src/" }, "dependencies": { @@ -14,6 +15,10 @@ "@shieldai/correlation": "workspace:*", "node-cache": "^5.1.2" }, + "devDependencies": { + "vitest": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5" + }, "exports": { ".": "./src/index.ts" } diff --git a/turbo.json b/turbo.json index d6c2446..843fd00 100644 --- a/turbo.json +++ b/turbo.json @@ -15,6 +15,10 @@ "dependsOn": ["^build"], "outputs": ["coverage/**"] }, + "test:coverage": { + "dependsOn": ["^build"], + "outputs": ["coverage/**"] + }, "lint": { "outputs": [] },