From ea8aff4096b32748bfff86b00363a4b1524dc269 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Thu, 5 Nov 2026 19:52:55 +0800 Subject: [PATCH 1/3] refactor(tests): improve quota and knowledge command tests by removing redundant flags and enhancing assertions --- .../journeys/j1-cold-start.e2e.test.ts | 5 ++++- .../e2e/knowledge/journeys/journey-helpers.ts | 1 - .../knowledge-chunk-category-file.e2e.test.ts | 14 ++++--------- .../knowledge-doc-status.e2e.test.ts | 1 - .../knowledge/knowledge-kb-delete.e2e.test.ts | 1 - packages/commands/tests/e2e/quota.e2e.test.ts | 20 +++++++++++++++++-- .../tests/output/local-command-output.test.ts | 1 + 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/commands/tests/e2e/knowledge/journeys/j1-cold-start.e2e.test.ts b/packages/commands/tests/e2e/knowledge/journeys/j1-cold-start.e2e.test.ts index 0079e1dd..77363d7b 100644 --- a/packages/commands/tests/e2e/knowledge/journeys/j1-cold-start.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/journeys/j1-cold-start.e2e.test.ts @@ -159,7 +159,10 @@ describe.skipIf(!isKbAdminE2EReady())("journey J1: 冷启动首答 (live, 自清 ], ); expect(chatBadVersionRun.exitCode, "未发布版本号应被服务端拒绝").not.toBe(0); - expect(chatBadVersionRun.stderr).toMatch(/HTTP 400/); + // JSON 错误通道写 HTTP_STATUS/400 或 http_status:400,不一定有 "HTTP 400" 字面量 + expect(chatBadVersionRun.stderr).toMatch( + /HTTP_STATUS\/400|"http_status"\s*:\s*400|ParamField\.Error/i, + ); } finally { for (const agentId of serviceAgentIds) { const deleteRun = await reporter.runStep( diff --git a/packages/commands/tests/e2e/knowledge/journeys/journey-helpers.ts b/packages/commands/tests/e2e/knowledge/journeys/journey-helpers.ts index 224cd863..42f1ed51 100644 --- a/packages/commands/tests/e2e/knowledge/journeys/journey-helpers.ts +++ b/packages/commands/tests/e2e/knowledge/journeys/journey-helpers.ts @@ -325,7 +325,6 @@ export async function createKbWithDocs( const createRun = await reporter.runStep("kb create --wait", routes, [ "knowledge", "create", - "--yes", "--name", kbName, "--description", diff --git a/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts index 0d12c47f..9693bd73 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts @@ -1126,7 +1126,6 @@ describe.skipIf(!isKbAdminE2EReady())( const kbCreateRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [ "knowledge", "create", - "--yes", "--name", `e2e-cate-kb-${Date.now() % 100000000}`.slice(0, 20), "--description", @@ -1247,7 +1246,6 @@ describe.skipIf(!isKbAdminE2EReady())( const createRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [ "knowledge", "create", - "--yes", "--name", `e2e-ck-${Date.now() % 100000000}`, "--description", @@ -1803,7 +1801,6 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li const kbCreateRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [ "knowledge", "create", - "--yes", "--name", `e2e-pk-${Date.now() % 100000000}`.slice(0, 20), "--description", @@ -1914,11 +1911,8 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li expect(chunkListEmptyPageRun.exitCode, chunkListEmptyPageRun.stderr).toBe(0); expect(chunkListEmptyPageRun.stdout).toMatch(/No chunks found\./); - // ── contract: updating a nonexistent chunk id — the server currently - // reports success (silent upsert-like semantics, verified live). The CLI - // passes the server result through without an existence pre-check; this - // assertion pins the known server behavior so a future server-side change - // (rejecting unknown ids) surfaces here immediately ── + // ── contract: updating a nonexistent chunk id — server rejects invalid + // chunkId (Index.InvalidParameter). CLI 不做存在性预检,原样透传服务端错误 ── const chunkUpdateMissingRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [ "knowledge", "chunk", @@ -1934,8 +1928,8 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li "--workspace-id", workspaceId, ]); - expect(chunkUpdateMissingRun.exitCode, chunkUpdateMissingRun.stderr).toBe(0); - expect(chunkUpdateMissingRun.stdout).toMatch(/updated: chunk_nonexistent_e2e_0000/); + expect(chunkUpdateMissingRun.exitCode).not.toBe(0); + expect(chunkUpdateMissingRun.stderr).toMatch(/chunkId|InvalidParameter/i); // ── P4: chunk update --content-file ── if (addedChunkId) { diff --git a/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts index dc03e0f2..812583db 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts @@ -150,7 +150,6 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge doc status (live, 自清 const createRun = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [ "knowledge", "create", - "--yes", "--name", `e2e-st-${Date.now() % 100000000}`, "--description", diff --git a/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts index 1ae18986..5793f1ae 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts @@ -111,7 +111,6 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清 const createRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [ "knowledge", "create", - "--yes", "--name", kbName, "--description", diff --git a/packages/commands/tests/e2e/quota.e2e.test.ts b/packages/commands/tests/e2e/quota.e2e.test.ts index 0f4dce5a..8f000bb7 100644 --- a/packages/commands/tests/e2e/quota.e2e.test.ts +++ b/packages/commands/tests/e2e/quota.e2e.test.ts @@ -311,11 +311,27 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: quota(DashScope)", () => { }); test("quota list --model 精确查询返回模型限流", async () => { + // 账号限流目录不一定含固定模型名(如 qwen3-max);先模糊取一条再测精确查询。 + const listRun = await runCommandE2e(QUOTA_ROUTES, [ + "quota", + "list", + "--name", + "qwen", + "--page-size", + "1", + "--output", + "json", + ]); + expect(listRun.exitCode, listRun.stderr).toBe(0); + const listed = parseStdoutJson<{ items?: { model?: string }[] }>(listRun.stdout); + const model = listed.items?.[0]?.model; + expect(model, "quota list --name qwen 应至少返回一条模型限流").toBeTruthy(); + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "list", "--model", - "qwen3-max", + model!, "--output", "json", ]); @@ -323,7 +339,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: quota(DashScope)", () => { const data = parseStdoutJson<{ items?: { model?: string; model_limit?: { request_limit?: number | null } | null }[]; }>(stdout); - expect(data.items?.[0]?.model).toBe("qwen3-max"); + expect(data.items?.[0]?.model).toBe(model); expect(data.items?.[0]).toHaveProperty("model_limit"); }); diff --git a/packages/commands/tests/output/local-command-output.test.ts b/packages/commands/tests/output/local-command-output.test.ts index ceb77369..8296be4e 100644 --- a/packages/commands/tests/output/local-command-output.test.ts +++ b/packages/commands/tests/output/local-command-output.test.ts @@ -43,6 +43,7 @@ const retained = { future: { values: [false, 0, null, []] } }; beforeEach(() => { vi.stubEnv("NO_COLOR", "1"); vi.stubEnv("BAILIAN_INSTALL_METHOD", "brew"); + vi.stubEnv("DASHSCOPE_API_KEY", undefined as unknown as string); vi.spyOn(httpTransport, "fetch").mockImplementation(async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; if (url.endsWith("/index.json")) return Response.json({ version: 1, skills: registrySkills }); From 25c566f411e34f291e4e63f99e143e1d3b9c5600 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 6 Nov 2026 16:12:43 +0800 Subject: [PATCH 2/3] feat(memory): enhance add method to support extraction retries and update tests accordingly --- .../tests/e2e/memory/journeys/helpers.ts | 68 ++++++++++++------- .../e2e/memory/journeys/main.live.test.ts | 3 +- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/packages/commands/tests/e2e/memory/journeys/helpers.ts b/packages/commands/tests/e2e/memory/journeys/helpers.ts index 5f9dda92..12a7bef2 100644 --- a/packages/commands/tests/e2e/memory/journeys/helpers.ts +++ b/packages/commands/tests/e2e/memory/journeys/helpers.ts @@ -199,35 +199,53 @@ export class MemoryJourney { return result.events!; } - async add(text: string, options: { direct?: boolean; schemaId?: string } = {}): Promise { + async add( + text: string, + options: { direct?: boolean; schemaId?: string; requireExtraction?: boolean } = {}, + ): Promise { const payload = options.direct ? ["--content", text] : ["--messages", JSON.stringify([{ role: "user", content: text }])]; - this.uncertainSubmission = true; - this.flush(); - const receipt = await this.command("add", [ - "add", - "--user-id", - this.userId, - ...payload, - ...(options.schemaId ? ["--profile-schema", options.schemaId] : []), - "--wait", - "0", - ]); - expect(receipt.event_id).toBeTruthy(); - this.events.add(receipt.event_id!); - this.pending.add(receipt.event_id!); - this.uncertainSubmission = false; - this.flush(); - const events = await this.settle(receipt.event_id!); - expect( - events.some((event) => event.status === "FAILED"), - JSON.stringify(events), - ).toBe(false); - expect(events.some((event) => ["SUCCEEDED", "SUCCESS"].includes(event.status ?? ""))).toBe( - true, + // Server-side observation extraction is nondeterministic: a terminal + // SUCCEEDED event may still carry an empty result. An empty terminal + // result never materializes later, so retry instead of waiting it out. + const maxAttempts = options.requireExtraction ? 3 : 1; + let eventId = ""; + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + this.uncertainSubmission = true; + this.flush(); + const receipt = await this.command( + attempt === 1 ? "add" : `add retry ${attempt - 1}`, + [ + "add", + "--user-id", + this.userId, + ...payload, + ...(options.schemaId ? ["--profile-schema", options.schemaId] : []), + "--wait", + "0", + ], + ); + expect(receipt.event_id).toBeTruthy(); + this.events.add(receipt.event_id!); + this.pending.add(receipt.event_id!); + this.uncertainSubmission = false; + this.flush(); + eventId = receipt.event_id!; + const events = await this.settle(eventId); + expect( + events.some((event) => event.status === "FAILED"), + JSON.stringify(events), + ).toBe(false); + expect(events.some((event) => ["SUCCEEDED", "SUCCESS"].includes(event.status ?? ""))).toBe( + true, + ); + if (!options.requireExtraction) return eventId; + if (events.some((event) => (event.result ?? []).length > 0)) return eventId; + } + throw new Error( + `add extracted nothing after ${maxAttempts} attempts; evidence ${this.outputDir}`, ); - return receipt.event_id!; } async remove(nodeId: string, allowMissing = false) { diff --git a/packages/commands/tests/e2e/memory/journeys/main.live.test.ts b/packages/commands/tests/e2e/memory/journeys/main.live.test.ts index 565e4c6c..2d43a0de 100644 --- a/packages/commands/tests/e2e/memory/journeys/main.live.test.ts +++ b/packages/commands/tests/e2e/memory/journeys/main.live.test.ts @@ -47,7 +47,7 @@ describe.skipIf(!liveEnabled())("Memory J1–J5 (real cloud, isolated CLI subpro "J3 a later correction becomes a searchable current preference", async () => journey("J3", async (fixture) => { - await fixture.add("请记住,我当前最喜欢的运动是徒步。"); + await fixture.add("请记住,我当前最喜欢的运动是徒步。", { requireExtraction: true }); await eventually( () => fixture.search("当前最喜欢的运动是什么?"), (nodes) => nodes.some((node) => /徒步/.test(node.content)), @@ -55,6 +55,7 @@ describe.skipIf(!liveEnabled())("Memory J1–J5 (real cloud, isolated CLI subpro ); await fixture.add( "更新我的长期偏好:我以前最喜欢徒步,但现在已经改成游泳。游泳是我当前最喜欢的运动,徒步只是过去的偏好。", + { requireExtraction: true }, ); await eventually( () => fixture.list(), From c8699f1450c54304d51dac13ef2612526b983979 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 6 Nov 2026 17:17:59 +0800 Subject: [PATCH 3/3] feat(memory): implement transient project-existence rejection handling in add method --- .../commands/tests/e2e/memory/live-helpers.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/commands/tests/e2e/memory/live-helpers.ts b/packages/commands/tests/e2e/memory/live-helpers.ts index fd148543..d24e8833 100644 --- a/packages/commands/tests/e2e/memory/live-helpers.ts +++ b/packages/commands/tests/e2e/memory/live-helpers.ts @@ -24,6 +24,11 @@ export function assertMemoryServiceRejection( ); } +/** True only for the flaky server-side project-existence rejection (see add). */ +function isTransientProjectRejection(error: unknown): boolean { + return error instanceof Error && /project_ids got a project does not exist/i.test(error.message); +} + /** Each journey owns only a fresh user; cleanup never touches existing user data. */ export function memoryJourney( libraryId: string | undefined = process.env.BAILIAN_E2E_MEMORY_LIBRARY_ID, @@ -71,7 +76,17 @@ export function memoryJourney( for (const [index, argument] of args.entries()) { if (argument === "--project-id") projects.add(args[index + 1]); } - return invoke(["add", "--user-id", userId, ...args]); + // Project-existence validation is transiently inconsistent: a valid + // project can be reported as missing, then accepted minutes later. + // Only that exact rejection is retryable; anything else fails at once. + for (let attempt = 1; ; attempt += 1) { + try { + return await invoke(["add", "--user-id", userId, ...args]); + } catch (error) { + if (attempt >= 3 || !isTransientProjectRejection(error)) throw error; + await delay(attempt * 5_000); + } + } } async function search(args: string[]): Promise { const result = await invoke(["search", "--user-id", userId, ...args]);