Files
growqr-backend/scripts/count-fallback.test.ts
-Puter ac7bdca912 feat(backend): REST-authoritative qscore pipeline with Redis opt-in
- 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.
2026-07-10 20:12:13 +05:30

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);