fix: preserve insight producer outages

This commit is contained in:
Sai-karthik
2026-07-15 17:30:47 +00:00
parent 7caf05f4f5
commit 171495f1e1
3 changed files with 73 additions and 3 deletions
+50
View File
@@ -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 readyQscore: QscoreReadResult<QscoreProxyResult> = { status: "ready", data: qscoreFixture() };
const history = { status: "ready" as const, data: [{ rq_score: 0, date: "2026-07-15" }] }; 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`); 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({ const malformedA2a = await buildHomeInsightSnapshots({
userId: "user_fixture", userId: "user_fixture",
qscore: readyQscore, qscore: readyQscore,
+4 -1
View File
@@ -74,4 +74,7 @@ cross-read.
fabricated gap count. fabricated gap count.
All product-service calls are concurrent, bounded, and failure-isolated. A 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.
+19 -2
View File
@@ -226,13 +226,27 @@ function unavailableWhenNoUsableFallback(
label: string, label: string,
unit: string, unit: string,
): InsightSnapshot { ): 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, { return snapshot("unavailable", source, label, unit, {
reason: read.reason, reason: read.reason,
detail: fallback.detail, 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 { function a2aData(read: ProducerRead, action: string): ProducerRead {
if (read.status === "unavailable") return read; if (read.status === "unavailable") return read;
if (read.data.status !== "completed" || !Array.isArray(read.data.messages)) { if (read.data.status !== "completed" || !Array.isArray(read.data.messages)) {
@@ -280,7 +294,7 @@ export async function buildHomeInsightSnapshots(input: {
qscoreHistory: QscoreReadResult<QscoreHistoryPoint[]>; qscoreHistory: QscoreReadResult<QscoreHistoryPoint[]>;
recentEvents: InsightEvent[]; recentEvents: InsightEvent[];
}, dependencies: ProducerDependencies = defaultDependencies): Promise<HomeInsightSnapshots> { }, 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.interviewPageState(input.userId)),
readProducer(() => dependencies.roleplayPageState(input.userId)), readProducer(() => dependencies.roleplayPageState(input.userId)),
readProducer(() => dependencies.resumeState(input.userId)), readProducer(() => dependencies.resumeState(input.userId)),
@@ -289,6 +303,9 @@ export async function buildHomeInsightSnapshots(input: {
readProducer(() => dependencies.courseSummary(input.userId)), readProducer(() => dependencies.courseSummary(input.userId)),
readProducer(() => dependencies.assessments(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 qscoreData = input.qscore.status === "ready" ? input.qscore.data : null;
const historyDetail = input.qscoreHistory.status === "ready" const historyDetail = input.qscoreHistory.status === "ready"