fix(roleplay): hydrate qscore with canonical identity

This commit is contained in:
-Puter
2026-07-14 22:41:32 +05:30
parent d2353273d5
commit d6f583f238
2 changed files with 94 additions and 7 deletions
+74
View File
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import { buildPersonalizedRoleplayConfigurePayload } from "../src/routes/services.js";
import { toQscoreUserId } from "../src/services/qscore-proxy.js";
const clerkUserId = "user_3GTcs6PBFcUrXUP6bH6U8dcwXBq";
const originalFetch = globalThis.fetch;
const requests: Request[] = [];
globalThis.fetch = (async (input, init) => {
const request = new Request(input, init);
requests.push(request);
return new Response(
JSON.stringify({
org_id: "growqr",
user_id: toQscoreUserId(clerkUserId),
rq_score: 61,
iq_score: 60,
eq_score: 62,
sq_score: 61,
profession: "General",
formula_version: "v1",
formula_id: "default",
calculated_at: "2026-07-14T00:00:00Z",
quotients: {},
breakdown: {},
ledger_seq_from: null,
ledger_seq_to: null,
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}) as typeof fetch;
try {
const payload = await buildPersonalizedRoleplayConfigurePayload(
new Request("http://backend.test/roleplay/preview"),
{
scenario_type: "salary-negotiation",
metadata: { difficulty: "medium" },
},
clerkUserId,
{
resolveUserContext: async () => ({ clerk_id: clerkUserId }),
},
);
assert.equal(requests.length, 1);
const qscoreUrl = new URL(requests[0]!.url);
assert.equal(qscoreUrl.pathname, `/v1/qscore/${toQscoreUserId(clerkUserId)}`);
assert.equal(qscoreUrl.searchParams.get("org_id"), "growqr");
assert.equal((payload.qscore as Record<string, unknown>).rq_score, 61);
assert.equal(payload.user_id, clerkUserId);
requests.length = 0;
const suppliedPayload = await buildPersonalizedRoleplayConfigurePayload(
new Request("http://backend.test/roleplay/preview"),
{
qscore: { rq_score: 74, source: "caller" },
metadata: {},
},
clerkUserId,
{
resolveUserContext: async () => ({
clerk_id: clerkUserId,
qscore: { rq_score: 68, source: "context" },
}),
},
);
assert.equal(requests.length, 0);
assert.deepEqual(suppliedPayload.qscore, { rq_score: 74, source: "caller" });
console.log("roleplay QScore identity: all assertions passed");
} finally {
globalThis.fetch = originalFetch;
}
+20 -7
View File
@@ -668,9 +668,17 @@ async function buildPersonalizedConfigurePayload(req: Request, body: JsonObject,
};
}
async function buildPersonalizedRoleplayConfigurePayload(req: Request, body: JsonObject, userId: string): Promise<JsonObject> {
export async function buildPersonalizedRoleplayConfigurePayload(
req: Request,
body: JsonObject,
userId: string,
dependencies: {
resolveUserContext?: typeof resolveGrowUserContext;
loadQscore?: typeof getQscoreFromService;
} = {},
): Promise<JsonObject> {
const { mission: _mission, ...rest } = body;
const userContext = await resolveGrowUserContext(req, userId).catch((err) => {
const userContext = await (dependencies.resolveUserContext ?? resolveGrowUserContext)(req, userId).catch((err) => {
log.warn({ err, userId }, "failed to resolve Grow user context for roleplay configure");
return {} as Record<string, unknown>;
});
@@ -692,14 +700,19 @@ async function buildPersonalizedRoleplayConfigurePayload(req: Request, body: Jso
metadata.context_notes = getString(incomingMetadata.context_notes) ?? candidateProfile;
}
const orgId = String(rest.org_id ?? "growqr");
const suppliedQscore = isRecord(rest.qscore)
? rest.qscore
: isRecord(userContext.qscore)
? userContext.qscore
: undefined;
const qscore = suppliedQscore ?? await (dependencies.loadQscore ?? getQscoreFromService)(userId, orgId);
return {
...rest,
user_id: String(rest.user_id ?? userId),
org_id: String(rest.org_id ?? "growqr"),
metadata,
...((rest.qscore as JsonObject | undefined) ?? (isRecord(userContext.qscore) ? userContext.qscore : undefined)
? { qscore: (rest.qscore as JsonObject | undefined) ?? userContext.qscore }
: {}),
org_id: orgId,
...(qscore ? { qscore } : {}),
user_context: userContext,
};
}