mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 16:02:47 +00:00
* 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>
69 lines
3.1 KiB
TypeScript
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 })
|
|
})
|
|
})
|