Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ export async function createKbWithDocs(
const createRun = await reporter.runStep("kb create --wait", routes, [
"knowledge",
"create",
"--yes",
"--name",
kbName,
"--description",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
68 changes: 43 additions & 25 deletions packages/commands/tests/e2e/memory/journeys/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,35 +199,53 @@ export class MemoryJourney {
return result.events!;
}

async add(text: string, options: { direct?: boolean; schemaId?: string } = {}): Promise<string> {
async add(
text: string,
options: { direct?: boolean; schemaId?: string; requireExtraction?: boolean } = {},
): Promise<string> {
const payload = options.direct
? ["--content", text]
: ["--messages", JSON.stringify([{ role: "user", content: text }])];
this.uncertainSubmission = true;
this.flush();
const receipt = await this.command<MemoryAddBody>("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<MemoryAddBody>(
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ 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)),
"initial preference",
);
await fixture.add(
"更新我的长期偏好:我以前最喜欢徒步,但现在已经改成游泳。游泳是我当前最喜欢的运动,徒步只是过去的偏好。",
{ requireExtraction: true },
);
await eventually(
() => fixture.list(),
Expand Down
17 changes: 16 additions & 1 deletion packages/commands/tests/e2e/memory/live-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Node[]> {
const result = await invoke<MemoryNodeListBody>(["search", "--user-id", userId, ...args]);
Expand Down
20 changes: 18 additions & 2 deletions packages/commands/tests/e2e/quota.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,35 @@ 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",
]);
expect(exitCode, stderr).toBe(0);
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");
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Loading