feat(test): conditionally skip database writes during test run

This commit is contained in:
Gabriel Ramos 2026-06-09 19:38:24 -04:00
parent d74cdc445e
commit 51f82100f9
3 changed files with 152 additions and 74 deletions

View file

@ -48,54 +48,61 @@ export async function POST(request: NextRequest) {
// Generate candidate embedding // Generate candidate embedding
const embedding = await generateEmbedding(cleanText); 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"; 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 // Call n8n webhook
let n8nResponseData = null; let n8nResponseData = null;
let webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL; const webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL;
if (isTest && webhookUrl) {
webhookUrl = webhookUrl.replace("/webhook/", "/webhook-test/");
}
if (webhookUrl) { if (webhookUrl) {
try { try {
@ -105,11 +112,12 @@ export async function POST(request: NextRequest) {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
candidateId: candidate.id, candidateId,
interviewId: interview.id, interviewId,
candidateName: candidate.name, candidateName,
candidateEmail: email, candidateEmail: email,
text: cleanText, text: cleanText,
isTest,
}), }),
}); });
@ -133,10 +141,9 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
candidateId: candidate.id, candidateId,
interviewId: interview.id, interviewId,
candidateName: candidate.name, candidateName,
candidateEmail: email,
n8nResponse: n8nResponseData, n8nResponse: n8nResponseData,
}); });
} catch (error: unknown) { } catch (error: unknown) {

View file

@ -89,35 +89,57 @@ export async function GET(request: NextRequest) {
const { candidateId, interviewId, n8nResponse } = parseResult; const { candidateId, interviewId, n8nResponse } = parseResult;
// 5. Verification - Poll the Supabase `scores` table to verify n8n updated the DB
let scoreRecord = null; let scoreRecord = null;
let verified = false;
let verificationAttempts = 0; let verificationAttempts = 0;
const maxAttempts = 10;
const delayMs = 1500;
for (let i = 0; i < maxAttempts; i++) { if (testMode) {
verificationAttempts++; // In test mode, we skip DB insertion, so n8n returns the structured response directly.
// Wait for n8n execution to finish and write back // Let's verify that the response contains the expected evaluation structure.
await new Promise((resolve) => setTimeout(resolve, delayMs)); 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 if (hasEvaluation && hasAiScore) {
.from("scores") verified = true;
.select("*") scoreRecord = n8nResponse;
.eq("candidate_id", candidateId) }
.eq("interview_id", interviewId) } else {
.maybeSingle(); // 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) { for (let i = 0; i < maxAttempts; i++) {
scoreRecord = score; verificationAttempts++;
break; 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({ return NextResponse.json({
status: scoreRecord ? "success" : "completed_with_pending_evaluation", status: verified ? "success" : "completed_with_pending_evaluation",
message: scoreRecord message: verified
? "Pipeline test executed and verified successfully!" ? (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 write-back is pending or failed.", : "Pipeline executed but AI evaluation verification failed.",
testDetails: { testDetails: {
jobUsed: { jobUsed: {
id: job.id, id: job.id,
@ -131,7 +153,7 @@ export async function GET(request: NextRequest) {
}, },
verification: { verification: {
attempts: verificationAttempts, attempts: verificationAttempts,
verified: !!scoreRecord, verified,
scoreData: scoreRecord, scoreData: scoreRecord,
} }
} }

View file

@ -371,6 +371,36 @@ async function main() {
position: [700, 300], 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 = { const supabaseInsertNode = {
parameters: { parameters: {
operation: "create", operation: "create",
@ -382,7 +412,7 @@ async function main() {
name: "Insert Score to Supabase", name: "Insert Score to Supabase",
type: "n8n-nodes-base.supabase", type: "n8n-nodes-base.supabase",
typeVersion: 1, typeVersion: 1,
position: [900, 300], position: [1100, 420],
credentials: { credentials: {
supabaseApi: { supabaseApi: {
id: supabaseCredId, id: supabaseCredId,
@ -399,7 +429,7 @@ async function main() {
name: "Respond to Webhook", name: "Respond to Webhook",
type: "n8n-nodes-base.respondToWebhook", type: "n8n-nodes-base.respondToWebhook",
typeVersion: 1.1, typeVersion: 1.1,
position: [1100, 300], position: [1300, 300],
}; };
const wNodes: any[] = [ const wNodes: any[] = [
@ -408,6 +438,7 @@ async function main() {
primaryModelNode, primaryModelNode,
jsonParserNode, jsonParserNode,
setNode, setNode,
checkIfTestNode,
supabaseInsertNode, supabaseInsertNode,
respondWebhookNode, respondWebhookNode,
]; ];
@ -459,6 +490,24 @@ async function main() {
}, },
"Format Evaluation Data": { "Format Evaluation Data": {
main: [ 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", node: "Insert Score to Supabase",