fix: preserve insight producer outages
This commit is contained in:
@@ -49,6 +49,18 @@ function qscoreFixture(): QscoreProxyResult {
|
||||
};
|
||||
}
|
||||
|
||||
function inactiveCategoryQscoreFixture(): QscoreProxyResult {
|
||||
const fixture = qscoreFixture();
|
||||
const display = fixture.breakdown.display as { categories: Array<Record<string, unknown>> };
|
||||
for (const category of display.categories) {
|
||||
if (category.id === "pathway_progress") continue;
|
||||
category.score = 0;
|
||||
category.active = false;
|
||||
category.present_rules = 0;
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
const readyQscore: QscoreReadResult<QscoreProxyResult> = { status: "ready", data: qscoreFixture() };
|
||||
const history = { status: "ready" as const, data: [{ rq_score: 0, date: "2026-07-15" }] };
|
||||
|
||||
@@ -155,6 +167,44 @@ for (const [key, value] of Object.entries(unavailable)) {
|
||||
assert.equal(value.value, null, `${key} outage must not become zero`);
|
||||
}
|
||||
|
||||
const inactiveQscore: QscoreReadResult<QscoreProxyResult> = {
|
||||
status: "ready",
|
||||
data: inactiveCategoryQscoreFixture(),
|
||||
};
|
||||
const primaryOutageWithNoQscoreEvidence = await buildHomeInsightSnapshots({
|
||||
userId: "user_fixture",
|
||||
qscore: inactiveQscore,
|
||||
qscoreHistory: history,
|
||||
recentEvents: [],
|
||||
}, {
|
||||
interviewPageState: failed,
|
||||
roleplayPageState: failed,
|
||||
resumeState: failed,
|
||||
socialState: failed,
|
||||
matchmakingStats: failed,
|
||||
courseSummary: failed,
|
||||
assessments: failed,
|
||||
});
|
||||
|
||||
for (const key of ["interview", "resume", "opportunity", "socialBrand", "roleplay", "learning", "skillGap"] as const) {
|
||||
assert.equal(primaryOutageWithNoQscoreEvidence[key].status, "unavailable", `${key} outage must not be masked by inactive qscore evidence`);
|
||||
assert.equal(primaryOutageWithNoQscoreEvidence[key].value, null, `${key} outage with inactive qscore must not become zero`);
|
||||
}
|
||||
|
||||
const malformedCourseA2a = await buildHomeInsightSnapshots({
|
||||
userId: "user_fixture",
|
||||
qscore: inactiveQscore,
|
||||
qscoreHistory: history,
|
||||
recentEvents: [],
|
||||
}, {
|
||||
...producers,
|
||||
courseSummary: async () => ({ status: "completed", messages: [] }),
|
||||
});
|
||||
assert.equal(malformedCourseA2a.learning.status, "unavailable");
|
||||
assert.equal(malformedCourseA2a.learning.value, null);
|
||||
assert.equal(malformedCourseA2a.learning.reason, "producer_response_invalid");
|
||||
assert.equal(malformedCourseA2a.learning.source, "courses.course-state");
|
||||
|
||||
const malformedA2a = await buildHomeInsightSnapshots({
|
||||
userId: "user_fixture",
|
||||
qscore: readyQscore,
|
||||
|
||||
@@ -74,4 +74,7 @@ cross-read.
|
||||
fabricated gap count.
|
||||
|
||||
All product-service calls are concurrent, bounded, and failure-isolated. A
|
||||
single unavailable producer does not fail the full snapshot response.
|
||||
single unavailable producer does not fail the full snapshot response. A direct
|
||||
producer failure may fall back only to an active, `ready` canonical qscore
|
||||
category. An inactive category with `no_category_evidence` never masks an
|
||||
outage as an empty numeric zero.
|
||||
|
||||
@@ -226,13 +226,27 @@ function unavailableWhenNoUsableFallback(
|
||||
label: string,
|
||||
unit: string,
|
||||
): InsightSnapshot {
|
||||
if (read.status === "ready" || fallback.status === "ready" || fallback.reason === "no_category_evidence") return fallback;
|
||||
if (read.status === "ready" || fallback.status === "ready") return fallback;
|
||||
return snapshot("unavailable", source, label, unit, {
|
||||
reason: read.reason,
|
||||
detail: fallback.detail,
|
||||
});
|
||||
}
|
||||
|
||||
function sessionPageData(read: ProducerRead): ProducerRead {
|
||||
if (read.status === "unavailable") return read;
|
||||
return Array.isArray(read.data.recent_sessions)
|
||||
? read
|
||||
: { status: "unavailable", reason: "producer_response_invalid" };
|
||||
}
|
||||
|
||||
function socialStateData(read: ProducerRead): ProducerRead {
|
||||
if (read.status === "unavailable") return read;
|
||||
return typeof read.data.linkedin_connected === "boolean"
|
||||
? read
|
||||
: { status: "unavailable", reason: "producer_response_invalid" };
|
||||
}
|
||||
|
||||
function a2aData(read: ProducerRead, action: string): ProducerRead {
|
||||
if (read.status === "unavailable") return read;
|
||||
if (read.data.status !== "completed" || !Array.isArray(read.data.messages)) {
|
||||
@@ -280,7 +294,7 @@ export async function buildHomeInsightSnapshots(input: {
|
||||
qscoreHistory: QscoreReadResult<QscoreHistoryPoint[]>;
|
||||
recentEvents: InsightEvent[];
|
||||
}, dependencies: ProducerDependencies = defaultDependencies): Promise<HomeInsightSnapshots> {
|
||||
const [interviewRead, roleplayRead, resumeRead, socialRead, matchmakingRaw, courseRaw, assessmentRead] = await Promise.all([
|
||||
const [interviewRaw, roleplayRaw, resumeRead, socialRaw, matchmakingRaw, courseRaw, assessmentRead] = await Promise.all([
|
||||
readProducer(() => dependencies.interviewPageState(input.userId)),
|
||||
readProducer(() => dependencies.roleplayPageState(input.userId)),
|
||||
readProducer(() => dependencies.resumeState(input.userId)),
|
||||
@@ -289,6 +303,9 @@ export async function buildHomeInsightSnapshots(input: {
|
||||
readProducer(() => dependencies.courseSummary(input.userId)),
|
||||
readProducer(() => dependencies.assessments(input.userId)),
|
||||
]);
|
||||
const interviewRead = sessionPageData(interviewRaw);
|
||||
const roleplayRead = sessionPageData(roleplayRaw);
|
||||
const socialRead = socialStateData(socialRaw);
|
||||
|
||||
const qscoreData = input.qscore.status === "ready" ? input.qscore.data : null;
|
||||
const historyDetail = input.qscoreHistory.status === "ready"
|
||||
|
||||
Reference in New Issue
Block a user