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
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) {

View file

@ -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,
}
}

View file

@ -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",