From 51f82100f9c56f3a5eaa523e2cc471b48e69e6f9 Mon Sep 17 00:00:00 2001 From: Gabriel Ramos Date: Tue, 9 Jun 2026 19:38:24 -0400 Subject: [PATCH] feat(test): conditionally skip database writes during test run --- .../candidates/api/parse-cv/route.ts | 109 ++++++++++-------- app/webhook-test/route.ts | 64 ++++++---- scripts/deploy-n8n-v2.ts | 53 ++++++++- 3 files changed, 152 insertions(+), 74 deletions(-) diff --git a/app/(dashboard)/candidates/api/parse-cv/route.ts b/app/(dashboard)/candidates/api/parse-cv/route.ts index b4708da..5ba5e73 100644 --- a/app/(dashboard)/candidates/api/parse-cv/route.ts +++ b/app/(dashboard)/candidates/api/parse-cv/route.ts @@ -48,54 +48,61 @@ export async function POST(request: NextRequest) { // Generate candidate embedding const embedding = await generateEmbedding(cleanText); - // Initialize Supabase admin client - const supabase = createServerSupabaseClient(); - - // Insert candidate - const { data: candidate, error: candidateError } = await supabase - .from("candidates") - .insert({ - name, - contact_info: { email, phone }, - embedding, - }) - .select("*") - .single(); - - if (candidateError || !candidate) { - return NextResponse.json( - { error: candidateError?.message || "Failed to insert candidate" }, - { status: 500 } - ); - } - - // Insert an initial interview - const { data: interview, error: interviewError } = await supabase - .from("interviews") - .insert({ - candidate_id: candidate.id, - job_id: jobId, - interview_date: new Date().toISOString(), - stage: "Screening", - }) - .select("*") - .single(); - - if (interviewError || !interview) { - return NextResponse.json( - { error: interviewError?.message || "Failed to insert interview" }, - { status: 500 } - ); - } - const isTest = formData.get("isTest") === "true"; + let candidateId = "00000000-0000-0000-0000-000000000000"; + let interviewId = "00000000-0000-0000-0000-000000000000"; + let candidateName = name; + + if (!isTest) { + // Initialize Supabase admin client + const supabase = createServerSupabaseClient(); + + // Insert candidate + const { data: candidate, error: candidateError } = await supabase + .from("candidates") + .insert({ + name, + contact_info: { email, phone }, + embedding, + }) + .select("*") + .single(); + + if (candidateError || !candidate) { + return NextResponse.json( + { error: candidateError?.message || "Failed to insert candidate" }, + { status: 500 } + ); + } + + // Insert an initial interview + const { data: interview, error: interviewError } = await supabase + .from("interviews") + .insert({ + candidate_id: candidate.id, + job_id: jobId, + interview_date: new Date().toISOString(), + stage: "Screening", + }) + .select("*") + .single(); + + if (interviewError || !interview) { + return NextResponse.json( + { error: interviewError?.message || "Failed to insert interview" }, + { status: 500 } + ); + } + + candidateId = candidate.id; + interviewId = interview.id; + candidateName = candidate.name; + } + // Call n8n webhook let n8nResponseData = null; - let webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL; - if (isTest && webhookUrl) { - webhookUrl = webhookUrl.replace("/webhook/", "/webhook-test/"); - } + const webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL; if (webhookUrl) { try { @@ -105,11 +112,12 @@ export async function POST(request: NextRequest) { "Content-Type": "application/json", }, body: JSON.stringify({ - candidateId: candidate.id, - interviewId: interview.id, - candidateName: candidate.name, + candidateId, + interviewId, + candidateName, candidateEmail: email, text: cleanText, + isTest, }), }); @@ -133,10 +141,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, - candidateId: candidate.id, - interviewId: interview.id, - candidateName: candidate.name, - candidateEmail: email, + candidateId, + interviewId, + candidateName, n8nResponse: n8nResponseData, }); } catch (error: unknown) { diff --git a/app/webhook-test/route.ts b/app/webhook-test/route.ts index d8d3a4a..64f7048 100644 --- a/app/webhook-test/route.ts +++ b/app/webhook-test/route.ts @@ -89,35 +89,57 @@ export async function GET(request: NextRequest) { const { candidateId, interviewId, n8nResponse } = parseResult; - // 5. Verification - Poll the Supabase `scores` table to verify n8n updated the DB let scoreRecord = null; + let verified = false; let verificationAttempts = 0; - const maxAttempts = 10; - const delayMs = 1500; - for (let i = 0; i < maxAttempts; i++) { - verificationAttempts++; - // Wait for n8n execution to finish and write back - await new Promise((resolve) => setTimeout(resolve, delayMs)); + if (testMode) { + // In test mode, we skip DB insertion, so n8n returns the structured response directly. + // Let's verify that the response contains the expected evaluation structure. + const hasEvaluation = n8nResponse && typeof n8nResponse === "object" && "evaluation" in n8nResponse; + const hasAiScore = n8nResponse && typeof n8nResponse === "object" && "ai_score" in n8nResponse; - const { data: score, error: scoreError } = await supabase - .from("scores") - .select("*") - .eq("candidate_id", candidateId) - .eq("interview_id", interviewId) - .maybeSingle(); + if (hasEvaluation && hasAiScore) { + verified = true; + scoreRecord = n8nResponse; + } + } else { + // In live mode, we poll the DB to verify, but we MUST clean it up immediately afterwards + const maxAttempts = 10; + const delayMs = 1500; - if (score && !scoreError) { - scoreRecord = score; - break; + for (let i = 0; i < maxAttempts; i++) { + verificationAttempts++; + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + const { data: score, error: scoreError } = await supabase + .from("scores") + .select("*") + .eq("candidate_id", candidateId) + .eq("interview_id", interviewId) + .maybeSingle(); + + if (score && !scoreError) { + scoreRecord = score; + verified = true; + break; + } + } + + // Cleanup immediately to avoid database clutter! + if (candidateId && candidateId !== "00000000-0000-0000-0000-000000000000") { + console.log(`Cleaning up test candidate: ${candidateId}`); + await supabase.from("scores").delete().eq("candidate_id", candidateId); + await supabase.from("interviews").delete().eq("candidate_id", candidateId); + await supabase.from("candidates").delete().eq("id", candidateId); } } return NextResponse.json({ - status: scoreRecord ? "success" : "completed_with_pending_evaluation", - message: scoreRecord - ? "Pipeline test executed and verified successfully!" - : "Pipeline executed but AI evaluation write-back is pending or failed.", + status: verified ? "success" : "completed_with_pending_evaluation", + message: verified + ? (testMode ? "Pipeline test executed and verified successfully (In-Memory / No DB Write)!" : "Pipeline test executed, verified, and cleaned successfully from DB!") + : "Pipeline executed but AI evaluation verification failed.", testDetails: { jobUsed: { id: job.id, @@ -131,7 +153,7 @@ export async function GET(request: NextRequest) { }, verification: { attempts: verificationAttempts, - verified: !!scoreRecord, + verified, scoreData: scoreRecord, } } diff --git a/scripts/deploy-n8n-v2.ts b/scripts/deploy-n8n-v2.ts index 95f192b..1bc620f 100644 --- a/scripts/deploy-n8n-v2.ts +++ b/scripts/deploy-n8n-v2.ts @@ -371,6 +371,36 @@ async function main() { position: [700, 300], }; + const checkIfTestNode = { + parameters: { + conditions: { + options: { + caseSensitive: true, + leftValue: "", + typeValidation: "loose" + }, + combinator: "and", + conditions: [ + { + id: "is-test-check", + operator: { + name: "filter.operator.equals", + type: "boolean", + operation: "equals" + }, + leftValue: "={{ $('Webhook Trigger').item.json.body.isTest }}", + rightValue: true + } + ] + } + }, + id: "check-if-test", + name: "Check If Test", + type: "n8n-nodes-base.if", + typeVersion: 2.2, + position: [900, 300], + }; + const supabaseInsertNode = { parameters: { operation: "create", @@ -382,7 +412,7 @@ async function main() { name: "Insert Score to Supabase", type: "n8n-nodes-base.supabase", typeVersion: 1, - position: [900, 300], + position: [1100, 420], credentials: { supabaseApi: { id: supabaseCredId, @@ -399,7 +429,7 @@ async function main() { name: "Respond to Webhook", type: "n8n-nodes-base.respondToWebhook", typeVersion: 1.1, - position: [1100, 300], + position: [1300, 300], }; const wNodes: any[] = [ @@ -408,6 +438,7 @@ async function main() { primaryModelNode, jsonParserNode, setNode, + checkIfTestNode, supabaseInsertNode, respondWebhookNode, ]; @@ -459,6 +490,24 @@ async function main() { }, "Format Evaluation Data": { main: [ + [ + { + node: "Check If Test", + type: "main", + index: 0, + }, + ], + ], + }, + "Check If Test": { + main: [ + [ + { + node: "Respond to Webhook", + type: "main", + index: 0, + }, + ], [ { node: "Insert Score to Supabase",