fix onboarding GET service network and rev10 serialization
This commit is contained in:
@@ -47,4 +47,4 @@ networks:
|
||||
name: matchmaking-service_default
|
||||
user-service-staging:
|
||||
external: true
|
||||
name: growqr-app_growqr-net
|
||||
name: user-service-staging_default
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"test:passive-actions": "tsx scripts/passive-actions.test.ts",
|
||||
"test:curator-static": "tsx scripts/curator-static-registry.test.ts",
|
||||
"test:curator-reconcile": "tsx scripts/curator-persisted-reconcile.test.ts",
|
||||
"test:onboarding-read": "tsx scripts/onboarding-rev10-read.test.ts",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"workflows:smoke": "tsx src/workflows/smoke-test.ts",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"onboarding": {
|
||||
"import": {
|
||||
"method": "manual",
|
||||
"status": "not_started",
|
||||
"resume_id": null,
|
||||
"linkedin_url": null,
|
||||
"resume_summary": null,
|
||||
"resume_filename": null,
|
||||
"linkedin_profile_id": null
|
||||
},
|
||||
"status": "completed",
|
||||
"consent": {
|
||||
"accepted_at": "2026-07-12T21:31:40.189Z",
|
||||
"terms_version": "2026-07",
|
||||
"privacy_accepted": true
|
||||
},
|
||||
"profile": {
|
||||
"icp": "experienced",
|
||||
"mode": "individual",
|
||||
"intent": "grow",
|
||||
"question_branch": "professional"
|
||||
},
|
||||
"progress": {
|
||||
"stage": "reveal",
|
||||
"updated_at": "2026-07-12T21:33:01.914Z",
|
||||
"branch_index": 4
|
||||
},
|
||||
"revision": 10,
|
||||
"responses": {
|
||||
"target_role": "Senior Software Engineer",
|
||||
"target_field": "technical",
|
||||
"work_context": "clients",
|
||||
"career_barriers": ["offers"],
|
||||
"desired_outcomes": ["role"],
|
||||
"experience_level": "5+",
|
||||
"target_milestone": null,
|
||||
"venture_industry": null,
|
||||
"current_situation": "employed",
|
||||
"weekly_time_commitment": "steady"
|
||||
},
|
||||
"qx_estimate": 100,
|
||||
"completed_at": "2026-07-12T21:31:42.957Z",
|
||||
"access_choice": "trial",
|
||||
"schema_version": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { serializeOnboardingState } from "../src/services/onboarding-serialization.js";
|
||||
|
||||
type StoredFixture = { onboarding: Record<string, unknown> };
|
||||
const fixturePath = fileURLToPath(new URL("./fixtures/onboarding-rev10.json", import.meta.url));
|
||||
const fixture = JSON.parse(readFileSync(fixturePath, "utf8")) as StoredFixture;
|
||||
const response = serializeOnboardingState("user_3EX1LG3gBk3KY6kfD9PiTkWubs4", fixture.onboarding);
|
||||
|
||||
assert.equal(response.id, "user_3EX1LG3gBk3KY6kfD9PiTkWubs4");
|
||||
assert.equal(response.revision, 10);
|
||||
assert.equal(response.data.revision, 10);
|
||||
assert.equal(response.data.status, "completed");
|
||||
assert.equal(response.data.profile.icp, "experienced");
|
||||
assert.equal(response.data.access_choice, "trial");
|
||||
assert.equal(response.data.completed_at, "2026-07-12T21:31:42.957Z");
|
||||
assert.equal(response.updatedAt, "2026-07-12T21:33:01.914Z");
|
||||
assert.deepEqual(response.payload, {
|
||||
schema_version: 1,
|
||||
source_revision: 10,
|
||||
primary_intent: "grow",
|
||||
mode: "individual",
|
||||
question_branch: "professional",
|
||||
onboarding_icp: "experienced",
|
||||
curator_registry_icp: null,
|
||||
target_role: "Senior Software Engineer",
|
||||
target_field: "technical",
|
||||
experience_context: "5+ · clients",
|
||||
goals: ["role"],
|
||||
barriers: ["offers"],
|
||||
priority: "role",
|
||||
weekly_time_commitment: "steady",
|
||||
});
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(response));
|
||||
assert.deepEqual(serialized, response, "GET response must be JSON serializable without losing rev10 state");
|
||||
|
||||
const compose = readFileSync(fileURLToPath(new URL("../docker-compose.override.yml", import.meta.url)), "utf8");
|
||||
assert.match(
|
||||
compose,
|
||||
/user-service-staging:\s+external:\s+true\s+name:\s+user-service-staging_default/s,
|
||||
"backend must join the canonical user-service staging network",
|
||||
);
|
||||
assert.doesNotMatch(compose, /name:\s+growqr-app_growqr-net/, "obsolete substitute user network must not be configured");
|
||||
|
||||
console.log("onboarding-rev10-read: fixture serialization and staging network assertions passed");
|
||||
+3
-15
@@ -5,6 +5,7 @@ import { onboarding, users, userStacks, type UserStack } from "../db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { log } from "../log.js";
|
||||
import { fetchUserProfile, patchPreferencesRequest, userServiceTarget } from "../services/user-profile.js";
|
||||
import { serializeOnboardingState } from "../services/onboarding-serialization.js";
|
||||
import {
|
||||
onboardingCompletedAtFromPreferences,
|
||||
} from "../v1/curator/curator-onboarding-loop.js";
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
getLatestValidOnboardingLedgerEvent,
|
||||
recordAndProcessOnboardingCompletion,
|
||||
extractOnboardingData,
|
||||
deriveOnboardingPayload,
|
||||
assertOnboardingRevision,
|
||||
mergeOnboardingPatch,
|
||||
resetOnboardingLedger,
|
||||
@@ -123,17 +123,6 @@ function recordPreferences(profile: Record<string, unknown>): Record<string, unk
|
||||
return isPlainObject(profile.preferences) ? profile.preferences : {};
|
||||
}
|
||||
|
||||
/** Build the { id, data, payload, revision, updatedAt } response shape. */
|
||||
function onboardingStateResponse(userId: string, data: OnboardingData) {
|
||||
const updatedAt = data.progress.updated_at ?? data.completed_at;
|
||||
return {
|
||||
id: userId,
|
||||
data,
|
||||
payload: deriveOnboardingPayload(data),
|
||||
revision: data.revision,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
type OnboardingResetResult =
|
||||
@@ -314,8 +303,7 @@ export function userRoutes() {
|
||||
const userId = c.get("userId");
|
||||
const profile = await fetchUserProfile(c.req.raw);
|
||||
const preferences = recordPreferences(profile);
|
||||
const data = extractOnboardingData(preferences.onboarding);
|
||||
return c.json(onboardingStateResponse(userId, data));
|
||||
return c.json(serializeOnboardingState(userId, preferences.onboarding));
|
||||
});
|
||||
|
||||
app.patch("/onboarding", async (c) => {
|
||||
@@ -389,7 +377,7 @@ export function userRoutes() {
|
||||
}
|
||||
}
|
||||
|
||||
return c.json(onboardingStateResponse(userId, nextData));
|
||||
return c.json(serializeOnboardingState(userId, nextPreferences.onboarding));
|
||||
});
|
||||
|
||||
// DELETE /onboarding — authenticated per-user reset. Reopens the onboarding
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
deriveOnboardingPayload,
|
||||
extractOnboardingData,
|
||||
} from "../events/onboarding-ledger.js";
|
||||
|
||||
/** Build the authenticated GET /users/onboarding response from persisted JSON. */
|
||||
export function serializeOnboardingState(userId: string, stored: unknown) {
|
||||
const data = extractOnboardingData(stored);
|
||||
const payload = deriveOnboardingPayload(data);
|
||||
const updatedAt = data.progress.updated_at ?? data.completed_at;
|
||||
return {
|
||||
id: userId,
|
||||
data,
|
||||
payload,
|
||||
revision: data.revision,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user