Files
growqr-backend/scripts/passive-actions.test.ts

209 lines
7.9 KiB
TypeScript

import assert from "node:assert/strict";
import { careerTransitionReducer } from "../src/missions/career-transition/reducer.js";
import { interviewToOfferReducer } from "../src/missions/interview-to-offer/reducer.js";
import { personalBrandOpportunityReducer } from "../src/missions/personal-brand-opportunity-engine/reducer.js";
import { promotionReadinessReducer } from "../src/missions/promotion-readiness/reducer.js";
import { salaryNegotiationReducer } from "../src/missions/salary-negotiation-war-room/reducer.js";
import type { GrowActiveMission } from "../src/actors/missions/types.js";
import type { MissionReducer } from "../src/missions/reducer-types.js";
import type { MissionReducerContext } from "../src/missions/reducer-types.js";
function missionFor(missionId: GrowActiveMission["missionId"], actorType: GrowActiveMission["actorType"]): GrowActiveMission {
return {
instanceId: `mission-${missionId}-test`,
missionId,
workflowId: missionId,
title: missionId,
shortTitle: missionId,
status: "active",
progressPercent: 0,
currentStageId: "resume",
actorType,
createdAt: Date.now(),
updatedAt: Date.now(),
};
}
const mission = missionFor("interview-to-offer", "interviewToOfferMissionActor");
function ctxWithMission(activeMission: GrowActiveMission, event: Partial<MissionReducerContext["event"]> & { source: string; type: string; payload?: Record<string, unknown> }): MissionReducerContext {
return {
userId: "user_test",
activeMission,
event: {
id: `event-${activeMission.missionId}-${event.type}`,
userId: "user_test",
orgId: null,
source: event.source,
type: event.type,
category: "service",
occurredAt: new Date(),
receivedAt: new Date(),
mission: event.mission,
subject: null,
correlation: null,
payload: event.payload ?? {},
raw: {},
dedupeKey: null,
processingStatus: "pending",
processingError: null,
processedAt: null,
},
qscoreSignals: [],
insight: {
summary: "test insight",
confidence: "low",
recommendedActions: [],
missionStageHints: [],
},
};
}
function ctx(event: Partial<MissionReducerContext["event"]> & { source: string; type: string; payload?: Record<string, unknown> }): MissionReducerContext {
return ctxWithMission(mission, event);
}
const interviewFeedbackPayload = {
review: {
status: "completed",
overall_score: 72,
weak_areas: ["impact metrics", "ownership clarity"],
proof_gaps: ["no scale numbers"],
story_issues: ["STAR structure is loose"],
summary: "Good direction, but missing measurable proof.",
},
};
const roleplayFeedbackPayload = {
review: {
status: "completed",
weak_areas: ["concision", "objection handling"],
story_gaps: ["needs clearer tradeoff story"],
summary: "Good empathy, but answers need tighter story framing.",
},
};
const reducerCases: Array<{
name: string;
reducer: MissionReducer;
mission: GrowActiveMission;
}> = [
{
name: "interview to offer",
reducer: interviewToOfferReducer,
mission,
},
{
name: "career transition",
reducer: careerTransitionReducer,
mission: missionFor("career-transition", "careerTransitionMissionActor"),
},
{
name: "promotion readiness",
reducer: promotionReadinessReducer,
mission: missionFor("promotion-readiness", "promotionReadinessMissionActor"),
},
{
name: "salary negotiation",
reducer: salaryNegotiationReducer,
mission: missionFor("salary-negotiation-war-room", "salaryNegotiationWarRoomMissionActor"),
},
{
name: "personal brand",
reducer: personalBrandOpportunityReducer,
mission: missionFor("personal-brand-opportunity-engine", "personalBrandOpportunityMissionActor"),
},
];
const resumeResult = interviewToOfferReducer.reduce(ctx({
source: "resume-builder",
type: "resume.analysis.completed",
payload: {
analysis: {
summary: "Strong backend platform project.",
strengths: ["Built an event-driven backend"],
gaps: ["Add impact metrics"],
missing_keywords: ["Kafka", "AWS"],
},
},
}));
const proofPractice = resumeResult.actions.find((action) => action.payload?.passiveAction === "resume_analysis_to_interview_practice");
assert.ok(proofPractice, "resume analysis should create an interview practice passive action");
assert.equal(proofPractice?.serviceId, "interview-service");
assert.equal(proofPractice?.toolName, "interview.configure_practice");
assert.match(String(proofPractice?.payload?.prompt), /event-driven backend/i);
const interviewResult = interviewToOfferReducer.reduce(ctx({
source: "interview-service",
type: "interview.feedback.generated",
payload: interviewFeedbackPayload,
}));
const resumeUpgrade = interviewResult.actions.find((action) => action.payload?.passiveAction === "interview_feedback_to_resume_upgrade");
assert.ok(resumeUpgrade, "interview feedback should create a resume upgrade passive action");
assert.equal(resumeUpgrade?.mode, "approval_required");
assert.equal(resumeUpgrade?.serviceId, "resume-service");
assert.deepEqual(resumeUpgrade?.payload?.missingProof, ["no scale numbers"]);
assert.deepEqual(resumeUpgrade?.payload?.storyIssues, ["STAR structure is loose", "add measurable impact proof"]);
const roleplayResult = interviewToOfferReducer.reduce(ctx({
source: "roleplay-service",
type: "roleplay.feedback.generated",
payload: roleplayFeedbackPayload,
}));
const storyArtifact = roleplayResult.artifacts.find((artifact) => artifact.type === "story_bank_update");
const communicationDrill = roleplayResult.actions.find((action) => action.payload?.passiveAction === "roleplay_feedback_to_communication_drill");
assert.ok(storyArtifact, "roleplay feedback should create a story bank artifact");
assert.ok(communicationDrill, "roleplay feedback should create a communication drill passive action");
assert.equal(communicationDrill?.serviceId, "interview-service");
assert.equal(communicationDrill?.toolName, "interview.configure_practice");
assert.deepEqual(communicationDrill?.payload?.storyIssues, ["needs clearer tradeoff story", "tighten STAR story structure"]);
for (const testCase of reducerCases) {
const reducerResumeResult = testCase.reducer.reduce(ctxWithMission(testCase.mission, {
source: "resume-builder",
type: "resume.analysis.completed",
payload: {
analysis: {
summary: "Strong backend platform project.",
strengths: ["Built an event-driven backend"],
gaps: ["Add impact metrics"],
missing_keywords: ["Kafka", "AWS"],
},
},
}));
assert.ok(
reducerResumeResult.actions.some((action) => action.payload?.passiveAction === "resume_analysis_to_interview_practice"),
`${testCase.name} resume analysis should create an interview practice passive action`,
);
const reducerInterviewResult = testCase.reducer.reduce(ctxWithMission(testCase.mission, {
source: "interview-service",
type: "interview.feedback.generated",
payload: interviewFeedbackPayload,
}));
assert.ok(
reducerInterviewResult.actions.some((action) => action.payload?.passiveAction === "interview_feedback_to_resume_upgrade"),
`${testCase.name} interview feedback should create a resume upgrade passive action`,
);
const reducerRoleplayResult = testCase.reducer.reduce(ctxWithMission(testCase.mission, {
source: "roleplay-service",
type: "roleplay.feedback.generated",
payload: roleplayFeedbackPayload,
}));
assert.ok(
reducerRoleplayResult.actions.some((action) => action.payload?.passiveAction === "roleplay_feedback_to_communication_drill"),
`${testCase.name} roleplay feedback should create a communication drill passive action`,
);
assert.ok(
reducerRoleplayResult.artifacts.some((artifact) => artifact.type === "story_bank_update"),
`${testCase.name} roleplay feedback should create a story bank update artifact`,
);
}
console.log("passive-actions tests passed");
process.exit(0);