File size: 8,131 Bytes
0d14b54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53148eb
 
 
 
 
 
 
 
0d14b54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b1c90fd
 
 
 
 
 
0d14b54
 
 
 
 
 
53148eb
0d14b54
 
 
 
 
 
 
 
 
 
53148eb
0d14b54
53148eb
0d14b54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b60973d
 
 
 
 
 
 
53148eb
b60973d
 
 
53148eb
b60973d
 
 
 
 
 
 
 
 
 
 
 
 
 
5a65b82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d14b54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// 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![test figure](${up.url})`,
    },
  })
  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)
})