132 lines
5.1 KiB
TypeScript
132 lines
5.1 KiB
TypeScript
/**
|
|
* D11 (UX-Refactor P4) — dataDir resolution + startup tier read.
|
|
*
|
|
* - resolveDataDir: explicit option > WAGGLE_DATA_DIR env > ~/.waggle. Before
|
|
* D11, startService ignored the env var while local/index.ts, the
|
|
* marketplace installer, and memory-mcp honored it — a custom install split
|
|
* its state across two directories.
|
|
* - readTierFromDataDir: the startup log line's tier source — same
|
|
* config.json contract as GET /api/tier, fails closed to FREE.
|
|
*/
|
|
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { startService, resolveDataDir } from '../src/local/service.js';
|
|
import { readTierFromDataDir } from '../src/middleware/assert-tier.js';
|
|
import type { FastifyInstance } from 'fastify';
|
|
|
|
function makeTmpDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-d11-test-'));
|
|
}
|
|
|
|
const ENV_KEY = 'WAGGLE_DATA_DIR';
|
|
|
|
describe('resolveDataDir (D11)', () => {
|
|
const envBefore = process.env[ENV_KEY];
|
|
afterEach(() => {
|
|
if (envBefore === undefined) delete process.env[ENV_KEY];
|
|
else process.env[ENV_KEY] = envBefore;
|
|
});
|
|
|
|
it('explicit option wins over the env var', () => {
|
|
process.env[ENV_KEY] = 'D:/somewhere/else';
|
|
expect(resolveDataDir('D:/explicit')).toBe('D:/explicit');
|
|
});
|
|
|
|
it('WAGGLE_DATA_DIR is honored when no option is given', () => {
|
|
process.env[ENV_KEY] = 'D:/from-env';
|
|
expect(resolveDataDir()).toBe('D:/from-env');
|
|
});
|
|
|
|
it('defaults to ~/.waggle with neither', () => {
|
|
delete process.env[ENV_KEY];
|
|
expect(resolveDataDir()).toBe(path.join(os.homedir(), '.waggle'));
|
|
});
|
|
|
|
it('a set-but-EMPTY env var falls through to the default (never an empty dataDir)', () => {
|
|
process.env[ENV_KEY] = '';
|
|
expect(resolveDataDir()).toBe(path.join(os.homedir(), '.waggle'));
|
|
});
|
|
});
|
|
|
|
describe('startService honors WAGGLE_DATA_DIR (D11 integration)', () => {
|
|
const envBefore = process.env[ENV_KEY];
|
|
let server: FastifyInstance | undefined;
|
|
let tmp: string | undefined;
|
|
|
|
afterEach(async () => {
|
|
if (envBefore === undefined) delete process.env[ENV_KEY];
|
|
else process.env[ENV_KEY] = envBefore;
|
|
if (server) await server.close();
|
|
server = undefined;
|
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
tmp = undefined;
|
|
});
|
|
|
|
it('boots against the env-pointed directory when no dataDir option is passed', async () => {
|
|
tmp = makeTmpDir();
|
|
const envDir = path.join(tmp, 'env-pointed');
|
|
process.env[ENV_KEY] = envDir;
|
|
const port = 4600 + Math.floor(Math.random() * 400);
|
|
const logSpy = vi.spyOn(console, 'log');
|
|
|
|
try {
|
|
const result = await startService({ port, skipLiteLLM: true });
|
|
server = result.server;
|
|
|
|
// The env-pointed dir got the install, ~/.waggle got nothing new from us.
|
|
expect(fs.existsSync(path.join(envDir, 'personal.mind'))).toBe(true);
|
|
expect(server.localConfig.dataDir).toBe(envDir);
|
|
|
|
// The ratified D11 deliverable is the LOG LINE itself — pin it (review
|
|
// gap: deleting the line kept every gate green).
|
|
const dataDirLine = logSpy.mock.calls
|
|
.map((c) => String(c[0]))
|
|
.find((m) => m.includes('[waggle:service] Data dir:'));
|
|
expect(dataDirLine).toContain(envDir);
|
|
expect(dataDirLine).toMatch(/· tier: (TRIAL|FREE|TEAMS|ENTERPRISE)$/);
|
|
} finally {
|
|
logSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('readTierFromDataDir (D11 startup log tier source)', () => {
|
|
let tmp: string;
|
|
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
|
|
|
function writeConfig(obj: Record<string, unknown>): string {
|
|
tmp = makeTmpDir();
|
|
fs.writeFileSync(path.join(tmp, 'config.json'), JSON.stringify(obj));
|
|
return tmp;
|
|
}
|
|
|
|
it('reads a canonical tier', () => {
|
|
expect(readTierFromDataDir(writeConfig({ tier: 'TEAMS' }))).toBe('TEAMS');
|
|
});
|
|
|
|
it('migrates legacy names (solo/basic/pro all → FREE after the Solo/Team collapse)', () => {
|
|
expect(readTierFromDataDir(writeConfig({ tier: 'solo' }))).toBe('FREE');
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
expect(readTierFromDataDir(writeConfig({ tier: 'basic' }))).toBe('FREE');
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
expect(readTierFromDataDir(writeConfig({ tier: 'pro' }))).toBe('FREE');
|
|
});
|
|
|
|
it('TRIAL downgrades to FREE when expired (effective tier, not raw)', () => {
|
|
const expired = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
expect(readTierFromDataDir(writeConfig({ tier: 'TRIAL', trialStartedAt: expired }))).toBe('FREE');
|
|
});
|
|
|
|
it('fails closed to FREE on missing dir, missing config, malformed JSON, unknown tier', () => {
|
|
expect(readTierFromDataDir(undefined)).toBe('FREE');
|
|
expect(readTierFromDataDir(path.join(os.tmpdir(), 'waggle-d11-nonexistent'))).toBe('FREE');
|
|
tmp = makeTmpDir();
|
|
fs.writeFileSync(path.join(tmp, 'config.json'), '{not json');
|
|
expect(readTierFromDataDir(tmp)).toBe('FREE');
|
|
fs.writeFileSync(path.join(tmp, 'config.json'), JSON.stringify({ tier: 'PLATINUM' }));
|
|
expect(readTierFromDataDir(tmp)).toBe('FREE');
|
|
});
|
|
});
|