bf5c4e5f00
Takes V1 from code-complete toward the closed beta: - Tests: the suite grows from 5 to 29. handler.test.ts covers route dispatch, guest device-id validation and user creation, JWT provider derivation, the paywall flag pass-through, free/premium mode selection, the assistant-final transcript rule (post-unlock delivery), the 402 abuse cap and its premium exemption, and purchase-verify validation. chat.test.ts covers structured-output parsing, premium masking of analysis_ready, the exact Responses API payload (knowledge base + strict JSON schema + the no-manufactured-suspense rule) and error handling. subscription.test.ts covers the Apple adapter (active/expired/wrong-product/sandbox retry on 21007) and the Google adapter (real RS256 JWT signing against a generated key, token exchange, expiry, 410-gone), with fetch and secrets mocked. - EAS: apps/mobile/eas.json with development/preview/production profiles for TestFlight and Play internal-testing builds. - LAUNCH.md: step-by-step path to 20-50 beta users — AWS deploy and secrets, migration, sign-in providers, store products at $5.99/$49.99, EAS builds, CI role, and the beta measurement plan (retention, dialogue depth, upgrade timing, tester interviews). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118DaxZR36RpnY524vRqx3z
74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../src/secrets.js', () => ({
|
|
getSecret: vi.fn(async () => ({ OPENAI_API_KEY: 'test-key' })),
|
|
}));
|
|
|
|
import { generateReply } from '../src/chat.js';
|
|
|
|
function openAiResponse(reply: string, analysisReady: boolean) {
|
|
return {
|
|
ok: true,
|
|
json: async () => ({
|
|
output: [
|
|
{
|
|
type: 'message',
|
|
content: [
|
|
{
|
|
type: 'output_text',
|
|
text: JSON.stringify({ reply, analysis_ready: analysisReady }),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
text: async () => '',
|
|
};
|
|
}
|
|
|
|
const fetchMock = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
process.env.KNOWLEDGE_DIR = 'knowledge';
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
fetchMock.mockReset();
|
|
});
|
|
|
|
describe('generateReply', () => {
|
|
it('parses structured output and surfaces analysis_ready in free mode', async () => {
|
|
fetchMock.mockResolvedValue(openAiResponse('A transition.', true));
|
|
const result = await generateReply([{ role: 'user', content: 'Hi' }], 'free');
|
|
expect(result).toEqual({ reply: 'A transition.', analysisReady: true });
|
|
});
|
|
|
|
it('never reports analysis_ready in premium mode', async () => {
|
|
fetchMock.mockResolvedValue(openAiResponse('Full analysis.', true));
|
|
const result = await generateReply([{ role: 'user', content: 'Hi' }], 'premium');
|
|
expect(result).toEqual({ reply: 'Full analysis.', analysisReady: false });
|
|
});
|
|
|
|
it('sends the knowledge base and a strict JSON schema to the Responses API', async () => {
|
|
fetchMock.mockResolvedValue(openAiResponse('Ok.', false));
|
|
await generateReply([{ role: 'user', content: 'Hi' }], 'free');
|
|
|
|
const [url, init] = fetchMock.mock.calls[0] as [string, { body: string }];
|
|
expect(url).toBe('https://api.openai.com/v1/responses');
|
|
const payload = JSON.parse(init.body) as {
|
|
instructions: string;
|
|
text: { format: { type: string; strict: boolean } };
|
|
};
|
|
expect(payload.instructions).toContain('Semantika');
|
|
expect(payload.instructions).toContain('Never manufacture suspense');
|
|
expect(payload.instructions).toContain('# Neurosemantics');
|
|
expect(payload.text.format.type).toBe('json_schema');
|
|
expect(payload.text.format.strict).toBe(true);
|
|
});
|
|
|
|
it('throws on a non-ok response', async () => {
|
|
fetchMock.mockResolvedValue({ ok: false, status: 500, text: async () => 'boom' });
|
|
await expect(generateReply([{ role: 'user', content: 'Hi' }], 'free')).rejects.toThrow(
|
|
'OpenAI request failed',
|
|
);
|
|
});
|
|
});
|