This commit is contained in:
129
packages/server/tests/local/onboarding-status.test.ts
Normal file
129
packages/server/tests/local/onboarding-status.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* P4 (UX-Refactor) — server-authoritative onboarding status.
|
||||
*
|
||||
* THE clean-install pin: a freshly-booted server (ensureDefault already seeded
|
||||
* the default workspace, personal mind empty, no flag) must report
|
||||
* completed:false — the workspace-count heuristic this replaces reported the
|
||||
* seeded stub as a "returning user" and silently skipped the wizard for every
|
||||
* brand-new production install (S4 founder flag, confirmed in P4).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { MindDB, FrameStore, SessionStore } from '@waggle/core';
|
||||
import { onboardingRoutes } from '../../src/local/routes/onboarding.js';
|
||||
|
||||
function createTestServer(dataDir: string, db: MindDB, workspaces: Array<{ id: string }>) {
|
||||
const server = Fastify({ logger: false });
|
||||
server.decorate('localConfig', { dataDir });
|
||||
server.decorate('multiMind', { personal: db });
|
||||
server.decorate('workspaceManager', { list: () => workspaces });
|
||||
server.register(onboardingRoutes);
|
||||
return server;
|
||||
}
|
||||
|
||||
describe('GET/POST /api/onboarding (P4 clean-install fix)', () => {
|
||||
let tmp: string;
|
||||
let db: MindDB;
|
||||
let server: ReturnType<typeof Fastify>;
|
||||
// The boot-seeded default workspace is ALWAYS present (ensureDefault).
|
||||
const seededOnly = [{ id: 'default-workspace' }];
|
||||
|
||||
beforeEach(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-onb-test-'));
|
||||
db = new MindDB(':memory:');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.close();
|
||||
db.close();
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('CLEAN INSTALL: seeded default workspace + empty mind + no flag → completed:false', async () => {
|
||||
server = createTestServer(tmp, db, seededOnly);
|
||||
const res = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json()).toEqual({ completed: false, source: 'none' });
|
||||
});
|
||||
|
||||
it('completion flag → completed:true (the POST stamp round-trip)', async () => {
|
||||
server = createTestServer(tmp, db, seededOnly);
|
||||
const post = await server.inject({ method: 'POST', url: '/api/onboarding/complete' });
|
||||
expect(post.statusCode).toBe(200);
|
||||
expect(fs.existsSync(path.join(tmp, 'first-launch.flag'))).toBe(true);
|
||||
|
||||
const res = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(res.json()).toEqual({ completed: true, source: 'flag' });
|
||||
});
|
||||
|
||||
it('legacy evidence: any frame in the personal mind → completed:true', async () => {
|
||||
const sessions = new SessionStore(db);
|
||||
const gop = sessions.create().gop_id;
|
||||
new FrameStore(db).createIFrame(gop, 'Real prior usage.', 'normal');
|
||||
server = createTestServer(tmp, db, seededOnly);
|
||||
|
||||
const res = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(res.json()).toEqual({ completed: true, source: 'legacy-evidence' });
|
||||
});
|
||||
|
||||
it('legacy evidence: user-created workspaces beyond the seeded default → completed:true', async () => {
|
||||
server = createTestServer(tmp, db, [...seededOnly, { id: 'my-real-project' }]);
|
||||
const res = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(res.json()).toEqual({ completed: true, source: 'legacy-evidence' });
|
||||
});
|
||||
|
||||
it('POST is idempotent', async () => {
|
||||
server = createTestServer(tmp, db, seededOnly);
|
||||
await server.inject({ method: 'POST', url: '/api/onboarding/complete' });
|
||||
const second = await server.inject({ method: 'POST', url: '/api/onboarding/complete' });
|
||||
expect(second.statusCode).toBe(200);
|
||||
expect(second.json()).toEqual({ completed: true });
|
||||
});
|
||||
|
||||
it('PENDING LATCH: wizard-origin writes after the first status call never flip it (review chain)', async () => {
|
||||
server = createTestServer(tmp, db, seededOnly);
|
||||
// First status call on the clean install stamps the pending latch.
|
||||
const first = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(first.json()).toEqual({ completed: false, source: 'none' });
|
||||
expect(fs.existsSync(path.join(tmp, 'onboarding-pending.flag'))).toBe(true);
|
||||
|
||||
// The wizard's step-1 profile write (PUT /api/profile → 'User identity:'
|
||||
// frame) — the exact pre-completion evidence the review verified. With
|
||||
// the latch, it must NOT turn the user into a "returning user".
|
||||
const sessions = new SessionStore(db);
|
||||
const gop = sessions.create().gop_id;
|
||||
new FrameStore(db).createIFrame(gop, 'User identity: Name: New User', 'important');
|
||||
|
||||
const after = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(after.json()).toEqual({ completed: false, source: 'pending' });
|
||||
|
||||
// Completion clears the latch and stamps the flag.
|
||||
await server.inject({ method: 'POST', url: '/api/onboarding/complete' });
|
||||
expect(fs.existsSync(path.join(tmp, 'onboarding-pending.flag'))).toBe(false);
|
||||
const done = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(done.json()).toEqual({ completed: true, source: 'flag' });
|
||||
});
|
||||
|
||||
it('the latch never fires for a REAL returning user (evidence wins on first contact)', async () => {
|
||||
const sessions = new SessionStore(db);
|
||||
const gop = sessions.create().gop_id;
|
||||
new FrameStore(db).createIFrame(gop, 'Genuine prior usage.', 'normal');
|
||||
server = createTestServer(tmp, db, seededOnly);
|
||||
|
||||
const res = await server.inject({ method: 'GET', url: '/api/onboarding/status' });
|
||||
expect(res.json()).toEqual({ completed: true, source: 'legacy-evidence' });
|
||||
expect(fs.existsSync(path.join(tmp, 'onboarding-pending.flag'))).toBe(false);
|
||||
});
|
||||
|
||||
it('registration pin: the real server wires onboardingRoutes (drop = red)', () => {
|
||||
const indexSrc = fs.readFileSync(
|
||||
path.resolve(import.meta.dirname, '..', '..', 'src', 'local', 'index.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(indexSrc).toContain("import { onboardingRoutes } from './routes/onboarding.js'");
|
||||
expect(indexSrc).toContain('server.register(onboardingRoutes)');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user