fix onboarding GET service network and rev10 serialization

This commit is contained in:
-Puter
2026-07-13 03:59:13 +05:30
parent 4b652358d3
commit fdc320b995
6 changed files with 117 additions and 16 deletions
+1 -1
View File
@@ -47,4 +47,4 @@ networks:
name: matchmaking-service_default
user-service-staging:
external: true
name: growqr-app_growqr-net
name: user-service-staging_default
+1
View File
@@ -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",
+47
View File
@@ -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
}
}
+47
View File
@@ -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
View File
@@ -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
+18
View File
@@ -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,
};
}