237 lines
9.8 KiB
JavaScript
Executable File
237 lines
9.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const args = new Map();
|
|
for (let i = 2; i < process.argv.length; i += 1) {
|
|
const key = process.argv[i];
|
|
if (!key.startsWith("--")) continue;
|
|
const next = process.argv[i + 1];
|
|
args.set(key.slice(2), next && !next.startsWith("--") ? next : "true");
|
|
if (next && !next.startsWith("--")) i += 1;
|
|
}
|
|
|
|
const baseUrl = (args.get("base-url") || process.env.BACKEND_BASE_URL || "http://127.0.0.1:4000").replace(/\/$/, "");
|
|
const userId = args.get("user-id") || process.env.SMOKE_USER_ID || "registry-write-smoke";
|
|
const iterations = Number(args.get("iterations") || process.env.SMOKE_ITERATIONS || 1);
|
|
const previewTimeoutMs = Number(args.get("preview-timeout-ms") || process.env.SMOKE_PREVIEW_TIMEOUT_MS || 180000);
|
|
const serviceToken = process.env.SERVICE_TOKEN;
|
|
|
|
if (!serviceToken) {
|
|
throw new Error("SERVICE_TOKEN is required for authenticated backend write-flow probes.");
|
|
}
|
|
|
|
function assert(condition, message, detail) {
|
|
if (condition) return;
|
|
const suffix = detail === undefined ? "" : `\n${JSON.stringify(detail, null, 2).slice(0, 3000)}`;
|
|
throw new Error(`${message}${suffix}`);
|
|
}
|
|
|
|
function authHeaders(extra = {}) {
|
|
return {
|
|
authorization: `Bearer ${serviceToken}`,
|
|
"x-growqr-user": userId,
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
async function request(name, path, init = {}, timeoutMs = 90000) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
const started = Date.now();
|
|
try {
|
|
const res = await fetch(`${baseUrl}${path}`, { ...init, signal: controller.signal });
|
|
const text = await res.text();
|
|
let json;
|
|
try {
|
|
json = text ? JSON.parse(text) : {};
|
|
} catch {
|
|
json = undefined;
|
|
}
|
|
const durationMs = Date.now() - started;
|
|
assert(res.ok, `${name} returned HTTP ${res.status}`, { text, durationMs });
|
|
return { json, text, durationMs };
|
|
} catch (error) {
|
|
if (error?.name === "AbortError") {
|
|
const durationMs = Date.now() - started;
|
|
throw new Error(`${name} timed out after ${durationMs}ms`, { cause: error });
|
|
}
|
|
throw error;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function rejectFallbackLike(name, value) {
|
|
if (value && typeof value === "object") {
|
|
assert(!("error" in value), `${name} contains error field`, value);
|
|
assert(!("detail" in value && /internal|fallback|not implemented/i.test(String(value.detail))), `${name} contains error detail`, value);
|
|
}
|
|
const text = JSON.stringify(value).toLowerCase();
|
|
const bad = ["placeholder", "dummy", "not implemented", "fallback"];
|
|
const found = bad.find((needle) => text.includes(needle));
|
|
assert(!found, `${name} contains fallback/error-like marker: ${found}`, value);
|
|
}
|
|
|
|
function outlineOf(json) {
|
|
return Array.isArray(json?.question_outline) ? json.question_outline : json?.prompt_outline;
|
|
}
|
|
|
|
function assertDraftPreview(name, json) {
|
|
rejectFallbackLike(name, json);
|
|
assert(typeof json.session_id === "string" && json.session_id.length > 12, `${name} missing session_id`, json);
|
|
assert(json.status === "draft", `${name} should create draft`, json);
|
|
assert(json.needs_approval === true, `${name} should require approval`, json);
|
|
assert(Array.isArray(outlineOf(json)) && outlineOf(json).length >= 2, `${name} missing generated outline`, json);
|
|
assert(Boolean(json.opening_prompt), `${name} missing opening_prompt`, json);
|
|
assert(Boolean(json.candidate_brief), `${name} missing candidate_brief`, json);
|
|
}
|
|
|
|
function asInterviewQuestions(preview, iteration) {
|
|
return outlineOf(preview).slice(0, 3).map((item, index) => ({
|
|
text: `${String(item.question || item.text || "").replace(/\s+/g, " ").trim()} [write-flow ${iteration}.${index + 1}]`,
|
|
topic: String(item.topic || `Smoke interview ${index + 1}`),
|
|
expected_framework: String(item.expected_framework || "none"),
|
|
}));
|
|
}
|
|
|
|
function asRoleplayPrompts(preview, iteration) {
|
|
return outlineOf(preview).slice(0, 3).map((item, index) => ({
|
|
text: `${String(item.prompt || item.question || item.text || "").replace(/\s+/g, " ").trim()} [write-flow ${iteration}.${index + 1}]`,
|
|
topic: String(item.topic || `Smoke roleplay ${index + 1}`),
|
|
}));
|
|
}
|
|
|
|
async function runInterviewFlow(iteration) {
|
|
const prefix = `[write ${iteration}] interview`;
|
|
const previewPayload = {
|
|
user_id: userId,
|
|
org_id: "growqr",
|
|
persona_id: "emma",
|
|
interview_type: "behavioral",
|
|
duration_minutes: 5,
|
|
context: {
|
|
target_role: "Product Manager",
|
|
company_name: "GrowQR Write Flow",
|
|
difficulty: "medium",
|
|
source: "registry-write-flow",
|
|
personalize: false,
|
|
},
|
|
};
|
|
const preview = await request(`${prefix} preview`, "/services/interview/preview", {
|
|
method: "POST",
|
|
headers: authHeaders({ "content-type": "application/json" }),
|
|
body: JSON.stringify(previewPayload),
|
|
}, previewTimeoutMs);
|
|
assertDraftPreview(`${prefix} preview`, preview.json);
|
|
|
|
const questions = asInterviewQuestions(preview.json, iteration);
|
|
assert(questions.every((item) => item.text.includes("[write-flow")), `${prefix} question edit payload invalid`, questions);
|
|
const edited = await request(`${prefix} questions edit`, "/services/interview/questions", {
|
|
method: "POST",
|
|
headers: authHeaders({ "content-type": "application/json" }),
|
|
body: JSON.stringify({ session_id: preview.json.session_id, questions }),
|
|
});
|
|
rejectFallbackLike(`${prefix} questions edit`, edited.json);
|
|
assert(edited.json?.status === "draft", `${prefix} edit should keep draft status`, edited.json);
|
|
assert(edited.json?.questions_edited === true, `${prefix} edit should mark questions_edited`, edited.json);
|
|
assert(outlineOf(edited.json)?.[0]?.question?.includes("[write-flow"), `${prefix} edited question not persisted`, edited.json);
|
|
|
|
const approved = await request(`${prefix} approve`, "/services/interview/approve", {
|
|
method: "POST",
|
|
headers: authHeaders({ "content-type": "application/json" }),
|
|
body: JSON.stringify({ session_id: preview.json.session_id }),
|
|
});
|
|
rejectFallbackLike(`${prefix} approve`, approved.json);
|
|
assert(approved.json?.status === "configured", `${prefix} approve should configure session`, approved.json);
|
|
assert(approved.json?.approved === true, `${prefix} approve missing approved flag`, approved.json);
|
|
|
|
const review = await request(`${prefix} review`, `/services/interview/review/${encodeURIComponent(preview.json.session_id)}`, {
|
|
headers: authHeaders(),
|
|
}, 15000);
|
|
rejectFallbackLike(`${prefix} review`, review.json);
|
|
assert(review.json?.status === "processing" || typeof review.json?.overall_score === "number", `${prefix} review shape invalid`, review.json);
|
|
|
|
return {
|
|
sessionId: preview.json.session_id,
|
|
reviewStatus: review.json?.status ?? "complete",
|
|
durationsMs: {
|
|
preview: preview.durationMs,
|
|
edit: edited.durationMs,
|
|
approve: approved.durationMs,
|
|
review: review.durationMs,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function runRoleplayFlow(iteration) {
|
|
const prefix = `[write ${iteration}] roleplay`;
|
|
const previewPayload = {
|
|
user_id: userId,
|
|
org_id: "growqr",
|
|
persona_id: "emma",
|
|
duration_minutes: 5,
|
|
roleplay_type: "custom",
|
|
brief: "Practice a concise salary negotiation opening for a product manager offer.",
|
|
metadata: {
|
|
target_role: "Product Manager",
|
|
difficulty: "medium",
|
|
source: "registry-write-flow",
|
|
personalize: false,
|
|
},
|
|
};
|
|
const preview = await request(`${prefix} preview`, "/services/roleplay/preview", {
|
|
method: "POST",
|
|
headers: authHeaders({ "content-type": "application/json" }),
|
|
body: JSON.stringify(previewPayload),
|
|
}, previewTimeoutMs);
|
|
assertDraftPreview(`${prefix} preview`, preview.json);
|
|
|
|
const questions = asRoleplayPrompts(preview.json, iteration);
|
|
assert(questions.every((item) => item.text.includes("[write-flow")), `${prefix} prompt edit payload invalid`, questions);
|
|
const edited = await request(`${prefix} prompt edit`, "/services/roleplay/questions", {
|
|
method: "POST",
|
|
headers: authHeaders({ "content-type": "application/json" }),
|
|
body: JSON.stringify({ session_id: preview.json.session_id, questions }),
|
|
});
|
|
rejectFallbackLike(`${prefix} prompt edit`, edited.json);
|
|
assert(edited.json?.status === "draft", `${prefix} edit should keep draft status`, edited.json);
|
|
assert(edited.json?.questions_edited === true, `${prefix} edit should mark questions_edited`, edited.json);
|
|
assert(outlineOf(edited.json)?.[0]?.prompt?.includes("[write-flow"), `${prefix} edited prompt not persisted`, edited.json);
|
|
|
|
const approved = await request(`${prefix} approve`, "/services/roleplay/approve", {
|
|
method: "POST",
|
|
headers: authHeaders({ "content-type": "application/json" }),
|
|
body: JSON.stringify({ session_id: preview.json.session_id }),
|
|
});
|
|
rejectFallbackLike(`${prefix} approve`, approved.json);
|
|
assert(approved.json?.status === "configured", `${prefix} approve should configure session`, approved.json);
|
|
assert(approved.json?.approved === true, `${prefix} approve missing approved flag`, approved.json);
|
|
|
|
const review = await request(`${prefix} review`, `/services/roleplay/review/${encodeURIComponent(preview.json.session_id)}`, {
|
|
headers: authHeaders(),
|
|
}, 15000);
|
|
rejectFallbackLike(`${prefix} review`, review.json);
|
|
assert(review.json?.status === "processing" || typeof review.json?.overall_score === "number", `${prefix} review shape invalid`, review.json);
|
|
|
|
return {
|
|
sessionId: preview.json.session_id,
|
|
reviewStatus: review.json?.status ?? "complete",
|
|
durationsMs: {
|
|
preview: preview.durationMs,
|
|
edit: edited.durationMs,
|
|
approve: approved.durationMs,
|
|
review: review.durationMs,
|
|
},
|
|
};
|
|
}
|
|
|
|
const results = [];
|
|
for (let i = 1; i <= iterations; i += 1) {
|
|
const interview = await runInterviewFlow(i);
|
|
const roleplay = await runRoleplayFlow(i);
|
|
const result = { iteration: i, interview, roleplay };
|
|
results.push(result);
|
|
console.log(JSON.stringify(result));
|
|
}
|
|
|
|
console.log(JSON.stringify({ ok: true, iterations, results }));
|