mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* docs(managed-agent): generate the server configuration reference from its metadata
The server has about 180 settings, 84 of them mapped to QWEN_MANAGED_AGENT_*
variables, and no list of them with their variables, defaults and limits:
operators read a 2000-line developer README for them.
- The server module runs spring-boot-configuration-processor once more,
over ManagedAgentProperties only (the two Druid beans share a prefix,
which the processor refuses), and writes the configuration metadata.
The compiled classes are unchanged. runtime-broker.ssh is marked
@NestedConfigurationProperty, and STRICT_HARNESS_KEYS @Getter(NONE), so
the metadata lists the one and not the other.
- configuration-descriptions.json holds an English and a Chinese
description for each setting and section.
- scripts/generate-managed-agent-configuration-reference.mjs writes
docs/users/managed-agent-configuration-reference{,.zh-CN}.md from the
metadata, the packaged application.yml (variables and defaults), the
${qwen.managed-agent.*} placeholders in the main sources (settings read
outside the properties class) and the descriptions. --check fails on a
setting without a description, a description of no setting, or a stale
page; the reference workflow runs it after the Java build.
- The archive ships the pages as docs/configuration{,.zh-CN}.md, and the
deployment guide and the README link them.
* fix(managed-agent): link the shipped documents by their archive names
The archive renames each document it ships, so a link written against
the repository, such as the configuration reference's language switch or
the guide's link to it, named a file the archive does not have. The
packager now points every link between shipped documents at the
document's name in the archive and leaves every other link as written.
* build(managed-agent): take the configuration processor's version from the BOM
spring-boot.version is defined nowhere in this build, so the processor
path's <version> only resolved because the compiler plugin falls back to
dependency management. Say so by naming no version.
* test(managed-agent): name no tracked Markdown in a link fixture
classify-profile.test.mjs requires every Markdown file a test names to be
in TEST_READ_MARKDOWN, and the kept-link fixture's docs/runtime-broker.md
matched packages/managed-agent-server/docs/runtime-broker.md, which no
test reads. The fixture only needs a link to a file the archive does not
ship, so it names one that does not exist.
466 lines
15 KiB
JavaScript
466 lines
15 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
APPLICATION_YML,
|
|
DESCRIPTIONS,
|
|
JAVA_SOURCES,
|
|
METADATA,
|
|
PAGES,
|
|
buildReference,
|
|
parseApplicationYml,
|
|
parsePlaceholder,
|
|
scanJavaPlaceholders,
|
|
} from '../generate-managed-agent-configuration-reference.mjs';
|
|
|
|
const SCRIPT = fileURLToPath(
|
|
new URL(
|
|
'../generate-managed-agent-configuration-reference.mjs',
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
const YML = `spring:
|
|
datasource:
|
|
url: '\${SPRING_DATASOURCE_URL:jdbc:mysql://127.0.0.1:3306/db}'
|
|
logging:
|
|
pattern:
|
|
# A quote inside a quoted value.
|
|
correlation: '%X{id} ''x'''
|
|
qwen:
|
|
managed-agent:
|
|
harness:
|
|
enabled: '\${QWEN_MANAGED_AGENT_HARNESS_ENABLED:false}'
|
|
base-url: '\${QWEN_MANAGED_AGENT_HARNESS_BASE_URL:http://127.0.0.1:4170}'
|
|
token: '\${QWEN_MANAGED_AGENT_HARNESS_TOKEN:}'
|
|
connect-timeout: '5s'
|
|
session-store:
|
|
enabled: '\${QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED:\${qwen.managed-agent.harness.enabled:false}}'
|
|
dispatch:
|
|
scan-delay: '1s'
|
|
`;
|
|
|
|
const METADATA_FIXTURE = {
|
|
groups: [
|
|
{ name: 'qwen.managed-agent' },
|
|
{ name: 'qwen.managed-agent.harness' },
|
|
{ name: 'qwen.managed-agent.harness.pool' },
|
|
{ name: 'qwen.managed-agent.session-store' },
|
|
],
|
|
properties: [
|
|
{
|
|
name: 'qwen.managed-agent.agent-revision',
|
|
type: 'java.lang.String',
|
|
defaultValue: '1',
|
|
},
|
|
{
|
|
name: 'qwen.managed-agent.harness.enabled',
|
|
type: 'java.lang.Boolean',
|
|
defaultValue: true,
|
|
},
|
|
{ name: 'qwen.managed-agent.harness.base-url', type: 'java.lang.String' },
|
|
{
|
|
name: 'qwen.managed-agent.harness.token',
|
|
type: 'java.lang.String',
|
|
defaultValue: 'unused',
|
|
},
|
|
{
|
|
name: 'qwen.managed-agent.harness.connect-timeout',
|
|
type: 'java.time.Duration',
|
|
defaultValue: '1s',
|
|
},
|
|
{
|
|
name: 'qwen.managed-agent.harness.request-timeout',
|
|
type: 'java.time.Duration',
|
|
defaultValue: '30s',
|
|
},
|
|
{
|
|
name: 'qwen.managed-agent.harness.pool.instances',
|
|
type: 'java.util.List<com.example.Instance>',
|
|
},
|
|
{
|
|
name: 'qwen.managed-agent.session-store.enabled',
|
|
type: 'java.lang.Boolean',
|
|
},
|
|
{
|
|
name: 'qwen.managed-agent.definition-models',
|
|
type: 'java.util.List<java.lang.String>',
|
|
defaultValue: ['a|b', 'c'],
|
|
},
|
|
],
|
|
};
|
|
|
|
function described(names, overrides = {}) {
|
|
const properties = {};
|
|
for (const name of names) {
|
|
properties[name] = { en: `About ${name}.`, zh: `关于 ${name}。` };
|
|
}
|
|
// A copy, so a test that edits it leaves the shared entries alone.
|
|
return structuredClone({
|
|
groups: groupDescriptions(),
|
|
properties: { ...properties, ...overrides },
|
|
});
|
|
}
|
|
|
|
function groupDescriptions() {
|
|
return Object.fromEntries(
|
|
METADATA_FIXTURE.groups.map(({ name }) => [
|
|
name,
|
|
{ en: `The ${name} group.`, zh: `${name} 分组。` },
|
|
]),
|
|
);
|
|
}
|
|
|
|
const ALL = [
|
|
...METADATA_FIXTURE.properties.map((property) => property.name),
|
|
'qwen.managed-agent.dispatch.scan-delay',
|
|
'spring.datasource.url',
|
|
];
|
|
const TYPED = {
|
|
'qwen.managed-agent.dispatch.scan-delay': {
|
|
en: 'How often.',
|
|
zh: '多久一次。',
|
|
type: 'duration',
|
|
},
|
|
'spring.datasource.url': {
|
|
en: 'The JDBC URL.',
|
|
zh: 'JDBC URL。',
|
|
type: 'string',
|
|
},
|
|
};
|
|
|
|
function reference(
|
|
overrides = {},
|
|
placeholders = new Map([['qwen.managed-agent.dispatch.scan-delay', '1s']]),
|
|
) {
|
|
return buildReference({
|
|
metadata: METADATA_FIXTURE,
|
|
yml: YML,
|
|
placeholders,
|
|
descriptions: described(ALL, { ...TYPED, ...overrides }),
|
|
});
|
|
}
|
|
|
|
function rowOf(page, label) {
|
|
return page.split('\n').find((line) => line.startsWith(`| \`${label}\` |`));
|
|
}
|
|
|
|
test('application.yml values come back by dotted path, unquoted', () => {
|
|
const values = parseApplicationYml(YML);
|
|
assert.equal(values.get('qwen.managed-agent.harness.connect-timeout'), '5s');
|
|
assert.equal(values.get('logging.pattern.correlation'), "%X{id} 'x'");
|
|
assert.equal(
|
|
values.get('qwen.managed-agent.session-store.enabled'),
|
|
'${QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED:${qwen.managed-agent.harness.enabled:false}}',
|
|
);
|
|
assert.equal(values.has('qwen.managed-agent.harness'), false);
|
|
});
|
|
|
|
test('application.yml constructs the reader does not know are refused', () => {
|
|
for (const text of [
|
|
'a:\n - one\n',
|
|
"a: {b: 'c'}\n",
|
|
'a: "b"\n',
|
|
"a: 'b\n",
|
|
]) {
|
|
assert.throws(() => parseApplicationYml(text), /unsupported YAML/, text);
|
|
}
|
|
});
|
|
|
|
test('a placeholder gives its name and fallback, nested or not', () => {
|
|
assert.deepEqual(parsePlaceholder('${A:b}'), { name: 'A', fallback: 'b' });
|
|
assert.deepEqual(parsePlaceholder('${A:}'), { name: 'A', fallback: '' });
|
|
assert.deepEqual(parsePlaceholder('${A}'), {
|
|
name: 'A',
|
|
fallback: undefined,
|
|
});
|
|
assert.deepEqual(parsePlaceholder('${A:http://h:1}'), {
|
|
name: 'A',
|
|
fallback: 'http://h:1',
|
|
});
|
|
assert.deepEqual(parsePlaceholder('${A:${b.c:false}}'), {
|
|
name: 'A',
|
|
fallback: '${b.c:false}',
|
|
});
|
|
// A placeholder in the name: its colon is not the fallback's.
|
|
assert.deepEqual(parsePlaceholder('${a.${b:c}:d}'), {
|
|
name: 'a.${b:c}',
|
|
fallback: 'd',
|
|
});
|
|
assert.equal(parsePlaceholder('${A}-${B}'), undefined);
|
|
assert.equal(parsePlaceholder('plain'), undefined);
|
|
});
|
|
|
|
test('each row takes the variable and default application.yml gives', () => {
|
|
const { problems, pages } = reference();
|
|
assert.deepEqual(problems, []);
|
|
const page = pages.en;
|
|
assert.equal(
|
|
rowOf(page, 'enabled'),
|
|
'| `enabled` | `QWEN_MANAGED_AGENT_HARNESS_ENABLED` | boolean | `false` | About qwen.managed-agent.harness.enabled. |',
|
|
);
|
|
// application.yml's fallback, not the server's default; an empty one says so.
|
|
assert.match(
|
|
rowOf(page, 'token'),
|
|
/\| `QWEN_MANAGED_AGENT_HARNESS_TOKEN` \| string \| \*\(empty\)\* \|/,
|
|
);
|
|
assert.match(rowOf(page, 'base-url'), /\| `http:\/\/127\.0\.0\.1:4170` \|/);
|
|
// A literal in application.yml, with no variable.
|
|
assert.match(
|
|
rowOf(page, 'connect-timeout'),
|
|
/^\| `connect-timeout` \| — \| duration \| `5s` \|/,
|
|
);
|
|
// Not in application.yml: the server's own default, or none.
|
|
assert.match(rowOf(page, 'request-timeout'), /\| — \| duration \| `30s` \|/);
|
|
assert.match(rowOf(page, 'instances'), /\| — \| YAML list \| — \|/);
|
|
assert.match(rowOf(page, 'definition-models'), /\| list \| `a\\\|b,c` \|/);
|
|
// A default that is another property's value.
|
|
assert.match(
|
|
page,
|
|
/\| `enabled` \| `QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED` \| boolean \| follows `qwen\.managed-agent\.harness\.enabled` \|/,
|
|
);
|
|
// Read only through a placeholder: its type comes from the descriptions.
|
|
assert.match(
|
|
rowOf(page, 'dispatch.scan-delay'),
|
|
/\| — \| duration \| `1s` \|/,
|
|
);
|
|
assert.match(
|
|
rowOf(page, 'spring.datasource.url'),
|
|
/\| `SPRING_DATASOURCE_URL` \| string \| `jdbc:mysql:\/\/127\.0\.0\.1:3306\/db` \| The JDBC URL\. \|/,
|
|
);
|
|
});
|
|
|
|
test('the Chinese page carries the same rows in its own words', () => {
|
|
const { pages } = reference();
|
|
const page = pages.zh;
|
|
assert.match(page, /^# Managed Agent 服务端配置参考\n/);
|
|
assert.match(page, /\| 属性 \| 环境变量 \| 类型 \| 默认值 \| 说明 \|/);
|
|
assert.match(
|
|
rowOf(page, 'token'),
|
|
/\| 字符串 \| \*(空)\* \| 关于 qwen\.managed-agent\.harness\.token。 \|/,
|
|
);
|
|
assert.match(
|
|
page,
|
|
/\| 布尔 \| 同 `qwen\.managed-agent\.harness\.enabled` \|/,
|
|
);
|
|
assert.match(
|
|
page,
|
|
/\[部署指南\]\(managed-agent-reference-deployment\.zh-CN\.md\)/,
|
|
);
|
|
});
|
|
|
|
test('sections follow the groups, the top level first and the rest in order', () => {
|
|
const { pages } = reference();
|
|
const headings = pages.en
|
|
.split('\n')
|
|
.filter((line) => line.startsWith('## '));
|
|
assert.deepEqual(headings, [
|
|
'## Database and listener',
|
|
'## `qwen.managed-agent`',
|
|
'## `qwen.managed-agent.harness`',
|
|
'## `qwen.managed-agent.harness.pool`',
|
|
'## `qwen.managed-agent.session-store`',
|
|
]);
|
|
// A setting outside every metadata group joins the top level.
|
|
const top = pages.en.split('## `qwen.managed-agent`')[1].split('\n## ')[0];
|
|
assert.match(top, /\| `dispatch\.scan-delay` \|/);
|
|
assert.match(top, /\| `agent-revision` \|/);
|
|
assert.match(
|
|
pages.en,
|
|
/## `qwen\.managed-agent\.harness`\n\nThe qwen\.managed-agent\.harness group\.\n/,
|
|
);
|
|
});
|
|
|
|
test('a setting declared with its variable joins the first section', () => {
|
|
const { problems, pages } = reference({
|
|
'server.port': {
|
|
en: 'The port.',
|
|
zh: '端口。',
|
|
type: 'integer',
|
|
variable: 'SERVER_PORT',
|
|
default: '8080',
|
|
},
|
|
});
|
|
assert.deepEqual(problems, []);
|
|
assert.equal(
|
|
rowOf(pages.en, 'server.port'),
|
|
'| `server.port` | `SERVER_PORT` | integer | `8080` | The port. |',
|
|
);
|
|
});
|
|
|
|
test('a setting without a usable description stops the pages', () => {
|
|
const missing = described(
|
|
ALL.filter((name) => name !== 'qwen.managed-agent.harness.token'),
|
|
TYPED,
|
|
);
|
|
missing.properties['qwen.managed-agent.harness.base-url'] = {
|
|
en: 'A | B.',
|
|
zh: '换\n行',
|
|
};
|
|
delete missing.properties['qwen.managed-agent.agent-revision'].zh;
|
|
delete missing.groups['qwen.managed-agent.harness.pool'];
|
|
missing.properties['qwen.managed-agent.harness.removed'] = {
|
|
en: 'Gone.',
|
|
zh: '已删。',
|
|
};
|
|
missing.groups['qwen.managed-agent.removed'] = { en: 'Gone.', zh: '已删。' };
|
|
delete missing.properties['qwen.managed-agent.dispatch.scan-delay'].type;
|
|
const result = buildReference({
|
|
metadata: METADATA_FIXTURE,
|
|
yml: `${YML} unbound: 'x'\n`,
|
|
placeholders: new Map([['qwen.managed-agent.dispatch.scan-delay', '1s']]),
|
|
descriptions: missing,
|
|
});
|
|
assert.equal(result.pages, undefined);
|
|
assert.deepEqual(
|
|
result.problems.sort(),
|
|
[
|
|
`${APPLICATION_YML} sets qwen.managed-agent.unbound, which the server neither binds nor reads`,
|
|
`${DESCRIPTIONS} describes group qwen.managed-agent.removed, which is no group`,
|
|
`${DESCRIPTIONS} describes qwen.managed-agent.harness.removed, which is no setting`,
|
|
`group qwen.managed-agent.harness.pool has no en description in ${DESCRIPTIONS}`,
|
|
`group qwen.managed-agent.harness.pool has no zh description in ${DESCRIPTIONS}`,
|
|
`qwen.managed-agent.agent-revision has no zh description in ${DESCRIPTIONS}`,
|
|
'qwen.managed-agent.dispatch.scan-delay needs a "type" in ' +
|
|
DESCRIPTIONS,
|
|
"qwen.managed-agent.harness.base-url's en description has a | or a line break",
|
|
"qwen.managed-agent.harness.base-url's zh description has a | or a line break",
|
|
`qwen.managed-agent.harness.token has no en description in ${DESCRIPTIONS}`,
|
|
`qwen.managed-agent.harness.token has no zh description in ${DESCRIPTIONS}`,
|
|
].sort(),
|
|
);
|
|
});
|
|
|
|
test('the Java sources give the placeholder settings and refuse two defaults', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'qma-config-reference-'));
|
|
try {
|
|
const one = 'first/src/main/java/a';
|
|
const two = 'second/src/main/java/a/b';
|
|
await mkdir(path.join(root, one), { recursive: true });
|
|
await mkdir(path.join(root, two), { recursive: true });
|
|
await mkdir(path.join(root, 'second/src/test/java'), { recursive: true });
|
|
await writeFile(
|
|
path.join(root, one, 'One.java'),
|
|
'@Scheduled(fixedDelayString = "${qwen.managed-agent.dispatch.scan-delay:1s}")\n' +
|
|
'@Value("${qwen.managed-agent.other.flag:false}") boolean flag;\n' +
|
|
'@Value("${unrelated.setting:1}") int unrelated;\n',
|
|
);
|
|
await writeFile(
|
|
path.join(root, two, 'Two.java'),
|
|
'@Scheduled(fixedDelayString = "${qwen.managed-agent.dispatch.scan-delay:1s}")\n' +
|
|
'@Scheduled(fixedDelayString = "${qwen.managed-agent.relay.scan-delay:2s}")\n',
|
|
);
|
|
await writeFile(
|
|
path.join(root, two, 'notes.txt'),
|
|
'${qwen.managed-agent.ignored:1}',
|
|
);
|
|
// A test's placeholder is no setting of the server.
|
|
await writeFile(
|
|
path.join(root, 'second/src/test/java/Fixture.java'),
|
|
'@Value("${qwen.managed-agent.fixture.only:1}") int fixture;\n',
|
|
);
|
|
let scanned = await scanJavaPlaceholders(root);
|
|
assert.deepEqual(scanned.problems, []);
|
|
assert.deepEqual([...scanned.placeholders].sort(), [
|
|
['qwen.managed-agent.dispatch.scan-delay', '1s'],
|
|
['qwen.managed-agent.other.flag', 'false'],
|
|
['qwen.managed-agent.relay.scan-delay', '2s'],
|
|
]);
|
|
await writeFile(
|
|
path.join(root, two, 'Three.java'),
|
|
'@Scheduled(fixedDelayString = "${qwen.managed-agent.relay.scan-delay:5s}")\n',
|
|
);
|
|
scanned = await scanJavaPlaceholders(root);
|
|
assert.deepEqual(scanned.problems, [
|
|
'qwen.managed-agent.relay.scan-delay has two defaults in the Java sources: 5s and 2s',
|
|
]);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('--check fails on a stale page, and passes once the pages are written', async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), 'qma-config-reference-'));
|
|
const run = (...args) =>
|
|
spawnSync(process.execPath, [SCRIPT, ...args], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
});
|
|
try {
|
|
for (const file of [METADATA, APPLICATION_YML, DESCRIPTIONS, PAGES.en]) {
|
|
await mkdir(path.join(root, path.dirname(file)), { recursive: true });
|
|
}
|
|
const sources = path.join(root, JAVA_SOURCES, 'core/src/main/java/x');
|
|
await mkdir(sources, { recursive: true });
|
|
await writeFile(
|
|
path.join(sources, 'Scan.java'),
|
|
'@Scheduled(fixedDelayString = "${qwen.managed-agent.dispatch.scan-delay:1s}")\n',
|
|
);
|
|
await writeFile(path.join(root, APPLICATION_YML), YML);
|
|
await writeFile(
|
|
path.join(root, DESCRIPTIONS),
|
|
JSON.stringify(described(ALL, TYPED)),
|
|
);
|
|
|
|
let result = run('--check');
|
|
assert.equal(result.status, 1);
|
|
assert.match(
|
|
result.stderr,
|
|
/cannot read .*spring-configuration-metadata\.json.*build the server first/,
|
|
);
|
|
|
|
await writeFile(
|
|
path.join(root, METADATA),
|
|
JSON.stringify(METADATA_FIXTURE),
|
|
);
|
|
result = run('--check');
|
|
assert.equal(result.status, 1);
|
|
assert.match(
|
|
result.stderr,
|
|
/docs\/users\/managed-agent-configuration-reference\.md and docs\/users\/managed-agent-configuration-reference\.zh-CN\.md are out of date; run: mvn /,
|
|
);
|
|
|
|
result = run();
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(
|
|
result.stdout,
|
|
/Wrote docs\/users\/managed-agent-configuration-reference\.md/,
|
|
);
|
|
assert.equal(
|
|
await readFile(path.join(root, PAGES.en), 'utf8'),
|
|
reference().pages.en,
|
|
);
|
|
assert.equal(run('--check').status, 0);
|
|
|
|
await writeFile(path.join(root, PAGES.zh), 'edited by hand\n');
|
|
result = run('--check');
|
|
assert.equal(result.status, 1);
|
|
assert.match(result.stderr, /zh-CN\.md is out of date/);
|
|
|
|
const descriptions = described(ALL.slice(1), TYPED);
|
|
await writeFile(
|
|
path.join(root, DESCRIPTIONS),
|
|
JSON.stringify(descriptions),
|
|
);
|
|
result = run();
|
|
assert.equal(result.status, 1);
|
|
assert.match(
|
|
result.stderr,
|
|
/qwen\.managed-agent\.agent-revision has no en description/,
|
|
);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|