Files
windmill/chat-sdk/test/runnable.test.ts
T
hugocasaandClaude Opus 5.5 07d4f7fe61 feat: app conversations through the memory_id query parameter (#12025)
* feat: app conversations through the memory_id query parameter, chat-mode flows included

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate the mcp session instructions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: type backendAsync's memoryId option in the eval diagnostics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: pin git-sync hub scripts to windmill-cli 1.849.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: memoryId option on the sync backend call too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: bump the ui builder artifact for the memoryId option

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: shared-utils 1.0.14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: cli takes shared-utils 1.0.14 for the memoryId option

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: pin the app conversation scoping and the memoryId signatures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: assert the ownership refusal, not any refusal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: match the memoryId option in the cli wmill.ts signature tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: file an app's turns as the deployed flow's conversation, preview included

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:51:35 +01:00

69 lines
3.1 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { TurnRunningError } from '../src/api'
import { createChat } from '../src/chat'
import { memoryStorage, ndjson } from './support'
describe('raw app agent runnable', () => {
test('chats through the runnable without a request of its own', async () => {
const calls: unknown[] = []
let refuse = false
const start = async (args: unknown, opts: { memoryId: string }) => {
// As the host relays a 409: the server's body behind the error's stack.
if (refuse) {
const body = { error: 'still answering', running_turn: { job_id: 'job-1', user_seq: 1 } }
throw new Error(`Error: ${JSON.stringify(body)}\n at x`)
}
calls.push([args, opts])
return `job-${calls.length}`
}
const streamJob = async (jobId: string, onUpdate?: (u: { new_result_stream?: string }) => void) => {
const stream = ndjson({ type: 'token_delta', content: 'answer ' }, { type: 'token_delta', content: jobId })
onUpdate?.({ new_result_stream: stream.slice(0, 10) })
onUpdate?.({ new_result_stream: stream.slice(10) })
if (jobId === 'job-3') return { error: { name: 'Error', message: 'agent failed' } }
return { output: `answer ${jobId}`, messages: [] }
}
const storage = memoryStorage()
const wmill = { backendAsync: { chat: start, other: start }, streamJob }
const chat = createChat({
wmill,
runnable: 'chat',
storage,
fetch: () => Promise.reject(new Error('no request expected'))
})
expect(chat.getState().history).toBe('local')
await chat.sendMessage('hi')
await chat.sendMessage('again')
const conversationId = chat.getState().conversationId
expect(calls).toEqual([
[{ user_message: 'hi' }, { memoryId: conversationId }],
[{ user_message: 'again' }, { memoryId: conversationId }]
])
expect(chat.getState().messages.map((m) => m.content)).toEqual(['hi', 'answer job-1', 'again', 'answer job-2'])
await chat.sendMessage('third')
const messages = chat.getState().messages
const last = messages[messages.length - 1]
expect([last.content, last.success]).toEqual(['agent failed', false])
// A turn still running elsewhere takes the message back rather than failing a turn.
refuse = true
await expect(chat.sendMessage('fourth')).rejects.toBeInstanceOf(TurnRunningError)
expect(chat.getState().messages).toEqual(messages)
// Each runnable keeps its own history: another agent never sees this conversation.
expect(createChat({ wmill, runnable: 'chat', storage }).getState().conversations).toHaveLength(1)
expect(createChat({ wmill, runnable: 'other', storage }).getState().conversations).toEqual([])
// Nor does the same runnable name in another app, whose path the host puts in `ctx`.
const g = globalThis as { ctx?: unknown }
g.ctx = { path: 'u/admin/other_app' }
try {
expect(createChat({ wmill, runnable: 'chat', storage }).getState().conversations).toEqual([])
} finally {
delete g.ctx
}
// @ts-expect-error a name the app's module does not declare
createChat({ wmill, runnable: 'missing', storage })
})
})