Skip to content

Commit 5837c69

Browse files
authored
Merge pull request #18 from firstsun-dev/worktree-personality-stale-cache-fix
fix(personality): invalidate stale pre-MBTI cached snapshots
2 parents 9ea41ca + a998458 commit 5837c69

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

server/src/routes/personality.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,55 @@ describe('Personality routes', () => {
103103
testDb.close();
104104
});
105105

106+
describe('GET /api/personality', () => {
107+
it('serves a cached snapshot as-is when its profileVersion matches the current version', async () => {
108+
seedSnapshot('2026-W30', '__all__', {
109+
results_json: JSON.stringify({ profileVersion: 2, sessionCount: 3, marker: 'from-cache' }),
110+
});
111+
112+
const app = createApp();
113+
const res = await app.request('/api/personality?period=2026-W30');
114+
expect(res.status).toBe(200);
115+
const body = await res.json();
116+
expect(body.marker).toBe('from-cache');
117+
});
118+
119+
it('ignores a cached snapshot missing profileVersion (pre-MBTI schema) and recomputes fresh instead', async () => {
120+
// Simulates a snapshot persisted before the cognitiveFunctions + mbti addition —
121+
// no profileVersion field at all, so it JSON.parses fine but is missing fields the
122+
// frontend (MbtiCard, CognitiveFunctionRadarChart) unconditionally reads, which used
123+
// to crash the dashboard. readSnapshot must treat this as a cache miss.
124+
seedSnapshot('2026-W30', '__all__', {
125+
results_json: JSON.stringify({ sessionCount: 3, marker: 'stale-pre-v2-cache' }),
126+
});
127+
seedProject('proj-1', 'alpha');
128+
seedSession('sess-1', 'proj-1', { project_id: 'proj-1', started_at: '2026-07-20T10:00:00Z' });
129+
seedFacets('sess-1');
130+
131+
const app = createApp();
132+
const res = await app.request('/api/personality?period=2026-W30');
133+
expect(res.status).toBe(200);
134+
const body = await res.json();
135+
expect(body.marker).toBeUndefined();
136+
expect(body.profileVersion).toBe(2);
137+
expect(body.mbti).toBeDefined();
138+
expect(body.cognitiveFunctions).toBeDefined();
139+
});
140+
141+
it('ignores a cached snapshot with an old numeric profileVersion (1) and recomputes fresh instead', async () => {
142+
seedSnapshot('2026-W30', '__all__', {
143+
results_json: JSON.stringify({ profileVersion: 1, sessionCount: 3, marker: 'stale-v1-cache' }),
144+
});
145+
146+
const app = createApp();
147+
const res = await app.request('/api/personality?period=2026-W30');
148+
expect(res.status).toBe(200);
149+
const body = await res.json();
150+
expect(body.marker).toBeUndefined();
151+
expect(body.profileVersion).toBe(2);
152+
});
153+
});
154+
106155
describe('GET /api/personality/projects', () => {
107156
it('returns empty array when no projects have facet-analyzed sessions', async () => {
108157
seedProject('proj-1', 'alpha');

server/src/routes/personality.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,23 @@ interface PersonalitySnapshotRow {
131131
facet_count: number;
132132
}
133133

134+
// Current shape's profileVersion (see PersonalityProfile['profileVersion'] in types.ts).
135+
// Snapshots persisted before the cognitiveFunctions + mbti addition were written with
136+
// profileVersion 1 (or no field at all, for rows from before profileVersion existed) and
137+
// are missing those fields entirely — treating them as a cache hit would serve incomplete
138+
// data straight to the frontend (MbtiCard/CognitiveFunctionRadarChart expect them to always
139+
// be present). Guard against that by treating anything below the current version as a miss.
140+
const CURRENT_PROFILE_VERSION = 2;
141+
134142
function readSnapshot(db: ReturnType<typeof getDb>, period: string, projectId: string): PersonalityProfile | null {
135143
const row = db.prepare(
136144
`SELECT * FROM personality_snapshots WHERE period = ? AND project_id = ?`
137145
).get(period, projectId) as PersonalitySnapshotRow | undefined;
138146
if (!row) return null;
139147
try {
140-
return JSON.parse(row.results_json) as PersonalityProfile;
148+
const profile = JSON.parse(row.results_json) as PersonalityProfile;
149+
if (profile.profileVersion !== CURRENT_PROFILE_VERSION) return null;
150+
return profile;
141151
} catch {
142152
return null;
143153
}

0 commit comments

Comments
 (0)