feat(test): conditionally skip database writes during test run
This commit is contained in:
parent
d74cdc445e
commit
51f82100f9
3 changed files with 152 additions and 74 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue