Skip to content

Commit 4086da5

Browse files
committed
docs(knowledge): 修改多处参数描述为“精确匹配”并完善错误处理说明
- 将 category-list、file-list、service-list 等命令参数的描述更新为强调“精确匹配” - file-list 命令中 --name 参数改为匹配不含扩展名的精确文件名,并在备注中补充说明 - kb-stats 命令严格校验时间格式,增强无效格式报错及提示 - 丰富知识相关测试用例,增加对不存在 ID 的服务端错误传递和非零退出的正向断言 - 优化知识文档上传测试,支持跳过 node_modules/.git 文件夹及显示被跳过文件详情 - 修正知识搜索及聊天流程中未发布版本号引发的服务器拒绝场景测试 - 更新知识模块命令文档,补充参数要求和用法提示,提升用户指引明确度
1 parent d5c4bd3 commit 4086da5

17 files changed

Lines changed: 459 additions & 41 deletions

packages/commands/src/commands/knowledge/category-list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ const CATEGORY_LIST_FLAGS = {
1313
collectionId: {
1414
type: "string",
1515
valueHint: "<id>",
16-
description: "Filter by collection ID",
16+
description: "Filter by exact collection ID",
1717
},
1818
parentId: {
1919
type: "string",
2020
valueHint: "<id>",
21-
description: "List sub-categories of this category",
21+
description: "List sub-categories of this exact parent category",
2222
},
2323
name: {
2424
type: "string",

packages/commands/src/commands/knowledge/file-list.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ const FILE_LIST_FLAGS = {
1313
categoryId: {
1414
type: "string",
1515
valueHint: "<id>",
16-
description: "Category to list (find ids via the category list command)",
16+
description: "Category to list (find ids via the category list command); exact match",
1717
required: true,
1818
},
1919
name: {
2020
type: "string",
2121
valueHint: "<text>",
22-
description: "Filter by file name",
22+
description: "Filter by exact file name without its extension (a.md → pass a)",
2323
},
2424
fileId: {
2525
type: "array",
2626
valueHint: "<id>",
27-
description: "Filter by file ID (repeatable)",
27+
description: "Filter by exact file ID (repeatable)",
2828
},
2929
nextToken: {
3030
type: "string",
@@ -46,6 +46,7 @@ export default defineCommand({
4646
flags: FILE_LIST_FLAGS,
4747
notes: [
4848
"A real category id is required — the default value is not resolved here. Find the id via the category list command.",
49+
"--name matches the exact file name without its extension (for a.md pass a); partial keywords return no results.",
4950
"Pagination is cursor-based: reuse the printed next token to continue.",
5051
],
5152
exampleArgs: [

packages/commands/src/commands/knowledge/kb-stats.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ export function toEpochSecondsString(input: string): string {
3838
// Digits-only input is treated as Unix seconds; 13-digit millisecond timestamps are reduced to seconds
3939
return input.length >= 13 ? String(Math.floor(Number(input) / 1000)) : input;
4040
}
41-
const parsedMs = Date.parse(input);
41+
// Non-numeric input must be a full ISO date (YYYY-MM-DD, optionally with a time
42+
// part). Date.parse alone is too lenient — V8 silently reads truncated input
43+
// like "2026-" as Jan 1st, which would query a misleading range.
44+
const parsedMs = /^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(input) ? Date.parse(input) : Number.NaN;
4245
if (Number.isNaN(parsedMs)) {
4346
throw new BailianError(
4447
`Invalid time value: ${input}`,

packages/commands/src/commands/knowledge/service-list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const SERVICE_LIST_FLAGS = {
3434
indexId: {
3535
type: "string",
3636
valueHint: "<id>",
37-
description: "Filter by linked knowledge base (pipeline) ID",
37+
description: "Filter by exact linked knowledge base (pipeline) ID",
3838
},
3939
...PAGE_FLAGS,
4040
...WORKSPACE_FLAG,

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,30 @@ describe.skipIf(!isKbAdminE2EReady())("journey J1: 冷启动首答 (live, 自清
152152
answerText.includes(marker),
153153
answerText || "(empty)",
154154
);
155+
156+
// 5) Negative branch: calling a version number that was never published —
157+
// the server rejects (HTTP 400, verified live) and the CLI exits non-zero
158+
// with the error passed through (the version set is server-side state)
159+
const chatBadVersionRun = await reporter.runStep(
160+
"chat unpublished version (expect reject)",
161+
JOURNEY_J1_ROUTES,
162+
[
163+
"knowledge",
164+
"chat",
165+
"--message",
166+
`What is ${marker}?`,
167+
"--agent-id",
168+
chatAgentId,
169+
"--agent-version",
170+
"999",
171+
"--workspace-id",
172+
workspaceId,
173+
"--output",
174+
"json",
175+
],
176+
);
177+
expect(chatBadVersionRun.exitCode, "未发布版本号应被服务端拒绝").not.toBe(0);
178+
expect(chatBadVersionRun.stderr).toMatch(/HTTP 400/);
155179
} finally {
156180
for (const agentId of serviceAgentIds) {
157181
const deleteRun = await reporter.runStep(

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ describe.skipIf(!isKbAdminE2EReady())("journey J2: 内容运维 (live, 自清理
6767
expect(docStatusRun.exitCode, docStatusRun.stderr).toBe(0);
6868
expect(docStatusRun.stdout.trim()).toBe("COMPLETED");
6969

70+
// 2.1) Negative branch: querying a job id that does not exist on this base —
71+
// the server error (Index.IndexJobNotExist, verified live) passes through
72+
// verbatim with a non-zero exit
73+
const badJobStatusRun = await reporter.runStep(
74+
"doc status unknown job (expect reject)",
75+
JOURNEY_J2_ROUTES,
76+
[
77+
"knowledge",
78+
"doc",
79+
"status",
80+
"--index-id",
81+
kb.indexId,
82+
"--job-id",
83+
"job_nonexistent_e2e",
84+
"--workspace-id",
85+
workspaceId,
86+
],
87+
);
88+
expect(badJobStatusRun.exitCode, "不存在的 job id 应被服务端拒绝").not.toBe(0);
89+
expect(badJobStatusRun.stderr).toMatch(/not exist/i);
90+
7091
// 2.5) knowledge stats — monitor endpoint returns valid structure (hard)
7192
const statsRun = await reporter.runStep("kb stats", JOURNEY_J2_ROUTES, [
7293
"knowledge",

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,39 @@ describe.skipIf(!isKbAdminE2EReady())("journey J3: 检索精修 (live, 自清理
158158
restored.satisfied,
159159
`attempts=${restored.attempts} exit=${restored.value.exitCode}`,
160160
);
161+
162+
// 5) Negative branch: deleting a chunk id that does not exist on this base —
163+
// the server rejects (Index.InvalidParameter, verified live) and the error
164+
// passes through verbatim; the marker chunk must remain recallable afterwards
165+
const badDeleteRun = await reporter.runStep(
166+
"chunk delete unknown id (expect reject)",
167+
JOURNEY_J3_ROUTES,
168+
[
169+
"knowledge",
170+
"chunk",
171+
"delete",
172+
"--index-id",
173+
kb.indexId,
174+
"--chunk-id",
175+
"chunk_nonexistent_e2e_0000",
176+
"--yes",
177+
"--workspace-id",
178+
workspaceId,
179+
],
180+
);
181+
expect(badDeleteRun.exitCode, "不存在的 chunk id 应被服务端拒绝").not.toBe(0);
182+
expect(badDeleteRun.stderr).toMatch(/invalid|not exist/i);
183+
184+
const afterBadDelete = await reporter.runStep(
185+
"retrieve after failed delete",
186+
JOURNEY_J3_ROUTES,
187+
retrieveArgs,
188+
);
189+
expect(afterBadDelete.exitCode, afterBadDelete.stderr).toBe(0);
190+
expect(
191+
nodesRecallMarker(afterBadDelete.stdout, marker),
192+
`失败的删除误伤: ${marker} 不再召回`,
193+
).toBe(true);
161194
} finally {
162195
await cleanupKbFixture(reporter, JOURNEY_J3_ROUTES, fixture, workspaceId);
163196
reporter.finalize();

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,30 @@ describe.skipIf(!isKbAdminE2EReady())("journey J4: 问答服务调优 (live, 自
280280
);
281281
reporter.recordNote(`正式版 search 轮询 ${releasedPoll.attempts} 次`);
282282
expect(releasedPoll.satisfied, `发布后正式版 search 未召回 ${marker}`).toBe(true);
283+
284+
// 6) Negative branch: calling a version number that was never published —
285+
// the server rejects (RagAgentNotExist, verified live) and the CLI exits
286+
// non-zero with the error passed through (the version set is server-side state)
287+
const badVersionSearchRun = await reporter.runStep(
288+
"search unpublished version (expect reject)",
289+
JOURNEY_J4_ROUTES,
290+
[
291+
"knowledge",
292+
"search",
293+
"--query",
294+
marker,
295+
"--agent-id",
296+
agentId,
297+
"--agent-version",
298+
"999",
299+
"--workspace-id",
300+
workspaceId,
301+
"--output",
302+
"json",
303+
],
304+
);
305+
expect(badVersionSearchRun.exitCode, "未发布版本号应被服务端拒绝").not.toBe(0);
306+
expect(badVersionSearchRun.stderr).toMatch(/Agent application not found|RagAgentNotExist/i);
283307
} finally {
284308
if (agentId) {
285309
const deleteRun = await reporter.runStep("cleanup: service delete", JOURNEY_J4_ROUTES, [

packages/commands/tests/e2e/knowledge/journeys/j5-data-plane.e2e.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,26 @@ describe.skipIf(!isConnectorE2EReady())("journey J5: 数据面治理 (live, 自
141141
]);
142142
expect(fileDeleteRun.exitCode, fileDeleteRun.stderr).toBe(0);
143143
reporter.markCleaned(fileId);
144+
145+
// 3.1) Negative branch: deleting the same file again — it no longer exists,
146+
// so the server rejects ("Cant find out file", verified live) and the error
147+
// passes through verbatim with a non-zero exit
148+
const repeatDeleteRun = await reporter.runStep(
149+
"file delete again (expect reject)",
150+
JOURNEY_J5_ROUTES,
151+
[
152+
"knowledge",
153+
"file",
154+
"delete",
155+
"--file-id",
156+
fileId,
157+
"--yes",
158+
"--workspace-id",
159+
workspaceId,
160+
],
161+
);
162+
expect(repeatDeleteRun.exitCode, "重复删除已不存在的文件应被服务端拒绝").not.toBe(0);
163+
expect(repeatDeleteRun.stderr).toMatch(/cant find|InvalidParameter/i);
144164
} finally {
145165
if (categoryId) {
146166
// 4) Clean up the self-created category + list verifies removal

packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES,
1818
KNOWLEDGE_KB_DELETE_ROUTES,
1919
} from "../topic-routes.ts";
20-
import { deleteKbWithRetry } from "./journeys/journey-helpers.ts";
20+
import { deleteKbWithRetry, pollUntil } from "./journeys/journey-helpers.ts";
2121

2222
interface DryRunBody {
2323
endpoint?: string;
@@ -854,6 +854,21 @@ describe.skipIf(!isKbAdminE2EReady())(
854854
() => {
855855
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
856856

857+
test("file get 不存在的 file-id: 服务端错误原样透传 (非零退出)", async () => {
858+
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
859+
"knowledge",
860+
"file",
861+
"get",
862+
"--file-id",
863+
"file_nonexistent_e2e_0000",
864+
"--workspace-id",
865+
workspaceId,
866+
]);
867+
expect(exitCode).not.toBe(0);
868+
// Verified live: HTTP 200 + InvalidParameter with a "cant find" message
869+
expect(stderr).toMatch(/cant find|InvalidParameter/i);
870+
}, 60_000);
871+
857872
test("collection get/create live: 幂等复用固定名测试集合", async () => {
858873
// collection has no delete API — reuse a fixed name idempotently instead of creating
859874
// new ones (the legacy name e2e-test-connector is kept: that idempotent resource
@@ -1521,8 +1536,25 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
15211536
const categoryId = categoryAddRun.stdout.trim();
15221537
expect(categoryId).toMatch(/^cate_/);
15231538

1539+
// ── empty-result contract: a freshly created category has no files —
1540+
// text mode prints the friendly empty line and exits 0 (empty ≠ error) ──
1541+
const emptyCategoryListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
1542+
"knowledge",
1543+
"file",
1544+
"list",
1545+
"--category-id",
1546+
categoryId,
1547+
"--workspace-id",
1548+
workspaceId,
1549+
]);
1550+
expect(emptyCategoryListRun.exitCode, emptyCategoryListRun.stderr).toBe(0);
1551+
expect(emptyCategoryListRun.stdout).toMatch(/No files found\./);
1552+
15241553
const fixtureDir = mkdtempSync(join(tmpdir(), "param-e2e-"));
1525-
const filePath = join(fixtureDir, `param-${Date.now()}.md`);
1554+
// Server contract (verified live): listFile fileName filters by the exact
1555+
// file name WITHOUT extension — keep the stem for the --name assertion below
1556+
const fileStem = `param-${Date.now()}`;
1557+
const filePath = join(fixtureDir, `${fileStem}.md`);
15261558
writeFileSync(filePath, "# e2e param file\n");
15271559
const uploadRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
15281560
"knowledge",
@@ -1541,21 +1573,30 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
15411573
let indexId: string | undefined;
15421574

15431575
try {
1544-
// ── P2: file list --name (模糊匹配) ──
1545-
const fileListByNameRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
1546-
"knowledge",
1547-
"file",
1548-
"list",
1549-
"--category-id",
1550-
categoryId,
1551-
"--name",
1552-
"param-",
1553-
"--workspace-id",
1554-
workspaceId,
1555-
"--quiet",
1556-
]);
1557-
expect(fileListByNameRun.exitCode, fileListByNameRun.stderr).toBe(0);
1558-
expect(fileListByNameRun.stdout.trim().split("\n")).toContain(fileId);
1576+
// ── P2: file list --name —— exact match on the extension-less file name
1577+
// (e.g. a.md → pass a); fuzzy/partial keywords return an empty set.
1578+
// Poll to absorb list-visibility lag right after upload ──
1579+
const fileListByNamePoll = await pollUntil(
1580+
() =>
1581+
runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
1582+
"knowledge",
1583+
"file",
1584+
"list",
1585+
"--category-id",
1586+
categoryId,
1587+
"--name",
1588+
fileStem,
1589+
"--workspace-id",
1590+
workspaceId,
1591+
"--quiet",
1592+
]),
1593+
(run) => run.exitCode === 0 && run.stdout.trim().split("\n").includes(fileId),
1594+
{ timeoutMs: 60_000, intervalMs: 5_000 },
1595+
);
1596+
expect(
1597+
fileListByNamePoll.satisfied,
1598+
`file list --name 未包含 ${fileId} (attempts=${fileListByNamePoll.attempts})`,
1599+
).toBe(true);
15591600

15601601
// ── P2: file list --file-id ──
15611602
const fileListByFileIdRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
@@ -1638,7 +1679,9 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
16381679
]);
16391680
expect(listByParentRun.exitCode, listByParentRun.stderr).toBe(0);
16401681

1641-
// ── P3: category list --collection-id (no-error verification) ──
1682+
// ── P3: category list --collection-id — an unknown collection id is rejected
1683+
// by the server (Invalid connectorId, verified live) and the error passes
1684+
// through verbatim with a non-zero exit ──
16421685
const listByCollectionRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
16431686
"knowledge",
16441687
"category",
@@ -1649,7 +1692,8 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
16491692
workspaceId,
16501693
"--quiet",
16511694
]);
1652-
expect(listByCollectionRun.exitCode, listByCollectionRun.stderr).toBe(0);
1695+
expect(listByCollectionRun.exitCode, "不存在的 collection id 应被服务端拒绝").not.toBe(0);
1696+
expect(listByCollectionRun.stderr).toMatch(/Invalid connectorId|InvalidParameter/i);
16531697

16541698
// ── create kb for chunk tests ──
16551699
const kbCreateRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
@@ -1744,6 +1788,45 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
17441788
expect(node.metadata?.doc_id).toContain(fileId);
17451789
}
17461790

1791+
// ── empty-result contract: a page far past the end returns no chunks —
1792+
// text mode prints the friendly empty line and exits 0 (empty ≠ error) ──
1793+
const chunkListEmptyPageRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
1794+
"knowledge",
1795+
"chunk",
1796+
"list",
1797+
"--index-id",
1798+
indexId,
1799+
"--page-number",
1800+
"99",
1801+
"--workspace-id",
1802+
workspaceId,
1803+
]);
1804+
expect(chunkListEmptyPageRun.exitCode, chunkListEmptyPageRun.stderr).toBe(0);
1805+
expect(chunkListEmptyPageRun.stdout).toMatch(/No chunks found\./);
1806+
1807+
// ── contract: updating a nonexistent chunk id — the server currently
1808+
// reports success (silent upsert-like semantics, verified live). The CLI
1809+
// passes the server result through without an existence pre-check; this
1810+
// assertion pins the known server behavior so a future server-side change
1811+
// (rejecting unknown ids) surfaces here immediately ──
1812+
const chunkUpdateMissingRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
1813+
"knowledge",
1814+
"chunk",
1815+
"update",
1816+
"--index-id",
1817+
indexId,
1818+
"--chunk-id",
1819+
"chunk_nonexistent_e2e_0000",
1820+
"--doc-id",
1821+
fileId,
1822+
"--content",
1823+
"content for a chunk id that should not exist",
1824+
"--workspace-id",
1825+
workspaceId,
1826+
]);
1827+
expect(chunkUpdateMissingRun.exitCode, chunkUpdateMissingRun.stderr).toBe(0);
1828+
expect(chunkUpdateMissingRun.stdout).toMatch(/updated: chunk_nonexistent_e2e_0000/);
1829+
17471830
// ── P4: chunk update --content-file ──
17481831
if (addedChunkId) {
17491832
const updateContentFile = join(fixtureDir, `chunk-update-${Date.now()}.txt`);

0 commit comments

Comments
 (0)