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 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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user