265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
buildCareerReportProjection,
|
|
parseCareerReportDisplay,
|
|
type CareerReportActivity,
|
|
} from "../src/v1/analytics/career-report.js";
|
|
import type { HomeInsightSnapshots, InsightSnapshot } from "../src/v1/analytics/home-insight-snapshot.js";
|
|
import type { QscoreProxyResult } from "../src/services/qscore-proxy.js";
|
|
|
|
const categorySpecs = [
|
|
["learning_credentials", "Learning & Credentials"],
|
|
["resume_presence", "Resume Presence"],
|
|
["linkedin_presence", "LinkedIn Presence"],
|
|
["matching_activity", "Matching Activity"],
|
|
["interview_performance", "Interview Performance"],
|
|
["pathway_progress", "Pathway Progress"],
|
|
["roleplay_performance", "Roleplay Performance"],
|
|
["engagement_consistency", "Engagement & Consistency"],
|
|
["goals_readiness", "Goals & Readiness"],
|
|
];
|
|
|
|
function displayFixture() {
|
|
let signalIndex = 0;
|
|
const categories = categorySpecs.map(([id, label], categoryIndex) => {
|
|
const count = categoryIndex === categorySpecs.length - 1 ? 3 : 8;
|
|
const signals = Array.from({ length: count }, () => {
|
|
const index = signalIndex++;
|
|
return { id: `signal.${index}`, label: `Signal ${index}`, present: index < 4, score: index < 4 ? 50 : 0 };
|
|
});
|
|
return {
|
|
id,
|
|
label,
|
|
input_ids: [`input_${categoryIndex}`],
|
|
weight_percent: 10,
|
|
score: signals.some((signal) => signal.present) ? 50 : 0,
|
|
active: signals.some((signal) => signal.present),
|
|
present_rules: signals.filter((signal) => signal.present).length,
|
|
total_rules: signals.length,
|
|
signals,
|
|
};
|
|
});
|
|
return {
|
|
version: "rq-score-display-v1",
|
|
registered_signal_count: 67,
|
|
active_signal_count: 4,
|
|
categories,
|
|
};
|
|
}
|
|
|
|
function metric(overrides: Partial<InsightSnapshot> = {}): InsightSnapshot {
|
|
return {
|
|
status: "empty",
|
|
source: "fixture",
|
|
value: 0,
|
|
unit: "count",
|
|
label: "Fixture",
|
|
updatedAt: null,
|
|
reason: "no_evidence",
|
|
detail: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const snapshots: HomeInsightSnapshots = {
|
|
rqScore: metric({ status: "ready", source: "qscore.current", value: 61, reason: null }),
|
|
interview: metric({ source: "interview.page-state" }),
|
|
resume: metric({ status: "ready", source: "resume.state", value: 72, reason: null }),
|
|
opportunity: metric({ source: "matchmaking.scout-stats" }),
|
|
pathway: metric({ status: "unsupported", source: "qscore.breakdown.display", value: null, reason: "pathway_signal_contract_missing" }),
|
|
socialBrand: metric({ source: "social.state" }),
|
|
roleplay: metric({ source: "roleplay.page-state" }),
|
|
learning: metric({ source: "courses.course-state" }),
|
|
skillGap: metric({ source: "assessment.list" }),
|
|
};
|
|
|
|
const qscore: QscoreProxyResult = {
|
|
org_id: "growqr",
|
|
user_id: "00000000-0000-4000-8000-000000000000",
|
|
rq_score: 61,
|
|
iq_score: null,
|
|
eq_score: null,
|
|
sq_score: null,
|
|
profession: "",
|
|
formula_version: "v2",
|
|
formula_id: "fixture",
|
|
calculated_at: "2026-07-16T00:00:00Z",
|
|
quotients: {},
|
|
breakdown: { display: displayFixture() },
|
|
ledger_seq_from: null,
|
|
ledger_seq_to: null,
|
|
};
|
|
|
|
const activity: CareerReportActivity = {
|
|
windowDays: 14,
|
|
totalEvents: 0,
|
|
completedEvents: 0,
|
|
openedEvents: 0,
|
|
services: [],
|
|
opportunityEvents: 0,
|
|
};
|
|
|
|
const producerStatuses = {
|
|
interview: { status: "ready" as const, reason: null },
|
|
roleplay: { status: "ready" as const, reason: null },
|
|
resume: { status: "ready" as const, reason: null },
|
|
linkedin: { status: "ready" as const, reason: null },
|
|
matchmaking: { status: "ready" as const, reason: null },
|
|
courses: { status: "ready" as const, reason: null },
|
|
assessment: { status: "ready" as const, reason: null },
|
|
};
|
|
|
|
const report = buildCareerReportProjection({
|
|
generatedAt: "2026-07-16T01:00:00Z",
|
|
profile: {
|
|
status: "ready",
|
|
data: {
|
|
first_name: "Ada",
|
|
last_name: "Lovelace",
|
|
avatar_url: "https://images.example/ada.png",
|
|
updated_at: "2026-07-15T22:00:00Z",
|
|
preferences: {
|
|
onboarding: {
|
|
progress: { updated_at: "2026-07-15T23:00:00Z" },
|
|
responses: {
|
|
target_role: "Platform Engineer",
|
|
target_field: "Technology",
|
|
experience_level: "3-5 years",
|
|
current_situation: "employed",
|
|
desired_outcomes: ["Move into platform engineering"],
|
|
career_barriers: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
qscore: { status: "ready", data: qscore },
|
|
qscoreHistory: { status: "ready", data: [{ date: "2026-07-15", rq_score: 61 }] },
|
|
snapshots,
|
|
producerStatuses,
|
|
activity,
|
|
});
|
|
|
|
assert.equal(report.contractVersion, "career-report-v1");
|
|
assert.deepEqual(report.subject.displayName, {
|
|
status: "ready",
|
|
source: "user-service.profile",
|
|
value: "Ada Lovelace",
|
|
updatedAt: "2026-07-15T22:00:00Z",
|
|
reason: null,
|
|
});
|
|
assert.equal(report.subject.targetRole.value, "Platform Engineer");
|
|
assert.equal(report.subject.barriers.status, "empty");
|
|
assert.deepEqual(report.subject.barriers.value, []);
|
|
assert.equal(report.rqScore.score.value, 61);
|
|
assert.equal(report.rqScore.display.status, "ready");
|
|
assert.equal(report.rqScore.display.value?.registeredSignalCount, 67);
|
|
assert.equal(report.rqScore.display.value?.categories.length, 9);
|
|
assert.deepEqual(report.activity.value, activity);
|
|
assert.equal(report.sources.find((source) => source.id === "grow_events")?.status, "ready");
|
|
|
|
const malformed = structuredClone(displayFixture());
|
|
malformed.registered_signal_count = 66;
|
|
assert.equal(parseCareerReportDisplay(malformed), null, "mislabelled canonical display must be rejected");
|
|
const reportWithMalformedDisplay = buildCareerReportProjection({
|
|
profile: { status: "ready", data: {} },
|
|
qscore: { status: "ready", data: { ...qscore, breakdown: { display: malformed } } },
|
|
qscoreHistory: { status: "ready", data: [] },
|
|
snapshots,
|
|
producerStatuses,
|
|
activity,
|
|
});
|
|
assert.equal(reportWithMalformedDisplay.rqScore.display.status, "unavailable");
|
|
assert.equal(reportWithMalformedDisplay.rqScore.display.value, null);
|
|
assert.equal(reportWithMalformedDisplay.rqScore.display.reason, "display_contract_invalid");
|
|
|
|
const reportWithoutDisplay = buildCareerReportProjection({
|
|
profile: { status: "ready", data: {} },
|
|
qscore: { status: "ready", data: { ...qscore, breakdown: {} } },
|
|
qscoreHistory: { status: "ready", data: [] },
|
|
snapshots,
|
|
producerStatuses,
|
|
activity,
|
|
});
|
|
assert.equal(reportWithoutDisplay.rqScore.display.status, "unsupported");
|
|
assert.equal(reportWithoutDisplay.rqScore.display.value, null);
|
|
assert.equal(reportWithoutDisplay.rqScore.display.reason, "display_contract_missing");
|
|
|
|
const wrongCategoryId = structuredClone(displayFixture());
|
|
wrongCategoryId.categories[0].id = "not_a_canonical_category";
|
|
assert.equal(parseCareerReportDisplay(wrongCategoryId), null, "wrong category ids must be rejected");
|
|
|
|
const wrongCategoryOrder = structuredClone(displayFixture());
|
|
[wrongCategoryOrder.categories[0], wrongCategoryOrder.categories[1]] = [wrongCategoryOrder.categories[1], wrongCategoryOrder.categories[0]];
|
|
assert.equal(parseCareerReportDisplay(wrongCategoryOrder), null, "canonical categories out of order must be rejected");
|
|
|
|
const outOfRangeCategoryScore = structuredClone(displayFixture());
|
|
outOfRangeCategoryScore.categories[0].score = 101;
|
|
assert.equal(parseCareerReportDisplay(outOfRangeCategoryScore), null, "category scores above 100 must be rejected");
|
|
|
|
const outOfRangeSignalScore = structuredClone(displayFixture());
|
|
outOfRangeSignalScore.categories[0].signals[0].score = -1;
|
|
assert.equal(parseCareerReportDisplay(outOfRangeSignalScore), null, "signal scores below zero must be rejected");
|
|
|
|
const outOfRangeWeight = structuredClone(displayFixture());
|
|
outOfRangeWeight.categories[0].weight_percent = 101;
|
|
assert.equal(parseCareerReportDisplay(outOfRangeWeight), null, "category weights above 100 must be rejected");
|
|
|
|
const duplicatedSignal = structuredClone(displayFixture());
|
|
duplicatedSignal.categories[1].signals[0].id = duplicatedSignal.categories[0].signals[0].id;
|
|
assert.equal(parseCareerReportDisplay(duplicatedSignal), null, "signals assigned more than once must be rejected");
|
|
|
|
const categoryCountMismatch = structuredClone(displayFixture());
|
|
categoryCountMismatch.categories[0].present_rules += 1;
|
|
assert.equal(parseCareerReportDisplay(categoryCountMismatch), null, "category count mismatches must be rejected");
|
|
|
|
const malformedOnboarding = buildCareerReportProjection({
|
|
profile: {
|
|
status: "ready",
|
|
data: {
|
|
preferences: {
|
|
onboarding: {
|
|
responses: {
|
|
target_role: 42,
|
|
desired_outcomes: ["Real goal", 7],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
qscore: { status: "ready", data: qscore },
|
|
qscoreHistory: { status: "ready", data: [] },
|
|
snapshots,
|
|
producerStatuses,
|
|
activity,
|
|
});
|
|
assert.equal(malformedOnboarding.subject.targetRole.status, "unavailable");
|
|
assert.equal(malformedOnboarding.subject.targetRole.value, null);
|
|
assert.equal(malformedOnboarding.subject.targetRole.reason, "producer_response_invalid");
|
|
assert.equal(malformedOnboarding.subject.goals.status, "unavailable");
|
|
assert.equal(malformedOnboarding.subject.goals.value, null);
|
|
assert.equal(malformedOnboarding.subject.barriers.status, "empty", "missing optional arrays remain confirmed empty");
|
|
assert.deepEqual(malformedOnboarding.subject.barriers.value, []);
|
|
|
|
const unavailable = buildCareerReportProjection({
|
|
profile: { status: "unavailable", reason: "profile_unavailable" },
|
|
qscore: { status: "unavailable", reason: "request_failed" },
|
|
qscoreHistory: { status: "unavailable", reason: "request_failed" },
|
|
snapshots: Object.fromEntries(
|
|
Object.keys(snapshots).map((key) => [key, metric({ status: "unavailable", value: null, reason: "producer_unavailable" })]),
|
|
) as HomeInsightSnapshots,
|
|
producerStatuses: Object.fromEntries(
|
|
Object.keys(producerStatuses).map((key) => [key, { status: "unavailable", reason: "producer_unavailable" }]),
|
|
) as typeof producerStatuses,
|
|
activity,
|
|
});
|
|
assert.equal(unavailable.subject.displayName.status, "unavailable");
|
|
assert.equal(unavailable.subject.displayName.value, null);
|
|
assert.equal(unavailable.rqScore.score.status, "unavailable");
|
|
assert.equal(unavailable.rqScore.score.value, null);
|
|
assert.equal(unavailable.rqScore.history.value, null);
|
|
assert.equal(unavailable.rqScore.display.value, null);
|
|
|
|
console.log("career report contract: ok");
|