Spaces:
Sleeping
Sleeping
| // Production check against the deployed Space. Uses the real HF token. | |
| // PROD_URL=https://lvwerra-interactive-editor.hf.space HF_TOKEN=hf_xxx node test/prod.js | |
| import assert from 'node:assert' | |
| import * as Y from 'yjs' | |
| import { HocuspocusProvider, HocuspocusProviderWebsocket } from '@hocuspocus/provider' | |
| import WS from 'ws' | |
| const BASE = process.env.PROD_URL || 'https://lvwerra-interactive-editor.hf.space' | |
| const TOKEN = process.env.HF_TOKEN | |
| if (!TOKEN) throw new Error('HF_TOKEN required') | |
| const HANDLE = 'prod-smoke-agent' | |
| // The Space is private: the ws upgrade must carry the HF token for the proxy. | |
| class AuthedWS extends WS { | |
| constructor(url, protocols) { | |
| super(url, protocols, { headers: { authorization: `Bearer ${TOKEN}` } }) | |
| } | |
| } | |
| const api = (path, { method = 'GET', body } = {}) => | |
| fetch(BASE + path, { | |
| method, | |
| headers: { authorization: `Bearer ${TOKEN}`, ...(body ? { 'content-type': 'application/json' } : {}) }, | |
| body: body ? JSON.stringify(body) : undefined, | |
| }).then(r => r.json()) | |
| async function waitFor(fn, what, ms = 20000) { | |
| const t0 = Date.now() | |
| while (Date.now() - t0 < ms) { | |
| try { | |
| if (await fn()) return | |
| } catch {} | |
| await new Promise(r => setTimeout(r, 250)) | |
| } | |
| throw new Error(`timeout waiting for ${what}`) | |
| } | |
| async function main() { | |
| const me = await api('/api/me') | |
| assert.ok(me.user?.username, 'authenticated: ' + JSON.stringify(me)) | |
| console.log('✓ authenticated as', me.user.username) | |
| const { id: docId } = await api('/api/docs', { method: 'POST', body: { title: 'Prod Smoke' } }) | |
| assert.ok(docId) | |
| console.log('✓ doc created:', docId) | |
| const doc = new Y.Doc() | |
| const socket = new HocuspocusProviderWebsocket({ | |
| url: BASE.replace('https', 'wss') + '/collab', | |
| WebSocketPolyfill: AuthedWS, | |
| }) | |
| const provider = new HocuspocusProvider({ websocketProvider: socket, name: docId, document: doc, token: TOKEN }) | |
| provider.attach() | |
| const frag = doc.getXmlFragment('default') | |
| await waitFor(() => frag.length >= 2, 'ws sync') | |
| console.log('✓ websocket collab sync through the Space proxy') | |
| const reg = await api('/api/agents', { method: 'POST', body: { handle: HANDLE } }) | |
| assert.equal(reg.ok, true, JSON.stringify(reg)) | |
| assert.ok(reg.key?.startsWith('ak_'), 'agent key issued') | |
| const AGENT = reg.key | |
| const agentApi = (path, opts = {}) => | |
| fetch(BASE + path, { | |
| method: opts.method || 'GET', | |
| headers: { authorization: `Bearer ${AGENT}`, ...(opts.body ? { 'content-type': 'application/json' } : {}) }, | |
| body: opts.body ? JSON.stringify(opts.body) : undefined, | |
| }).then(r => r.json()) | |
| const threadId = 'prodth' + Math.random().toString(36).slice(2, 8) | |
| doc.transact(() => { | |
| const messages = new Y.Array() | |
| messages.push([{ id: 'pm1', author: me.user.username, authorType: 'user', text: `@${HANDLE} check this`, ts: Date.now() }]) | |
| const t = new Y.Map() | |
| t.set('id', threadId) | |
| t.set('anchorStart', b64(Y.createRelativePositionFromTypeIndex(frag, 1))) | |
| t.set('anchorEnd', b64(Y.createRelativePositionFromTypeIndex(frag, 2))) | |
| t.set('excerpt', 'prod excerpt') | |
| t.set('resolved', false) | |
| t.set('createdBy', me.user.username) | |
| t.set('createdAt', Date.now()) | |
| t.set('messages', messages) | |
| doc.getMap('threads').set(threadId, t) | |
| }) | |
| const poll = await (async () => { | |
| const resp = await fetch(`${BASE}/api/mentions/stream?wait=15`, { headers: { authorization: `Bearer ${AGENT}` } }) | |
| let text = '' | |
| for await (const chunk of resp.body) text += Buffer.from(chunk).toString() | |
| return JSON.parse(text.trim().split('\n').filter(l => !l.startsWith(':')).pop()) | |
| })() | |
| assert.equal(poll.mentions?.length, 1, JSON.stringify(poll)) | |
| console.log('✓ mention delivered via long-poll') | |
| const snap = await api(`/api/docs/${docId}`) | |
| assert.ok(snap.blocks?.length >= 2) | |
| const sugg = await agentApi(`/api/docs/${docId}/suggestions`, { | |
| method: 'POST', | |
| body: { | |
| block_index: 1, | |
| replacement_markdown: 'Rewritten by the **prod** smoke agent.', | |
| rationale: 'prod test', | |
| mention_id: poll.mentions[0].mention_id, | |
| thread_id: threadId, | |
| }, | |
| }) | |
| assert.equal(sugg.ok, true, JSON.stringify(sugg)) | |
| const reply = await agentApi(`/api/docs/${docId}/threads/${threadId}/reply`, { | |
| method: 'POST', | |
| body: { text: 'done — see suggestion', mention_id: poll.mentions[0].mention_id }, | |
| }) | |
| assert.equal(reply.ok, true) | |
| console.log('✓ agent reply + suggestion') | |
| await waitFor(() => { | |
| const msgs = doc.getMap('threads').get(threadId)?.get('messages')?.toArray() || [] | |
| return msgs[0]?.mentions?.[0]?.status === 'done' && msgs.some(m => m.authorType === 'agent') | |
| }, 'chip done + agent msg synced to client') | |
| console.log('✓ status chip ✅ and agent reply visible in live doc') | |
| const acc = await api(`/api/docs/${docId}/suggestions/${sugg.suggestion_id}/accept`, { method: 'POST' }) | |
| assert.equal(acc.ok, true, JSON.stringify(acc)) | |
| await waitFor(async () => (await api(`/api/docs/${docId}`)).markdown.includes('prod'), 'accept applied') | |
| console.log('✓ suggestion accepted and applied') | |
| // agent adds a figure: upload bytes, reference in a suggestion, accept | |
| const pngBytes = Buffer.from( | |
| 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAF0lEQVR4nGP8z8Dwn4EIwESMolGFtFEIAK5+AxGmizXcAAAAAElFTkSuQmCC', | |
| 'base64' | |
| ) | |
| const up = await fetch(`${BASE}/api/docs/${docId}/upload`, { | |
| method: 'POST', | |
| headers: { authorization: `Bearer ${AGENT}`, 'content-type': 'image/png' }, | |
| body: pngBytes, | |
| }).then(r => r.json()) | |
| assert.ok(up.url?.startsWith('/files/'), 'agent upload: ' + JSON.stringify(up)) | |
| const figSugg = await agentApi(`/api/docs/${docId}/suggestions`, { | |
| method: 'POST', | |
| body: { | |
| block_index: 1, | |
| replacement_markdown: `A paragraph with a figure.\n\n`, | |
| }, | |
| }) | |
| assert.equal(figSugg.ok, true, JSON.stringify(figSugg)) | |
| const figAcc = await api(`/api/docs/${docId}/suggestions/${figSugg.suggestion_id}/accept`, { method: 'POST' }) | |
| assert.equal(figAcc.ok, true, JSON.stringify(figAcc)) | |
| await waitFor(async () => (await api(`/api/docs/${docId}`)).markdown.includes('![test figure]'), 'figure in doc') | |
| const fig = await fetch(`${BASE}${up.url}`, { headers: { authorization: `Bearer ${TOKEN}` } }) | |
| assert.equal(fig.status, 200, 'figure served') | |
| console.log('✓ agent figure: upload -> suggestion -> accept -> served') | |
| // streaming long-poll must survive the proxy well past 60s (heartbeats) | |
| console.log('… streaming long-poll test (~70s: mention arrives 65s into the stream)') | |
| const streamP = (async () => { | |
| const resp = await fetch(`${BASE}/api/mentions/stream?wait=90`, { headers: { authorization: `Bearer ${AGENT}` } }) | |
| let text = '' | |
| for await (const chunk of resp.body) text += Buffer.from(chunk).toString() | |
| return text | |
| })() | |
| await new Promise(r => setTimeout(r, 65000)) | |
| doc.transact(() => { | |
| doc.getMap('threads').get(threadId).get('messages').push([ | |
| { id: 'pm-stream', author: me.user.username, authorType: 'user', text: `@${HANDLE} still there?`, ts: Date.now() }, | |
| ]) | |
| }) | |
| const sText = await streamP | |
| const sLast = sText.trim().split('\n').filter(l => !l.startsWith(':')).pop() | |
| const sRes = JSON.parse(sLast) | |
| assert.ok(sRes.mentions?.length >= 1, 'stream delivered after 65s: ' + String(sLast).slice(0, 200)) | |
| assert.ok(sText.split(':hb').length > 2, 'heartbeats flowed through the proxy') | |
| console.log('✓ streaming long-poll: mention delivered 65s in, through the Space proxy') | |
| provider.destroy() | |
| socket.destroy() | |
| try { | |
| await api(`/api/docs/${docId}`, { method: 'DELETE' }) | |
| await api(`/api/agents/${HANDLE}`, { method: 'DELETE' }) | |
| console.log('✓ cleanup (doc + handle deleted)') | |
| } catch { | |
| console.log('~ cleanup skipped (delete endpoint not in running image yet)') | |
| } | |
| console.log('\nPROD CHECK PASSED') | |
| process.exit(0) | |
| } | |
| function b64(rel) { | |
| return Buffer.from(Y.encodeRelativePosition(rel)).toString('base64url') | |
| } | |
| main().catch(err => { | |
| console.error('PROD CHECK FAILED:', err) | |
| process.exit(1) | |
| }) | |