feat: add verified manual pipeline migration for plugin runners

This commit is contained in:
RockChinQ
2026-09-16 18:13:11 +00:00
parent 273b1ea3cf
commit 8c119bc4b6
97 changed files with 16662 additions and 123 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,287 @@
import { test, expect, type Page } from '@playwright/test';
import { readFileSync } from 'node:fs';
const contract = JSON.parse(
readFileSync('tests/e2e/fixtures/runner-migration-contract.json', 'utf8'),
);
import { installLangBotApiMocks } from './fixtures/langbot-api';
const canonical = JSON.parse(
readFileSync('../src/langbot/templates/default-pipeline-config.json', 'utf8'),
);
const outputSchema = contract.output_schema;
async function setup(
page: Page,
plugin = 'weknora-agent',
name = 'WeKnoraAgent',
agentId: null | 'absent' = null,
converted?: typeof canonical,
) {
const schema = contract.plugins[plugin].component;
const id = `plugin:langbot-team/${name}/default`;
const raw: Record<string, unknown> = {
'api-key': ' fixture credential ',
'base-url': ' http://localhost:8080/api/v1 ',
'app-type': 'agent',
'enable-all-tools': false,
tools: ['tool-allowed'],
'knowledge-bases': ['kb-allowed'],
'mcp-resources': [{ server: 'private', uri: 'secret' }],
'mcp-resource-agent-read-enabled': false,
};
if (agentId === null) raw['agent-id'] = null;
if (plugin === 'LocalAgent') {
raw['advanced-settings'] = true;
raw.model = {
primary: 'llm-valid',
fallbacks: ['llm-fallback'],
reasoning: { 'llm-valid': 'high', 'llm-fallback': 'low' },
};
raw['remove-think'] = true;
}
let config = {
...structuredClone(canonical),
ai: { runner: { id }, runner_config: { [id]: raw } },
};
if (converted) config = structuredClone(converted);
else config.output.misc['remove-think'] = false; // Deliberate disagreement must not synchronize on mount.
const writes: any[] = [];
await installLangBotApiMocks(page, { authenticated: true });
await page.routeWebSocket('**/api/v1/pipelines/**/ws/connect**', (ws) => {
ws.onMessage((raw) => {
if (JSON.parse(String(raw)).type === 'authenticate')
ws.send(
JSON.stringify({
type: 'connected',
connection_id: 'fixture',
session_type: 'person',
}),
);
});
});
await page.route('**/api/v1/pipelines/preserve', async (route) => {
if (route.request().method() !== 'GET') {
const body = route.request().postDataJSON();
writes.push(body);
if (body.config) config = body.config;
}
await route.fulfill({
json: {
code: 0,
data: {
pipeline: {
uuid: 'preserve',
name: 'Preservation',
description: '',
emoji: '⚙️',
config,
},
},
},
});
});
await page.route('**/api/v1/pipelines/_/metadata', (route) =>
route.fulfill({
json: {
code: 0,
data: {
configs: [
{
name: 'ai',
label: { en_US: 'AI' },
stages: [
{
name: 'runner',
label: { en_US: 'Runner' },
config: contract.ai_schema.stages[0].config.map(
(field: Record<string, unknown>) =>
field.name === 'id'
? {
...field,
default: id,
options: [
{ name: id, label: schema.metadata.label },
],
}
: field,
),
},
{
name: id,
label: schema.metadata.label,
config: schema.spec.config,
},
],
},
outputSchema,
],
},
},
}),
);
await page.route('**/api/v1/provider/models/llm', (route) =>
route.fulfill({
json: {
code: 0,
data: {
models: ['llm-valid', 'llm-fallback'].map((uuid) => ({
uuid,
name: uuid,
provider_uuid: 'provider-valid',
provider: { uuid: 'provider-valid', name: 'Mock Provider' },
reasoning: { level: 'high' },
reasoning_capabilities: {
levels: ['provider_default', 'low', 'high'],
},
})),
},
},
}),
);
await page.goto('/home/pipelines?id=preserve');
await page.getByRole('tab', { name: 'AI', exact: true }).click();
return { writes, raw, id };
}
test('reasoning edit distinguishes provider default from model inheritance and preserves fallbacks', async ({
page,
}) => {
const { writes, raw, id } = await setup(page, 'LocalAgent', 'LocalAgent');
await page
.getByRole('button', { name: 'Reasoning level: High', exact: true })
.click();
await page.getByRole('slider').press('Home');
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(1);
expect(writes[0].config.ai.runner_config[id]).toEqual({
...raw,
model: {
...(raw.model as object),
reasoning: { 'llm-valid': 'provider_default', 'llm-fallback': 'low' },
},
});
await page.reload();
await page.getByRole('tab', { name: 'AI', exact: true }).click();
await page
.getByRole('button', {
name: 'Reasoning level: Provider default',
exact: true,
})
.click();
await page
.getByRole('button', { name: 'Use model setting', exact: true })
.click();
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(2);
expect(writes[1].config.ai.runner_config[id]).toEqual({
...raw,
model: { ...(raw.model as object), reasoning: { 'llm-fallback': 'low' } },
});
expect(writes[1].config.output).toEqual(writes[0].config.output);
await page.reload();
await page.getByRole('tab', { name: 'AI', exact: true }).click();
await expect(
page.getByRole('button', {
name: 'Reasoning level: Use model setting',
exact: true,
}),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
// The reverse transition must not require moving through another level.
await page
.getByRole('button', {
name: 'Reasoning level: Use model setting',
exact: true,
})
.click();
await page
.getByRole('button', { name: 'Provider default', exact: true })
.click();
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(3);
expect(writes[2].config.ai.runner_config[id]).toEqual(
writes[0].config.ai.runner_config[id],
);
// Clearing a fallback override must not clear the primary override.
await page
.getByRole('button', { name: 'Reasoning level: Low', exact: true })
.click();
await page
.getByRole('button', { name: 'Use model setting', exact: true })
.click();
await page.keyboard.press('Escape');
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(4);
expect(writes[3].config.ai.runner_config[id]).toEqual({
...raw,
model: {
...(raw.model as object),
reasoning: { 'llm-valid': 'provider_default' },
},
});
});
test('standalone picker keeps its original levels and provider default API', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/pipelines');
await page.evaluate(async () => {
// Vite serves the actual component, not a test replacement.
const resources = performance
.getEntriesByType('resource')
.map((entry) => entry.name);
const reactPath = resources.find((url) =>
new URL(url).pathname.endsWith('/react.js'),
)!;
const domPath = resources.find((url) =>
new URL(url).pathname.endsWith('/react-dom_client.js'),
)!;
const pickerPath =
'/src/app/home/components/reasoning/ReasoningLevelPicker.tsx';
const reactModule = await import(reactPath);
const React = reactModule.default || reactModule;
const domModule = await import(domPath);
const { createRoot } = domModule.default || domModule;
const { default: Picker } = await import(pickerPath);
const host = document.createElement('div');
const app = document.getElementById('root');
if (app) app.style.display = 'none';
document.body.appendChild(host);
function Harness() {
const [value, setValue] = React.useState('provider_default');
return React.createElement(Picker, {
value,
levels: ['provider_default', 'high'],
onChange: (next: string) => {
host.dataset.value = next;
setValue(next);
},
});
}
host.id = 'standalone-reasoning';
createRoot(host).render(React.createElement(Harness));
});
await page
.getByRole('button', {
name: 'Reasoning level: Provider default',
exact: true,
})
.click();
await expect(
page.getByRole('button', { name: 'Use model setting', exact: true }),
).toHaveCount(0);
await page.getByRole('slider').press('End');
await expect(page.locator('#standalone-reasoning')).toHaveAttribute(
'data-value',
'high',
);
await page.getByRole('slider').press('Home');
await expect(page.locator('#standalone-reasoning')).toHaveAttribute(
'data-value',
'provider_default',
);
});
@@ -0,0 +1,212 @@
import { test, expect, type Page } from '@playwright/test';
import { readFileSync } from 'node:fs';
const contract = JSON.parse(
readFileSync('tests/e2e/fixtures/runner-migration-contract.json', 'utf8'),
);
import { installLangBotApiMocks } from './fixtures/langbot-api';
const canonical = JSON.parse(
readFileSync('../src/langbot/templates/default-pipeline-config.json', 'utf8'),
);
const outputSchema = contract.output_schema;
const switchField = (page: Page, label: string) =>
page
.locator('label')
.filter({ hasText: label })
.locator('../..')
.getByRole('switch');
async function setup(
page: Page,
plugin = 'weknora-agent',
name = 'WeKnoraAgent',
agentId: null | 'absent' = null,
converted?: typeof canonical,
) {
const schema = contract.plugins[plugin].component;
const id = `plugin:langbot-team/${name}/default`;
const raw: Record<string, unknown> = {
'api-key': ' fixture credential ',
'base-url': ' http://localhost:8080/api/v1 ',
'app-type': 'agent',
'enable-all-tools': false,
tools: ['tool-allowed'],
'knowledge-bases': ['kb-allowed'],
'mcp-resources': [{ server: 'private', uri: 'secret' }],
'mcp-resource-agent-read-enabled': false,
};
if (agentId === null) raw['agent-id'] = null;
if (plugin === 'LocalAgent') {
raw['advanced-settings'] = true;
raw.model = {
primary: 'llm-valid',
fallbacks: [],
reasoning: { 'llm-valid': 'provider_default' },
};
raw['remove-think'] = true;
}
let config = {
...structuredClone(canonical),
ai: { runner: { id }, runner_config: { [id]: raw } },
};
if (converted) config = structuredClone(converted);
else config.output.misc['remove-think'] = false; // Deliberate disagreement must not synchronize on mount.
const writes: any[] = [];
await installLangBotApiMocks(page, { authenticated: true });
await page.routeWebSocket('**/api/v1/pipelines/**/ws/connect**', (ws) => {
ws.onMessage((raw) => {
if (JSON.parse(String(raw)).type === 'authenticate')
ws.send(
JSON.stringify({
type: 'connected',
connection_id: 'fixture',
session_type: 'person',
}),
);
});
});
await page.route('**/api/v1/pipelines/preserve', async (route) => {
if (route.request().method() !== 'GET') {
const body = route.request().postDataJSON();
writes.push(body);
if (body.config) config = body.config;
}
await route.fulfill({
json: {
code: 0,
data: {
pipeline: {
uuid: 'preserve',
name: 'Preservation',
description: '',
emoji: '⚙️',
config,
},
},
},
});
});
await page.route('**/api/v1/pipelines/_/metadata', (route) =>
route.fulfill({
json: {
code: 0,
data: {
configs: [
{
name: 'ai',
label: { en_US: 'AI' },
stages: [
{
name: 'runner',
label: { en_US: 'Runner' },
config: contract.ai_schema.stages[0].config.map(
(field: Record<string, unknown>) =>
field.name === 'id'
? {
...field,
default: id,
options: [
{ name: id, label: schema.metadata.label },
],
}
: field,
),
},
{
name: id,
label: schema.metadata.label,
config: schema.spec.config,
},
],
},
outputSchema,
],
},
},
}),
);
await page.goto('/home/pipelines?id=preserve');
await page.getByRole('tab', { name: 'AI', exact: true }).click();
return { writes, raw, id };
}
for (const candidate of contract.cases) {
test(`actual converter output stays editable and unchanged for ${candidate.legacy_runner}`, async ({
page,
}) => {
const name = candidate.plan.target_plugin.name;
const folder = Object.keys(contract.plugins).find(
(key) => contract.plugins[key].manifest.metadata.name === name,
)!;
const { writes } = await setup(
page,
folder,
name,
null,
candidate.plan.config,
);
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
expect(writes).toHaveLength(0);
await page.getByRole('tab', { name: 'Output', exact: true }).click();
await switchField(page, 'Quote Origin Message').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(1);
expect(writes[0].config.ai).toEqual(candidate.plan.config.ai);
expect(writes[0].config.output.misc['quote-origin']).toBe(
!candidate.plan.config.output.misc['quote-origin'],
);
});
}
for (const agentId of [null, 'absent'] as const) {
test(`mounted real WeKnora schema preserves ${agentId} agent ID and hidden policy`, async ({
page,
}) => {
const { writes, raw, id } = await setup(
page,
'weknora-agent',
'WeKnoraAgent',
agentId,
);
await expect(page.getByText('Agent ID', { exact: true })).toBeVisible();
expect(writes).toEqual([]);
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
await switchField(page, 'Enable Web Search').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(1);
expect(writes[0].config.ai.runner_config[id]).toEqual({
...raw,
'web-search-enabled': true,
});
await page.reload();
await page.getByRole('tab', { name: 'AI', exact: true }).click();
await expect(switchField(page, 'Enable Web Search')).toBeChecked();
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
expect(writes).toHaveLength(1);
});
}
test('remove-think is synchronized only after intentional output or runner toggles', async ({
page,
}) => {
const { writes, id } = await setup(page, 'LocalAgent', 'LocalAgent');
const runnerThink = switchField(page, 'Remove');
await expect(runnerThink).toBeChecked();
expect(writes).toEqual([]);
await page.getByRole('tab', { name: 'Output', exact: true }).click();
const outputThink = switchField(page, 'Remove CoT');
await expect(outputThink).not.toBeChecked();
await outputThink.click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(1);
expect(writes[0].config.output.misc['remove-think']).toBe(true);
expect(writes[0].config.ai.runner_config[id]['remove-think']).toBe(true);
await page.getByRole('tab', { name: 'AI', exact: true }).click();
await runnerThink.click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(2);
expect(writes[1].config.output.misc['remove-think']).toBe(false);
expect(writes[1].config.ai.runner_config[id]['remove-think']).toBe(false);
});
@@ -0,0 +1,233 @@
import { test, expect, type Page } from '@playwright/test';
import { readFileSync } from 'node:fs';
const contract = JSON.parse(
readFileSync('tests/e2e/fixtures/runner-migration-contract.json', 'utf8'),
);
import { installLangBotApiMocks } from './fixtures/langbot-api';
const canonical = contract.cases[0].plan.config;
const outputSchema = contract.output_schema;
const switchField = (page: Page, label: string) =>
page
.locator('label')
.filter({ hasText: label })
.locator('../..')
.getByRole('switch');
async function setup(
page: Page,
plugin = 'weknora-agent',
name = 'WeKnoraAgent',
agentId: null | 'absent' = null,
converted?: typeof canonical,
) {
const schema = contract.plugins[plugin].component;
const id = `plugin:langbot-team/${name}/default`;
const raw: Record<string, unknown> = {
'api-key': ' fixture credential ',
'base-url': ' http://localhost:8080/api/v1 ',
'app-type': 'agent',
'enable-all-tools': false,
tools: ['tool-allowed'],
'knowledge-bases': ['kb-allowed'],
'mcp-resources': [{ server: 'private', uri: 'secret' }],
'mcp-resource-agent-read-enabled': false,
};
if (agentId === null) raw['agent-id'] = null;
if (plugin === 'LocalAgent') {
raw['advanced-settings'] = true;
raw.model = {
primary: 'llm-valid',
fallbacks: [],
reasoning: { 'llm-valid': 'provider_default' },
};
raw['remove-think'] = true;
}
let config = {
...structuredClone(canonical),
ai: { runner: { id }, runner_config: { [id]: raw } },
};
if (converted) config = structuredClone(converted);
else config.output.misc['remove-think'] = false; // Deliberate disagreement must not synchronize on mount.
const writes: any[] = [];
await installLangBotApiMocks(page, { authenticated: true });
await page.routeWebSocket('**/api/v1/pipelines/**/ws/connect**', (ws) => {
ws.onMessage((raw) => {
if (JSON.parse(String(raw)).type === 'authenticate')
ws.send(
JSON.stringify({
type: 'connected',
connection_id: 'fixture',
session_type: 'person',
}),
);
});
});
await page.route('**/api/v1/pipelines/preserve', async (route) => {
if (route.request().method() !== 'GET') {
const body = route.request().postDataJSON();
writes.push(body);
if (body.config) config = body.config;
}
await route.fulfill({
json: {
code: 0,
data: {
pipeline: {
uuid: 'preserve',
name: 'Preservation',
description: '',
emoji: '⚙️',
config,
},
},
},
});
});
await page.route('**/api/v1/pipelines/_/metadata', (route) =>
route.fulfill({
json: {
code: 0,
data: {
configs: [
{
name: 'ai',
label: { en_US: 'AI' },
stages: [
{
name: 'runner',
label: { en_US: 'Runner' },
config: contract.ai_schema.stages[0].config.map(
(field: Record<string, unknown>) =>
field.name === 'id'
? {
...field,
default: id,
options: [
{ name: id, label: schema.metadata.label },
],
}
: field,
),
},
{
name: id,
label: schema.metadata.label,
config: schema.spec.config,
},
],
},
outputSchema,
],
},
},
}),
);
await page.goto('/home/pipelines?id=preserve');
await page.getByRole('tab', { name: 'AI', exact: true }).click();
return { writes, raw, id };
}
for (const kind of ['json', 'prompt', 'prompt-omitted'] as const) {
test(`canonical ${kind} edits preserve types and metadata, reject invalid drafts`, async ({
page,
}) => {
const candidate = contract.cases.find(
(c: any) =>
c.legacy_runner === (kind === 'json' ? 'langflow-api' : 'local-agent'),
);
const config = structuredClone(candidate.plan.config);
const id = candidate.plan.target_runner_id;
const field = kind === 'json' ? 'tweaks' : 'prompt';
const initial =
kind === 'json'
? {
node: {
enabled: false,
count: 0,
missing: null,
list: [false, 0, null],
},
}
: kind === 'prompt-omitted'
? [
{
role: 'assistant',
tool_calls: [],
name: 'original',
provider_specific_fields: { cache: false, count: 0 },
},
]
: [
{
role: 'user',
content: [{ type: 'text', text: 'original' }],
name: 'alice',
metadata: { flag: false, count: 0 },
},
{
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call1',
type: 'function',
function: { name: 'foo', arguments: '{}' },
},
],
},
{ role: 'tool', content: [], tool_call_id: 'call1' },
];
config.ai.runner_config[id][field] = initial;
config.ai.runner_config[id]['advanced-settings'] = true;
const { writes } = await setup(
page,
kind === 'json' ? 'langflow-agent' : 'LocalAgent',
candidate.plan.target_plugin.name,
null,
config,
);
const editor = page.getByTestId(`structured-editor-${field}`);
await expect(editor).toBeVisible();
expect(JSON.parse(await editor.inputValue())).toEqual(initial);
expect(writes).toHaveLength(0);
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
const edited = structuredClone(initial) as any;
if (kind === 'json') edited.node.count = 7;
else if (kind === 'prompt-omitted') edited[0].name = 'edited';
else edited[0].content[0].text = 'edited';
await editor.fill(JSON.stringify(edited, null, 2));
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(1);
expect(writes[0].config.ai.runner_config[id][field]).toEqual(edited);
if (kind === 'prompt-omitted')
expect(
Object.hasOwn(
writes[0].config.ai.runner_config[id][field][0],
'content',
),
).toBe(false);
await page.reload();
await page.getByRole('tab', { name: 'AI', exact: true }).click();
expect(JSON.parse(await editor.inputValue())).toEqual(edited);
await editor.fill('{ invalid');
await expect(
page.getByRole('alert').filter({ hasText: `${field} (JSON)` }),
).toBeVisible();
if (kind === 'json') await switchField(page, 'Advanced Settings').click();
// Make another valid edit so Save remains available: invalid JSON must
// block the entire save, not silently persist the previous value.
await page.getByRole('tab', { name: 'Output', exact: true }).click();
await switchField(page, 'Quote Origin Message').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.waitForTimeout(400);
expect(writes).toHaveLength(1);
await page.getByRole('tab', { name: 'AI', exact: true }).click();
if (kind === 'json') await switchField(page, 'Advanced Settings').click();
await expect(editor).toHaveValue('{ invalid');
await editor.fill(JSON.stringify(edited));
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect.poll(() => writes.length).toBe(2);
expect(writes[1].config.ai.runner_config[id][field]).toEqual(edited);
});
}
+24 -11
View File
@@ -3,20 +3,33 @@ import fs from 'node:fs';
import test from 'node:test';
import ts from 'typescript';
function loadCatalog(url) {
const compiled = ts.transpileModule(fs.readFileSync(url, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText;
const module = { exports: {} };
const require = (specifier) => {
assert.ok(specifier.startsWith('./'), 'Locale imports must be relative');
return loadCatalog(new URL(`${specifier}.ts`, url));
};
new Function('module', 'exports', 'require', compiled)(
module,
module.exports,
require,
);
return module.exports;
}
test('all locale catalogs cover Codex states and preserve the expiry placeholder', () => {
const directory = new URL('../../src/i18n/locales/', import.meta.url);
const files = fs
.readdirSync(directory)
.filter((file) => file.endsWith('.ts'));
assert.equal(files.length, 8);
let expected;
for (const file of fs.readdirSync(directory)) {
const compiled = ts.transpileModule(
fs.readFileSync(new URL(file, directory), 'utf8'),
{
compilerOptions: { module: ts.ModuleKind.CommonJS },
},
).outputText;
const module = { exports: {} };
new Function('module', 'exports', compiled)(module, module.exports);
const catalog = (module.exports.default || Object.values(module.exports)[0])
.models.codex;
for (const file of files) {
const exports = loadCatalog(new URL(file, directory));
const catalog = (exports.default || Object.values(exports)[0]).models.codex;
const keys = Object.keys(catalog).sort();
expected ??= keys;
assert.deepEqual(keys, expected, file);
@@ -66,6 +66,7 @@ test('normalizes only single-line text fields in a dynamic form save snapshot',
fallbacks: ['fallback-model'],
reasoning: {
'primary-model': 'high',
'fallback-model': 'provider_default',
},
},
});
@@ -0,0 +1,69 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
import test from 'node:test';
import ts from 'typescript';
const source = readFileSync(
'src/app/home/pipelines/pipeline-config-safety.ts',
'utf8',
);
const context = { exports: {} };
vm.runInNewContext(
ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText,
context,
);
const { isCurrentPipelineConfig } = context.exports;
const plugin = 'plugin:langbot-team/LocalAgent/default';
test('canonical current empty runner remains editable', () => {
const config = JSON.parse(
readFileSync(
'../src/langbot/templates/default-pipeline-config.json',
'utf8',
),
);
assert.equal(isCurrentPipelineConfig(config), true);
});
test('configured current binding remains editable', () => {
assert.equal(
isCurrentPipelineConfig({
ai: { runner: { id: plugin }, runner_config: {} },
}),
true,
);
});
for (const [name, ai] of Object.entries({
legacy: { runner: { runner: 'local-agent' } },
mixedSelector: { runner: { id: plugin, runner: 'local-agent' } },
mixedLegacySection: {
runner: { id: plugin },
runner_config: {},
'local-agent': {},
},
emptyMixedLegacySection: {
runner: { id: '' },
runner_config: {},
'dify-service-api': {},
},
emptyMissingContainer: { runner: { id: '' } },
emptyMalformedContainer: { runner: { id: '' }, runner_config: [] },
emptyAmbiguousContainer: {
runner: { id: '' },
runner_config: { orphan: {} },
},
emptyAmbiguousSelector: {
runner: { id: '', legacy: 'local-agent' },
runner_config: {},
},
missingId: { runner: {}, runner_config: {} },
nonPlugin: { runner: { id: 'local-agent' }, runner_config: {} },
})) {
test(`${name} does not hydrate current editor defaults`, () => {
assert.equal(isCurrentPipelineConfig({ ai }), false);
});
}
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import test from 'node:test';
import vm from 'node:vm';
import ts from 'typescript';
function load(path) {
const context = { exports: {} };
vm.runInNewContext(
ts.transpileModule(readFileSync(path, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText,
context,
);
return context.exports;
}
const warnings = [
...new Set(
[
...readFileSync(
'../src/langbot/pkg/pipeline/legacy_config_migration.py',
'utf8',
).matchAll(/_warn\(result,\s*'([^']+)'/g),
].map((match) => match[1]),
),
];
test('every converter warning has a specific explanation in all eight locales', () => {
const { migrationIssueKey } = load(
'src/app/home/pipelines/pipeline-migration-issues.ts',
);
const base = 'src/i18n/locales/pipeline-migration';
const locales = readdirSync(base).filter((name) => name.endsWith('.ts'));
assert.equal(locales.length, 8);
assert.ok(
warnings.length > 10,
'warning discovery must not silently go empty',
);
for (const file of locales) {
const catalog = load(`${base}/${file}`).default;
for (const code of warnings) {
const key = migrationIssueKey(code);
assert.ok(key, `${file}: missing mapping for ${code}`);
assert.equal(typeof catalog.notices?.[key], 'string', `${file}: ${code}`);
assert.ok(catalog.notices[key].length > 10);
}
assert.ok(catalog.notices.pluginVersion);
assert.ok(catalog.notices.pendingInteraction);
}
});
test('unknown backend issue codes never become translation keys or displayed messages', () => {
const { migrationIssueKey } = load(
'src/app/home/pipelines/pipeline-migration-issues.ts',
);
for (const code of [
'unknown',
'__proto__',
'constructor',
'secret-token.example',
]) {
assert.equal(migrationIssueKey(code), undefined);
}
});
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import ts from 'typescript';
const source = fs.readFileSync(
'src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx',
'utf8',
);
// Exercise the actual field callback without mocking React's form lifecycle.
const callback = source.slice(
source.indexOf('const updateModelReasoning ='),
source.indexOf('const replaceModel ='),
);
function edit(reasoning, uuid, level) {
let result;
const code = ts.transpileModule(
callback + '\nupdateModelReasoning(uuid, level);',
{ compilerOptions: { target: ts.ScriptTarget.ES2020 } },
).outputText;
new Function('modelValue', 'updateValue', 'uuid', 'level', code)(
{ reasoning },
(value) => {
result = value.reasoning;
},
uuid,
level,
);
return result;
}
test('provider default is an explicit override, not inheritance', () => {
assert.deepEqual(edit({ a: 'high', b: 'low' }, 'a', 'provider_default'), {
a: 'provider_default',
b: 'low',
});
});
test('inherit deletes only the selected override without mutating input', () => {
const input = Object.freeze({ a: 'provider_default', b: 'high' });
const output = edit(input, 'a', undefined);
assert.equal(Object.hasOwn(output, 'a'), false);
assert.deepEqual(output, { b: 'high' });
assert.deepEqual(input, { a: 'provider_default', b: 'high' });
});
@@ -0,0 +1,103 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import ts from 'typescript';
function load(relative) {
const source = fs.readFileSync(new URL(relative, import.meta.url), 'utf8');
const compiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
}).outputText;
const module = { exports: {} };
new Function('module', 'exports', compiled)(module, module.exports);
return module.exports;
}
const helper =
'../../src/app/home/pipelines/components/pipeline-form/RunnerConfigPreservation.ts';
test('mount emission is not a persisted edit', () => {
const { preserveRunnerConfig } = load(helper);
const raw = {
'api-key': ' secret ',
'agent-id': null,
'enable-all-tools': false,
};
assert.deepEqual(
preserveRunnerConfig(raw, undefined, {
'api-key': 'secret',
'agent-id': '',
mode: 'chat',
}),
raw,
);
});
test('unrelated edits preserve hidden Host policy, absent/null and credentials', () => {
const { preserveRunnerConfig } = load(helper);
const raw = {
'api-key': ' secret ',
'agent-id': null,
'enable-all-tools': false,
tools: ['one'],
'knowledge-bases': ['kb'],
'mcp-resources': [{ uri: 'private' }],
'mcp-resource-agent-read-enabled': false,
};
const first = {
'api-key': 'secret',
'agent-id': '',
mode: 'chat',
absent: 'default',
};
assert.deepEqual(
preserveRunnerConfig(raw, first, { ...first, mode: 'agent' }),
{ ...raw, mode: 'agent' },
);
});
test('intentional edits, removals and nested edits win without normalizing untouched siblings', () => {
const { preserveRunnerConfig } = load(helper);
const raw = {
'api-key': ' secret ',
model: {
primary: 'one',
fallbacks: ['two'],
reasoning: { one: 'provider_default', two: 'high' },
},
};
const first = { 'api-key': 'secret', model: raw.model };
assert.deepEqual(
preserveRunnerConfig(raw, first, {
'api-key': 'new',
model: {
primary: 'one',
fallbacks: [],
reasoning: { one: 'provider_default' },
},
}),
{
'api-key': 'new',
model: {
primary: 'one',
fallbacks: [],
reasoning: { one: 'provider_default' },
},
},
);
});
test('save normalization preserves explicit selected per-model provider_default', () => {
const { normalizeDynamicFormValuesForSave } = load(
'../../src/app/home/components/dynamic-form/DynamicFormSaveValues.ts',
);
const value = {
primary: 'one',
fallbacks: ['two'],
reasoning: { one: 'provider_default', two: 'high', removed: 'high' },
};
assert.deepEqual(
normalizeDynamicFormValuesForSave(
[{ name: 'model', type: 'model-fallback-selector' }],
{ model: value },
).model.reasoning,
{ one: 'provider_default', two: 'high' },
);
});
@@ -0,0 +1,74 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import ts from 'typescript';
const file = 'src/app/home/components/dynamic-form/StructuredFieldValue.ts';
function load() {
const compiled = ts.transpileModule(fs.readFileSync(file, 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS },
}).outputText;
const m = { exports: {} };
new Function('module', 'exports', compiled)(m, m.exports);
return m.exports;
}
test('JSON parsing preserves nested false, zero, null and arrays as typed values', () => {
const { parseStructuredDraft } = load();
const value = { n: { a: false, b: 0, c: null, d: [false, 0, null] } };
assert.deepEqual(parseStructuredDraft(JSON.stringify(value), false), value);
});
test('prompt parsing preserves structured content, null, empty arrays, metadata and first role', () => {
const { parseStructuredDraft } = load();
const value = [
{ role: 'user', content: [{ type: 'text', text: 'x' }], name: 'alice' },
{ role: 'assistant', content: null, tool_calls: [] },
{ role: 'tool', content: [], tool_call_id: 'x' },
];
assert.deepEqual(parseStructuredDraft(JSON.stringify(value), true), value);
});
test('prompt metadata edits preserve omitted content without inserting null', () => {
const { parseStructuredDraft, isPromptValue } = load();
const value = [
{
role: 'assistant',
tool_calls: [],
name: 'edited',
provider_specific_fields: { cache: false },
},
];
assert.equal(isPromptValue(value), true);
const result = parseStructuredDraft(JSON.stringify(value), true);
assert.deepEqual(result, value);
assert.equal(Object.hasOwn(result[0], 'content'), false);
for (const content of [false, 0, {}, undefined])
assert.equal(isPromptValue([{ ...value[0], content }]), false);
});
test('invalid JSON and non-message prompt shapes reject rather than silently retain old values', () => {
const { parseStructuredDraft } = load();
for (const raw of ['{broken', '', 'undefined'])
assert.throws(() => parseStructuredDraft(raw, false));
for (const raw of [
'{}',
'null',
'[{"role":0,"content":"x"}]',
'[{"role":"user","content":false}]',
])
assert.throws(() => parseStructuredDraft(raw, true));
});
test('friendly prompt editor is restricted to losslessly represented roles and string content', () => {
const { isSimplePrompt } = load();
assert.equal(
isSimplePrompt([
{ role: 'system', content: '' },
{ role: 'user', content: 'x' },
]),
true,
);
for (const value of [
[{ role: 'user', content: 'x' }],
[{ role: 'system', content: null }],
[{ role: 'system', content: [] }],
[{ role: 'system', content: 'x', name: 'a' }],
[],
])
assert.equal(isSimplePrompt(value), false);
});