Skip to content

Commit ef463e8

Browse files
committed
test(e2e): 优化检索结果标记召回判断并完善服务调优用例
- 新增节点召回标记判断函数 nodesRecallMarker,避免误判 marker 出现位置 - 将所有相关轮询断言替换为基于 nodesRecallMarker 的更严格判断 - 调整删除测试中对误伤判断的断言逻辑,确保准确检测召回标记 - 扩展服务调优用例,增加描述和温度参数调优测试,验证配置持久化 - 添加通过配置文件更新 kb_search_configs 并校验嵌套配置修改生效 - 部署后验证发布版本配置正确包含所有调优项 - 更新流程注释与断言提示,提升测试用例可读性和覆盖度
1 parent 8ee2c37 commit ef463e8

6 files changed

Lines changed: 192 additions & 26 deletions

File tree

packages/commands/tests/e2e/knowledge/journeys/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
| J1 | 冷启动:建库并获得首个答案 | `j1-cold-start.e2e.test.ts` | retrieve/search(beta) 召回标记词(硬);chat 回答引用标记词(软) |
1111
| J2 | 内容运维:文档增删的可见性 | `j2-content-ops.e2e.test.ts` | 双标记词命中 → 删除其一后 markerB 消失且 markerA 仍在(硬) |
1212
| J3 | 检索精修:chunk 排除生效 | `j3-chunk-tuning.e2e.test.ts` | exclude 后排除标志生效(硬);include 恢复(软)。retrieve 不过滤被排除 chunk,以 `is_displayed_chunk_content` 标志为准 |
13-
| J4 | 服务调优:草稿→修改→发布 | `j4-service-tuning.e2e.test.ts` | beta 草稿可用、update 落库、发布后正式版可用(硬) |
13+
| J4 | 服务调优:草稿→修改→发布 | `j4-service-tuning.e2e.test.ts` | beta 草稿可用、update 落库、发布后正式版详情携带修改且可用(硬) |
1414
| J5 | 数据面治理:collection/category/file | `j5-data-plane.e2e.test.ts` | 自建类目内文件可见/可删,类目删后消失(硬) |
1515
| J6 | 退场清理:删库验证消失 | 复用 [`../knowledge-kb-delete.e2e.test.ts`](../knowledge-kb-delete.e2e.test.ts) live 链 | delete 后 list 不再包含(硬) |
1616

packages/commands/tests/e2e/knowledge/journeys/j1-cold-start.e2e.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
cleanupKbFixture,
1111
createJourneyReporter,
1212
createKbWithDocs,
13+
nodesRecallMarker,
1314
patchSearchServiceRetrievalConfig,
1415
pollUntil,
1516
uniqueMarker,
@@ -44,7 +45,7 @@ describe.skipIf(!isKbAdminE2EReady())("journey J1: 冷启动首答 (live, 自清
4445
"--output",
4546
"json",
4647
]),
47-
(run) => run.exitCode === 0 && run.stdout.includes(marker),
48+
(run) => run.exitCode === 0 && nodesRecallMarker(run.stdout, marker),
4849
{ timeoutMs: 180_000, intervalMs: 15_000 },
4950
);
5051
reporter.recordNote(`retrieve 轮询 ${retrievePoll.attempts} 次`);
@@ -100,7 +101,7 @@ describe.skipIf(!isKbAdminE2EReady())("journey J1: 冷启动首答 (live, 自清
100101
"--output",
101102
"json",
102103
]),
103-
(run) => run.exitCode === 0 && run.stdout.includes(marker),
104+
(run) => run.exitCode === 0 && nodesRecallMarker(run.stdout, marker),
104105
{ timeoutMs: 120_000, intervalMs: 15_000 },
105106
);
106107
reporter.recordNote(`search 轮询 ${searchPoll.attempts} 次`);

