- Gate both Redis ingestion paths (canonical + legacy) behind separate default-off flags (GROW_EVENTS_REDIS_ENABLED, LEGACY_SERVICE_REDIS_ENABLED). The redis module is never imported when both flags are off. - Sever legacy URL cascade: interview/roleplay/resume/courses Redis URLs resolve ONLY from their own explicit env vars, never from REDIS_URL or GROW_EVENTS_REDIS_URL. - Export resolveRedisConfig() pure resolver for testability. - Fix onboarding signal ID from 'onboarding.completed_baseline' to 'onboarding.completed' (registry-valid in all v2 formula JSONs). - Ensure interview/roleplay completed events emit at least a single completion signal when session_count/scenario_count is absent (default 1). - Extract computeStreakFromDays() into streak-utils.ts (pure, no DB dep) with defensive dedup so duplicate days never inflate streaks. - Add focused tests: redis-gating, signal-registry membership, count fallback, streak policy (current/longest/duplicate/gap/recovery/7-day), service-ingest projector behavior. - typecheck: 80 pre-existing errors unchanged, 0 new.
69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { extractQscoreSignals } from "../src/events/projectors/qscore-projector.js";
|
|
import type { GrowEventRow } from "../src/db/schema.js";
|
|
|
|
/**
|
|
* Test: Completed interview and roleplay events MUST emit at least one signal
|
|
* even when session_count/scenario_count is absent. A completion event with no
|
|
* count and no rubric data should still produce a single-completion signal.
|
|
*/
|
|
|
|
function event(overrides: Partial<GrowEventRow> & { type: string; source: string; payload: Record<string, unknown> }): GrowEventRow {
|
|
return {
|
|
id: `event-${overrides.type}`,
|
|
userId: "user_test",
|
|
orgId: null,
|
|
source: overrides.source,
|
|
type: overrides.type,
|
|
category: "service",
|
|
occurredAt: new Date("2026-07-09T00:00:00.000Z"),
|
|
receivedAt: new Date("2026-07-09T00:00:01.000Z"),
|
|
mission: overrides.mission ?? null,
|
|
subject: overrides.subject ?? null,
|
|
correlation: overrides.correlation ?? null,
|
|
payload: overrides.payload,
|
|
raw: {},
|
|
dedupeKey: null,
|
|
processingStatus: "pending",
|
|
processingError: null,
|
|
processedAt: null,
|
|
};
|
|
}
|
|
|
|
// ── Interview completed with NO count and NO rubric ──────────────────────────
|
|
const interviewSignals = extractQscoreSignals(event({
|
|
source: "interview-service",
|
|
type: "interview.session.completed",
|
|
payload: {}, // nothing — just a bare completion event
|
|
}));
|
|
|
|
assert.ok(
|
|
interviewSignals.length > 0,
|
|
"Interview completed event with no count/rubric must emit at least one signal",
|
|
);
|
|
|
|
assert.ok(
|
|
interviewSignals.some((s) => s.signalId === "interview.sessions_completed" && s.score > 0),
|
|
"Bare interview completion should emit interview.sessions_completed with positive score",
|
|
);
|
|
|
|
// ── Roleplay completed with NO count and NO rubric ───────────────────────────
|
|
const roleplaySignals = extractQscoreSignals(event({
|
|
source: "roleplay-service",
|
|
type: "roleplay.scenario.completed",
|
|
payload: {}, // nothing — just a bare completion event
|
|
}));
|
|
|
|
assert.ok(
|
|
roleplaySignals.length > 0,
|
|
"Roleplay completed event with no count/rubric must emit at least one signal",
|
|
);
|
|
|
|
assert.ok(
|
|
roleplaySignals.some((s) => s.signalId === "roleplay.scenarios_completed" && s.score > 0),
|
|
"Bare roleplay completion should emit roleplay.scenarios_completed with positive score",
|
|
);
|
|
|
|
console.log("count-fallback tests passed");
|
|
process.exit(0);
|