From badb9dfea096c99c75ee958f5e08d4b31567f3a4 Mon Sep 17 00:00:00 2001 From: Michael Ryaboy Date: Wed, 23 Sep 2026 20:29:01 -0700 Subject: [PATCH 1/3] Restore public chat with classification tools --- .github/workflows/check.yml | 7 ++ e2e/chat.mjs | 137 +++++++++++++++++++++++++++++++++++ e2e/spending.mjs | 2 +- package.json | 1 + src/chatui.ts | 1 + src/home.ts | 8 +- src/index.ts | 19 +++-- test/chat.test.ts | 10 +-- tests/public-pricing.test.ts | 6 +- tests/spending.e2e.test.ts | 4 +- 10 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 e2e/chat.mjs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1e0123f..86ccb41 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -46,6 +46,13 @@ jobs: run: node .github/render-wrangler.mjs - name: Build application run: npm run build + - name: Verify public chat and classification tools + run: npm run test:e2e:chat + - name: Save chat verification evidence + uses: actions/upload-artifact@v4 + with: + name: chat-e2e + path: captures/chat-e2e.json - name: Verify spending through the built Worker and real Durable Objects run: npm run test:e2e:spending - name: Verify classification usage responses diff --git a/e2e/chat.mjs b/e2e/chat.mjs new file mode 100644 index 0000000..f37dff8 --- /dev/null +++ b/e2e/chat.mjs @@ -0,0 +1,137 @@ +import { Miniflare, convertV4MiniflareOptions, Response as WorkerResponse } from 'miniflare'; +import { mkdir, writeFile, readdir } from 'node:fs/promises'; +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +const report = { runtime: 'built Worker and real rate-limiter Durable Object', upstream: 'deterministic provider fixtures; no live inference', results: [] }; +let modelCalls = 0, classifications = 0; +const modules = (await readdir('dist/server', { recursive: true })).filter(p => /\.(js|wasm)$/.test(p)).sort((a, b) => a === 'index.js' ? -1 : b === 'index.js' ? 1 : a.localeCompare(b)).map(p => ({ type: p.endsWith('.wasm') ? 'CompiledWasm' : 'ESModule', path: `dist/server/${p}` })); +const sse = delta => new WorkerResponse(`data: ${JSON.stringify({ choices: [{ delta }] })}\n\ndata: [DONE]\n\n`, { headers: { 'content-type': 'text/event-stream' } }); +const mf = new Miniflare(convertV4MiniflareOptions({ workers: [{ name: 'chat', modules, modulesRoot: 'dist/server', compatibilityDate: '2026-08-01', compatibilityFlags: ['nodejs_compat'], + durableObjects: { FREE_BUDGET: { className: 'FreeBudget', useSQLite: true }, LIMITER: { className: 'RateLimiter', useSQLite: true } }, kvNamespaces: ['STATS'], + bindings: { SPENDING_ENABLED: 'true', PRIVACY_SALT: 'fixture', SPUR_API_KEY: 'fixture', TYPESAFE_API_KEY: 'fixture', OPENROUTER_API_KEY: 'fixture', ENTERPRISE_API_KEY: 'fixture-enterprise' }, + outboundService: async request => { + if (request.url.startsWith('https://api.spur.us/')) return WorkerResponse.json({}); + const body = await request.json(); + if (request.url.includes('openrouter.ai')) { + modelCalls++; + const last = body.messages.at(-1); + if (last.content === 'fail upstream') return new WorkerResponse('unavailable', { status: 503 }); + if (last.content === 'slow reply') await new Promise(resolve => setTimeout(resolve, 1000)); + if (last.role === 'user') return sse({ tool_calls: [{ index: 0, id: 'call_1', function: { name: 'classify_texts', arguments: JSON.stringify({ inputs: ['Please refund my invoice'], labels: ['billing', 'support'] }) } }] }); + assert.equal(last.role, 'tool'); + assert.match(last.content, /billing\t0.90/); + return sse({ content: 'The message is billing (90% confidence).' }); + } + assert.ok(request.url.includes('typesafe.ai'), `Unexpected upstream: ${request.url}`); + classifications++; + return WorkerResponse.json({ model: 'jev-1.13.0', usage: { input_tokens: 100, output_tokens: 0 }, answers: Object.fromEntries(Object.entries(body.questions).map(([id, question]) => { + const keys = Object.keys(question.criteria); + return [id, { choice: keys[0], confidence: 0.9, probabilities: { [keys[0]]: 0.9, [keys[1]]: 0.1 } }]; + })) }); + }, +}] })); +const post = (messages, ip = '203.0.113.10', path = '/v1/chat') => mf.dispatchFetch(`https://classifier.dev${path}`, { + method: 'POST', headers: { 'content-type': 'application/json', 'cf-connecting-ip': ip }, body: JSON.stringify({ messages }), +}); +const message = content => [{ role: 'user', content }]; +const events = async response => (await response.text()).split('\n\n').filter(line => line.startsWith('data: ')).map(line => JSON.parse(line.slice(6))); +try { + await mf.ready; + for (const path of ['/', '/chat', '/docs', '/benchmark']) { + const response = await mf.dispatchFetch(`https://classifier.dev${path}`, { headers: { accept: 'text/html' } }); + assert.equal(response.status, 200, path); + const html = await response.text(); + assert.match(html, /data-chat-open/); + assert.match(html, /id="chat" aria-label="Chat"/); + if (path === '/chat') assert.match(html, /id="chat" aria-label="Chat">/); + for (const script of html.matchAll(/