packages/commands/tests/e2e/knowledge/journeys/j2-content-ops.e2e.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
cleanupKbFixture,
1111
createJourneyReporter,
1212
createKbWithDocs,
13+
nodesRecallMarker,
1314
pollUntil,
1415
uniqueMarker,
1516
type KbFixture,
@@ -113,7 +114,7 @@ describe.skipIf(!isKbAdminE2EReady())("journey J2: 内容运维 (live, 自清理
113114
"--output",
114115
"json",
115116
]),
116-
(run) => run.exitCode === 0 && run.stdout.includes(marker),
117+
(run) => run.exitCode === 0 && nodesRecallMarker(run.stdout, marker),
117118
{ timeoutMs: 180_000, intervalMs: 15_000 },
118119
);
119120
const pollA = await retrieveMarker(markerA, "retrieve markerA");
@@ -165,7 +166,10 @@ describe.skipIf(!isKbAdminE2EReady())("journey J2: 内容运维 (live, 自清理
165166
"json",
166167
]);
167168
expect(stillA.exitCode, stillA.stderr).toBe(0);
168-
expect(stillA.stdout, `doc delete 误伤: ${markerA} 不再召回`).toContain(markerA);
169+
expect(
170+
nodesRecallMarker(stillA.stdout, markerA),
171+
`doc delete 误伤: ${markerA} 不再召回`,
172+
).toBe(true);
169173
} finally {
170174
await cleanupKbFixture(reporter, JOURNEY_J2_ROUTES, fixture, workspaceId);
171175
reporter.finalize();

packages/commands/tests/e2e/knowledge/journeys/j3-chunk-tuning.e2e.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
cleanupKbFixture,
1313
createJourneyReporter,
1414
createKbWithDocs,
15+
nodesRecallMarker,
1516
pollUntil,
1617
uniqueMarker,
1718
type KbFixture,
@@ -75,7 +76,7 @@ describe.skipIf(!isKbAdminE2EReady())("journey J3: 检索精修 (live, 自清理
7576
];
7677
const baseline = await pollUntil(
7778
() => reporter.runStep("retrieve baseline", JOURNEY_J3_ROUTES, retrieveArgs),
78-
(run) => run.exitCode === 0 && run.stdout.includes(marker),
79+
(run) => run.exitCode === 0 && nodesRecallMarker(run.stdout, marker),
7980
{ timeoutMs: 180_000, intervalMs: 15_000 },
8081
);
8182
expect(baseline.satisfied, `基线未召回标记词 ${marker}`).toBe(true);

packages/commands/tests/e2e/knowledge/journeys/j4-service-tuning.e2e.test.ts

Lines changed: 157 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
// J4 service tuning: draft (beta) usable → config change persisted → released version usable after deploy.
1+
// J4 service tuning: draft (beta) usable → config change persisted → released version carries the change after deploy.
22
// create + service create (search, initial draft/beta) → search --agent-version beta recalls (hard)
3-
// → service update --description → service get asserts the change (hard) → deploy → released search recalls (hard).
3+
// → service update --description/--temperature (scalar merge path) → get (beta) asserts (hard)
4+
// → service update --config-file tweaking kb_search_configs (whole-replace path) → get (beta) asserts
5+
// the nested change AND that the replace kept the scalar tuning (hard) → deploy
6+
// → get (released version) asserts both changes landed in the published version (hard) → released search recalls (hard).
7+
import { mkdtempSync, writeFileSync } from "node:fs";
8+
import { tmpdir } from "node:os";
9+
import { join } from "node:path";
410
import { describe, expect, test } from "vite-plus/test";
511
import { isKbAdminE2EReady, parseStdoutJson } from "../../helpers.ts";
612
import { JOURNEY_J4_ROUTES } from "../../topic-routes.ts";
713
import {
814
cleanupKbFixture,
915
createJourneyReporter,
1016
createKbWithDocs,
17+
nodesRecallMarker,
1118
patchSearchServiceRetrievalConfig,
1219
pollUntil,
1320
uniqueMarker,
@@ -17,7 +24,7 @@ import {
1724
describe.skipIf(!isKbAdminE2EReady())("journey J4: 问答服务调优 (live, 自清理)", () => {
1825
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
1926

20-
test("draft 可用 → update 落库 → deploy → 正式版可用", async () => {
27+
test("draft 可用 → update 落库 → deploy → 正式版详情携带修改且可用", async () => {
2128
const reporter = createJourneyReporter(import.meta.url);
2229
const marker = uniqueMarker("j4");
2330
const fixture: Partial<KbFixture> = {};
@@ -71,25 +78,33 @@ describe.skipIf(!isKbAdminE2EReady())("journey J4: 问答服务调优 (live, 自
7178
];
7279
const betaPoll = await pollUntil(
7380
() => reporter.runStep("search (beta)", JOURNEY_J4_ROUTES, betaSearchArgs),
74-
(run) => run.exitCode === 0 && run.stdout.includes(marker),
81+
(run) => run.exitCode === 0 && nodesRecallMarker(run.stdout, marker),
7582
{ timeoutMs: 180_000, intervalMs: 15_000 },
7683
);
7784
reporter.recordNote(`beta search 轮询 ${betaPoll.attempts} 次`);
7885
expect(betaPoll.satisfied, `search(beta) 未召回标记词 ${marker}`).toBe(true);
7986

80-
// 3) update the description (top-level scalar, valid for the search scene too) → get asserts persistence (hard)
87+
// 3) tune the draft: description (top-level) + temperature (config-level scalar,
88+
// read-merge-write keeps the backfilled retrieval params) → get asserts both (hard)
8189
const newDescription = `journey j4 tuned at ${Date.now()}`;
82-
const updateRun = await reporter.runStep("service update --description", JOURNEY_J4_ROUTES, [
83-
"knowledge",
84-
"service",
85-
"update",
86-
"--agent-id",
87-
agentId,
88-
"--description",
89-
newDescription,
90-
"--workspace-id",
91-
workspaceId,
92-
]);
90+
const tunedTemperature = 0.55;
91+
const updateRun = await reporter.runStep(
92+
"service update --description --temperature",
93+
JOURNEY_J4_ROUTES,
94+
[
95+
"knowledge",
96+
"service",
97+
"update",
98+
"--agent-id",
99+
agentId,
100+
"--description",
101+
newDescription,
102+
"--temperature",
103+
String(tunedTemperature),
104+
"--workspace-id",
105+
workspaceId,
106+
],
107+
);
93108
expect(updateRun.exitCode, updateRun.stderr).toBe(0);
94109

95110
const getRun = await reporter.runStep("service get (beta)", JOURNEY_J4_ROUTES, [
@@ -106,10 +121,88 @@ describe.skipIf(!isKbAdminE2EReady())("journey J4: 问答服务调优 (live, 自
106121
"json",
107122
]);
108123
expect(getRun.exitCode, getRun.stderr).toBe(0);
109-
const getData = parseStdoutJson<{ data?: { agent_desc?: string } }>(getRun.stdout);
124+
const getData = parseStdoutJson<{
125+
data?: {
126+
agent_desc?: string;
127+
agent_details?: Array<{
128+
agent_config?: { temperature?: number } & Record<string, unknown>;
129+
}>;
130+
};
131+
}>(getRun.stdout);
110132
expect(getData.data?.agent_desc).toBe(newDescription);
133+
expect(getData.data?.agent_details?.[0]?.agent_config?.temperature).toBe(tunedTemperature);
134+
135+
// 3.5) complex nested tuning via --config-file (whole-replace path, distinct
136+
// from the scalar merge path above): read the current beta config, tweak
137+
// kb_search_configs, write it back — then assert the nested change landed
138+
// AND the replace kept the scalar tuning intact
139+
const tunedDenseTopK = 66; // distinctive value, still recall-safe (loose top-k)
140+
const betaConfig = getData.data?.agent_details?.[0]?.agent_config as
141+
| ({ kb_search_configs?: Array<Record<string, unknown>> } & Record<string, unknown>)
142+
| undefined;
143+
expect(betaConfig?.kb_search_configs?.length, "beta 配置应含 kb_search_configs").toBeTruthy();
144+
for (const kbConfig of betaConfig!.kb_search_configs!) {
145+
kbConfig.dense_similarity_top_k = tunedDenseTopK;
146+
}
147+
const configDir = mkdtempSync(join(tmpdir(), "j4-config-"));
148+
const configFile = join(configDir, "agent-config.json");
149+
writeFileSync(configFile, JSON.stringify(betaConfig));
150+
const configUpdateRun = await reporter.runStep(
151+
"service update --config-file (kb_search_configs)",
152+
JOURNEY_J4_ROUTES,
153+
[
154+
"knowledge",
155+
"service",
156+
"update",
157+
"--agent-id",
158+
agentId,
159+
"--config-file",
160+
configFile,
161+
"--workspace-id",
162+
workspaceId,
163+
],
164+
);
165+
expect(configUpdateRun.exitCode, configUpdateRun.stderr).toBe(0);
166+
167+
const betaAfterConfigRun = await reporter.runStep(
168+
"service get (beta, after config-file)",
169+
JOURNEY_J4_ROUTES,
170+
[
171+
"knowledge",
172+
"service",
173+
"get",
174+
"--agent-id",
175+
agentId,
176+
"--agent-version",
177+
"beta",
178+
"--workspace-id",
179+
workspaceId,
180+
"--output",
181+
"json",
182+
],
183+
);
184+
expect(betaAfterConfigRun.exitCode, betaAfterConfigRun.stderr).toBe(0);
185+
const betaAfterConfig = parseStdoutJson<{
186+
data?: {
187+
agent_details?: Array<{
188+
agent_config?: {
189+
temperature?: number;
190+
kb_search_configs?: Array<{ dense_similarity_top_k?: number }>;
191+
};
192+
}>;
193+
};
194+
}>(betaAfterConfigRun.stdout).data?.agent_details?.[0]?.agent_config;
195+
expect(
196+
betaAfterConfig?.kb_search_configs?.[0]?.dense_similarity_top_k,
197+
"kb_search_configs 嵌套修改未落库",
198+
).toBe(tunedDenseTopK);
199+
expect(betaAfterConfig?.temperature, "config-file 整体替换不应冲掉已调优的 temperature").toBe(
200+
tunedTemperature,
201+
);
111202

112-
// 4) deploy → released search (without --agent-version) recalls (hard)
203+
// 4) deploy → the published version's detail must carry the tuned config (hard):
204+
// search alone only proves the released service responds, not that the
205+
// change actually shipped
113206
const deployRun = await reporter.runStep("service deploy", JOURNEY_J4_ROUTES, [
114207
"knowledge",
115208
"service",
@@ -122,8 +215,52 @@ describe.skipIf(!isKbAdminE2EReady())("journey J4: 问答服务调优 (live, 自
122215
"--quiet",
123216
]);
124217
expect(deployRun.exitCode, deployRun.stderr).toBe(0);
125-
reporter.recordNote(`deploy 版本号: ${deployRun.stdout.trim().split("\n").pop() ?? "?"}`);
218+
const deployedVersion = deployRun.stdout.trim().split("\n").pop() ?? "";
219+
expect(deployedVersion, "deploy 应输出新版本号").toBeTruthy();
220+
reporter.recordNote(`deploy 版本号: ${deployedVersion}`);
221+
222+
const releasedGetRun = await reporter.runStep(
223+
`service get (released v${deployedVersion})`,
224+
JOURNEY_J4_ROUTES,
225+
[
226+
"knowledge",
227+
"service",
228+
"get",
229+
"--agent-id",
230+
agentId,
231+
"--agent-version",
232+
deployedVersion,
233+
"--workspace-id",
234+
workspaceId,
235+
"--output",
236+
"json",
237+
],
238+
);
239+
expect(releasedGetRun.exitCode, releasedGetRun.stderr).toBe(0);
240+
const releasedData = parseStdoutJson<{
241+
data?: {
242+
agent_details?: Array<{
243+
agent_version?: string;
244+
agent_config?: {
245+
temperature?: number;
246+
kb_search_configs?: Array<{ dense_similarity_top_k?: number }>;
247+
};
248+
}>;
249+
};
250+
}>(releasedGetRun.stdout);
251+
const releasedDetail = releasedData.data?.agent_details?.find(
252+
(detail) => detail.agent_version === deployedVersion,
253+
);
254+
expect(releasedDetail, `get 应返回已发布版本 ${deployedVersion} 的详情`).toBeTruthy();
255+
expect(releasedDetail?.agent_config?.temperature, "调优的 temperature 未进入正式版配置").toBe(
256+
tunedTemperature,
257+
);
258+
expect(
259+
releasedDetail?.agent_config?.kb_search_configs?.[0]?.dense_similarity_top_k,
260+
"调优的 kb_search_configs 未进入正式版配置",
261+
).toBe(tunedDenseTopK);
126262

263+
// 5) released search (without --agent-version) recalls (hard)
127264
const releasedPoll = await pollUntil(
128265
() =>
129266
reporter.runStep("search (released)", JOURNEY_J4_ROUTES, [
@@ -138,7 +275,7 @@ describe.skipIf(!isKbAdminE2EReady())("journey J4: 问答服务调优 (live, 自
138275
"--output",
139276
"json",
140277
]),
141-
(run) => run.exitCode === 0 && run.stdout.includes(marker),
278+
(run) => run.exitCode === 0 && nodesRecallMarker(run.stdout, marker),
142279
{ timeoutMs: 120_000, intervalMs: 15_000 },
143280
);
144281
reporter.recordNote(`正式版 search 轮询 ${releasedPoll.attempts} 次`);

packages/commands/tests/e2e/knowledge/journeys/journey-helpers.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,29 @@ export async function cleanupKbFixture(
377377
}
378378
}
379379

380+
// ---- Marker-recall predicate (strict) ----
381+
382+
/**
383+
* Positive recall check for retrieve/search JSON output: the marker must appear
384+
* inside a returned node's text (not merely anywhere in the raw output — doc_name
385+
* or other metadata echoing the marker must not count as recall).
386+
* Negative assertions ("marker gone") should keep scanning the whole stdout:
387+
* absence from the entire output is the stronger guarantee.
388+
*/
389+
export function nodesRecallMarker(stdout: string, marker: string): boolean {
390+
try {
391+
const payload = JSON.parse(stdout) as {
392+
data?: { nodes?: Array<{ text?: string; metadata?: { content?: string } }> };
393+
};
394+
return (payload.data?.nodes ?? []).some((node) =>
395+
(node.metadata?.content ?? node.text ?? "").includes(marker),
396+
);
397+
} catch {
398+
// Mid-poll runs may fail before emitting JSON — treat as "not recalled yet"
399+
return false;
400+
}
401+
}
402+
380403
// ---- Backfill required retrieval parameters for search services (server gotcha) ----
381404
// The minimal kb_search_configs created by service create --index-id is missing
382405
// required fields like rerank_min_score / dense_similarity_top_k, so the search

0 commit comments

Comments
 (0)