Files
windmill/frontend/src/lib/components/flows/utils.test.ts
T
hugocasaandClaude Opus 5 c297ed0052 feat: managed memory with an inherited or custom memory id per step (#11118)
* feat: split ai agent memory into agent policy, run memory id and step history

* fix: scope string memory ids to workspace and flow, keep nested tool history inputs

* chore: update sqlx cache for the flow context query

* docs: describe memory id scoping as collision-free rather than isolated

* chore: regenerate openflow json after merging main

* fix: offer no memory id for legacy manual memory, document linked history inputs

* fix: seed provided messages from legacy manual memory and hide its note once set

* fix: bypass memory when a provided messages expression evaluates to null

* fix: require a user message when provided messages are empty

* chore: keep the empty messages comment within the line width

* docs: name the history inputs wherever linked steps list their flow-local inputs

* docs: keep the memory storage path on one line

* feat: managed memory with an inherited or custom memory id per step

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

* fix: list a custom memory id in the test run form and name where an inherited one comes from

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

* fix: keep memory id out of the add-field menu and drop the memory id telemetry

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

* fix: keep legacy auto memory without an id working after an untouched redeploy

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

* fix: rename step messages to previous_messages and address review

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

* style: rewrap comments and docs lines lengthened by the previous_messages rename

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

* refactor: read agent memory as either a legacy shape or the current one

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

* fix: name the memory setting in ignored-input notes and keep conversions honest

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

* fix: keep a legacy memory count unset on open and read a cleared count as off

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: address review on cleared test history and zero-count memory

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: drop flow-local keys from a linked agent resource before interpolating it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: keep a linked resource's own inputs as fallbacks and note ignored history on image runs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: restore the linked agent draft tests and log ignored history on every image run

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: resolve the one-of variant from the value when the selected one leaves the list

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: treat zero-count managed memory as off when enabling chat mode and shorten comments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: stop requiring user_message in the openflow agent contract when previous messages are the prompt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:55:43 +02:00

100 lines
3.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { FlowValue } from '$lib/gen'
import { modulesWithRetryOrSleep, normalizeAgentHistory } from './utils.svelte'
const constantRetry = { constant: { attempts: 1, seconds: 5 } }
function step(id: string, extra: Record<string, unknown> = {}) {
return { id, value: { type: 'identity' }, ...extra } as any
}
describe('modulesWithRetryOrSleep', () => {
it('reports retries and sleeps everywhere same_worker applies', () => {
const flow: FlowValue = {
modules: [
step('a', { retry: constantRetry }),
step('b', { sleep: { type: 'static', value: 3 } }),
step('c'),
{
id: 'd',
value: {
type: 'forloopflow',
modules: [step('e', { retry: constantRetry })],
iterator: { type: 'static', value: [] },
skip_failures: false
}
} as any
],
failure_module: step('failure', { retry: constantRetry }),
preprocessor_module: step('preprocessor', { sleep: { type: 'static', value: 1 } })
}
expect(modulesWithRetryOrSleep(flow)).toEqual(['a', 'b', 'e', 'failure', 'preprocessor'])
})
it('ignores what same_worker does not govern: agent tools and attempt-less retries', () => {
const flow: FlowValue = {
modules: [
step('a', { retry: { constant: { attempts: 0, seconds: 5 } } }),
{
id: 'b',
value: { type: 'aiagent', tools: [step('tool', { retry: constantRetry })] }
} as any
]
}
expect(modulesWithRetryOrSleep(flow)).toEqual([])
})
})
describe('normalizeAgentHistory', () => {
const legacy = () => ({
memory: {
type: 'static',
value: { kind: 'auto', context_length: 10, memory_id: '0f5c3a8e-1d2b-4c6a-9e7f-3b8d2a1c4e6f' }
}
})
// Outside chat the baked id is still read for runs that pass none, and an older worker must keep
// accepting the step, so a save leaves it exactly as it was.
it('keeps a legacy baked memory id outside chat mode', () => {
const transforms = legacy()
normalizeAgentHistory(transforms, false)
expect(transforms).toEqual(legacy())
})
it('drops a legacy baked memory id in chat mode, where it was never read', () => {
const transforms = legacy()
normalizeAgentHistory(transforms, true)
expect(transforms.memory.value).toEqual({ kind: 'auto', context_length: 10 })
})
it('drops an empty baked memory id, which names no memory', () => {
const transforms = {
memory: { type: 'static', value: { kind: 'auto', context_length: 10, memory_id: '' } }
}
normalizeAgentHistory(transforms, false)
expect(transforms.memory.value).toEqual({ kind: 'auto', context_length: 10 })
})
it('saves managed memory that keeps no messages as off, which is how it runs', () => {
for (const context_length of [0, null, undefined]) {
const transforms: Record<string, any> = {
memory: { type: 'static', value: { kind: 'window', context_length } }
}
normalizeAgentHistory(transforms, false)
expect(transforms.memory.value).toEqual({ kind: 'off' })
}
})
it('does not persist an empty static memory id or message list', () => {
const transforms: Record<string, any> = {
memory_id: { type: 'static', value: ' ' },
previous_messages: { type: 'static', value: [] }
}
normalizeAgentHistory(transforms, false)
expect(transforms).toEqual({})
})
})