fix(onboarding): never switch a module off on a record that already uses it

The derivation wrote an explicit off for every module the answers did not
name, which is right for a record being set up and wrong for one with years of
rows behind it. An established self-hoster has, for most modules, no stored
preference at all — they were default-on and nobody ever touched the toggle —
so the first confirm took away the navigation entry, the dashboard widget and
the settings entry for domains full of content. The rows survived; the
surfaces did not.

A derived off now asks first, and only for the modules it would actually
retract. Ownership is not re-answered: the measurement domains come from the
same map the MCP reads and the coach snapshot gates off, and each remaining
module names the one table its content lives in, exhaustively, so a module
added later fails to compile until somebody says where its content is. A
module whose domain holds rows keeps whatever the record already had, because
absence is what "on" looks like in the stored map.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 11:48:27 +02:00
1 parent cc6e6ca221
commit 2d2350252f
6 files changed
+372 -5

No files matched your search

@@ -331,6 +331,44 @@ describe("POST /api/onboarding/complete", () => {
expect((await modulePrefs(user.id)).mcp).toBe(true);
});
it("never switches off a domain the record already holds rows in", async () => {
const user = await makeUser("established");
await signIn(user.id);
// An account that has been running for years: mood entries and workouts,
// no stored module preference at all, because both were default-on and
// nobody ever touched a toggle. The questions below name neither.
await getPrismaClient().moodEntry.create({
data: {
userId: user.id,
date: "2026-09-01",
mood: "GUT",
score: 4,
moodLoggedAt: new Date("2026-09-01T18:00:00.000Z"),
},
});
await getPrismaClient().workout.create({
data: {
userId: user.id,
sportType: "RUNNING",
startedAt: new Date("2026-09-01T06:00:00.000Z"),
endedAt: new Date("2026-09-01T06:30:00.000Z"),
durationSec: 1800,
},
});
await answerTheQuestions();
await postComplete();
const prefs = await modulePrefs(user.id);
// Left alone rather than written false: absence is what "on" looks like
// in the stored map, and the surfaces stay.
expect(prefs).not.toHaveProperty("mood");
expect(prefs).not.toHaveProperty("workouts");
// A domain with nothing in it is still switched off by the answers.
expect(prefs.illness).toBe(false);
expect(prefs.vaccinations).toBe(false);
});
it("switches cycle tracking on through its own column when the area is chosen", async () => {
const user = await makeUser("cycle");
await signIn(user.id);