feat(memory): 完善记忆及画像相关命令支持并统一参数

- 在 CLI 文档中新增记忆 live 场景说明与变量需求
- 扩展 CLI 命令集,添加记忆画像 Profile 的增删改查功能
- 统一所有记忆相关命令加入 workspace 和 library 参数支持
- 记忆添加命令支持自定义 meta_data,完善输入校验和互斥说明
- 记忆删除、列表、获取用户画像等命令增加工作空间参数支持
- 画像模板相关命令新增分页参数及属性操作接口
- 重构知识命令共享模块,移除重复的 workspace 解析实现,统一导出
- 优化命令输出格式,文本模式下显示更多详细信息和异常提示
- 提供完整的示例参数,增强命令使用说明和提示文档
This commit is contained in:
zeyu.fz
2026-09-10 14:40:39 +08:00
parent 2090293f85
commit 75a7100702
39 changed files with 3797 additions and 588 deletions
+1
View File
@@ -34,6 +34,7 @@ export {
isDashScopeE2EReady,
isImageKbE2EReady,
isKbAdminE2EReady,
isMemoryE2EReady,
isMultimodalChatE2EReady,
isMultimodalSearchE2EReady,
isOpenApiE2EReady,
@@ -1,215 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import {
isBailianE2EEnabled,
isDashScopeE2EReady,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { MEMORY_ROUTES } from "./topic-routes.ts";
interface MemoryAddBody {
memory_ids?: string[];
}
interface MemoryListBody {
memory_nodes?: Array<{ memory_node_id: string; content: string }>;
}
interface MemorySearchBody {
memory_nodes?: Array<{ memory_node_id: string; content: string }>;
}
const DEFAULT_E2E_MEMORY_USER_ID = "e2e-vp-test";
// bailian-cli-test
const DEFAULT_E2E_MEMORY_LIBRARY_ID = "92e8626561c4472e8805d2328030d642";
/**
* 优先使用环境变量,若未设置则使用默认值。
*/
function memoryLibraryCliArgs(): string[] {
const id = process.env.BAILIAN_E2E_MEMORY_LIBRARY_ID?.trim() || DEFAULT_E2E_MEMORY_LIBRARY_ID;
return id ? ["--memory-library-id", id] : [];
}
/**
* Memory:先做 help / 分组等常规检测(不依赖密钥、不调记忆 API)。
* 需 E2E + DashScope 的缺参、dry-run 与 CRUD 放在 skip 块内。
*/
describe("e2e: memory", () => {
test("memory add --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/add|--user-id|--content|messages/i);
});
test("memory list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "list", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|--user-id|memory-library/i);
});
test("memory search --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
"memory",
"search",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/search|--query|user-id/i);
});
test("memory profile create --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
"memory",
"profile",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/profile|create|user-id/i);
});
});
/**
* 记忆库增 → 列 → 搜 → 改 → 删
*/
describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())(
"e2e: memory CRUD + search",
() => {
test("memory add 缺少 --user-id 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
...memoryLibraryCliArgs(),
"--content",
"仅内容无用户",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--user-id|Usage:/i);
});
test("memory add 缺少 --messages 与 --content 时报错正常退出", async () => {
const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
...memoryLibraryCliArgs(),
"--user-id",
userId,
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/messages|content|required/i);
});
test("memory add --dry-run 仅输出计划且不入网", async () => {
const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
"--dry-run",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--content",
"dry-run 不入网",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ request?: { user_id?: string; custom_content?: string } }>(
stdout,
);
expect(data.request?.user_id).toBe(userId);
expect(data.request?.custom_content).toContain("dry-run");
});
test("记忆库增删改查", async () => {
const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
const contentA = "CLI vp test:记忆写入(可删)";
const contentB = "CLI vp test:记忆已更新";
const addRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"add",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--content",
contentA,
"--output",
"json",
]);
expect(addRes.exitCode, addRes.stderr).toBe(0);
const added = parseStdoutJson<MemoryAddBody & { request_id?: string }>(addRes.stdout);
expect(added.request_id?.length ?? 0, addRes.stdout + addRes.stderr).toBeGreaterThan(0);
const listRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"list",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--output",
"json",
]);
expect(listRes.exitCode, listRes.stderr).toBe(0);
const listed = parseStdoutJson<MemoryListBody>(listRes.stdout);
if ((listed.memory_nodes?.length ?? 0) === 0) {
throw new Error(
"memory list 为空:add 已成功但列表无节点。请设置 BAILIAN_E2E_MEMORY_LIBRARY_ID 与阿里云百炼控制台当前「记忆库」ID 一致;" +
"并设置 BAILIAN_E2E_MEMORY_USER_ID 与控制台筛选的「用户 ID」一致。stdout=" +
listRes.stdout,
);
}
const nodeId = listed.memory_nodes![0]!.memory_node_id.trim();
expect(nodeId.length).toBeGreaterThan(0);
const searchRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"search",
...memoryLibraryCliArgs(),
"--user-id",
userId,
"--query",
"vp test",
"--top-k",
"5",
"--output",
"json",
]);
expect(searchRes.exitCode, searchRes.stderr).toBe(0);
const searched = parseStdoutJson<MemorySearchBody>(searchRes.stdout);
expect(searched.memory_nodes?.length ?? 0).toBeGreaterThan(0);
const updRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"update",
...memoryLibraryCliArgs(),
"--node-id",
nodeId!,
"--user-id",
userId,
"--content",
contentB,
"--output",
"json",
]);
expect(updRes.exitCode, updRes.stderr).toBe(0);
const delRes = await runCommandE2e(MEMORY_ROUTES, [
"memory",
"delete",
...memoryLibraryCliArgs(),
"--node-id",
nodeId!,
"--user-id",
userId,
"--output",
"json",
]);
expect(delRes.exitCode, delRes.stderr).toBe(0);
}, 180_000);
},
);
@@ -0,0 +1,246 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_ADD_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, memoryUserId, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory add", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--messages/i);
expect(stderr).toMatch(/--content/i);
expect(stderr).toMatch(/--profile-schema/i);
expect(stderr).toMatch(/--meta-data/i);
expect(stderr).toMatch(/--project-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--content",
"仅内容无用户",
]);
expect(exitCode).toBe(2);
});
test("缺 --messages 与 --content 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/messages|content/i);
});
test("--user-id 65 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"u".repeat(65),
"--content",
"over-long user id",
]);
expect(exitCode).toBe(2);
});
test("--content 513 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"x".repeat(513),
]);
expect(exitCode).toBe(2);
});
test("--messages 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--messages",
"not-json",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--messages 传对象而非数组报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--messages",
'{"role":"user","content":"hi"}',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--messages 超过 50 条报 USAGE (2)", async () => {
const messages = Array.from({ length: 51 }, (_unused, index) => ({
role: "user",
content: `turn ${index}`,
}));
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--messages",
JSON.stringify(messages),
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--meta-data 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"meta 格式错误",
"--meta-data",
"{oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--meta-data 传数组而非对象报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
memoryUserId(),
"--content",
"meta 类型错误",
"--meta-data",
'["a","b"]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run + --content 断言 endpoint / POST / custom_content", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--content",
"dry-run 不入网",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/^https:\/\/ws_test\.cn-beijing\.maas\.aliyuncs\.com\//);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/add$/);
expect(data.method).toBe("POST");
expect(data.request?.user_id).toBe("user1");
expect(data.request?.custom_content).toBe("dry-run 不入网");
// Not provided → must stay out of the body entirely
expect(data.request?.messages).toBeUndefined();
expect(data.request?.meta_data).toBeUndefined();
});
test("--dry-run + --messages 断言 messages 原样进 body", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--messages",
'[{"role":"user","content":"我喜欢旅行"},{"role":"assistant","content":"记住了"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.messages).toEqual([
{ role: "user", content: "我喜欢旅行" },
{ role: "assistant", content: "记住了" },
]);
expect(data.request?.custom_content).toBeUndefined();
});
test("--dry-run 断言 --meta-data/--project-id/--profile-schema/--memory-library-id 映射", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--content",
"参加了 WAIC",
"--meta-data",
'{"location":"上海","year":2026}',
"--project-id",
"proj_test",
"--profile-schema",
"schema_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.meta_data).toEqual({ location: "上海", year: 2026 });
expect(data.request?.project_id).toBe("proj_test");
expect(data.request?.profile_schema).toBe("schema_test");
expect(data.request?.memory_library_id).toBe("lib_test");
});
test("--dry-run 同传 --content 与 --messages 时两者都进 body(服务端择一)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ADD_ROUTES, [
"memory",
"add",
"--user-id",
"user1",
"--content",
"content wins",
"--messages",
'[{"role":"user","content":"ignored"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.custom_content).toBe("content wins");
expect(data.request?.messages).toHaveLength(1);
});
});
// The live self-cleaning chain (add → list → search → update → delete) lives in
// memory-delete.e2e.test.ts.
@@ -0,0 +1,243 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_DELETE_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryAddBody,
type MemoryDryRunBody,
type MemoryNodeListBody,
} from "./shared.ts";
describe("e2e: memory delete", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--node-id/i);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --node-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / DELETE / query 参数", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
"--user-id",
"user1",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
const endpoint = data.endpoint ?? "";
expect(endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\/node_test\?/);
expect(endpoint).toMatch(/user_id=user1/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
expect(data.method).toBe("DELETE");
});
test("--dry-run 不传 --memory-library-id 时 query 里不出现该键", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
"--node-id",
"node_test",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).not.toMatch(/memory_library_id/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory delete (live)", () => {
test("删除不存在的节点时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
...memoryScopeCliArgs(),
"--node-id",
`no-such-node-${Date.now()}`,
"--user-id",
memoryUserId(),
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
test("记忆节点自清理闭环:add → list → search → update → delete → list", async () => {
const userId = memoryUserId();
const marker = `vp-${Date.now()}`;
const contentBefore = `CLI vp test ${marker}:记忆写入(可删)`;
const contentAfter = `CLI vp test ${marker}:记忆已更新`;
const addRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"add",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--content",
contentBefore,
"--meta-data",
`{"marker":"${marker}"}`,
"--output",
"json",
]);
expect(addRes.exitCode, addRes.stderr).toBe(0);
const added = parseStdoutJson<MemoryAddBody>(addRes.stdout);
expect(added.request_id?.length ?? 0, addRes.stdout).toBeGreaterThan(0);
// The contract says AddMemory reports the changed nodes; a bare request_id
// with no node means the write silently did nothing.
expect(added.memory_nodes?.length ?? 0, addRes.stdout).toBeGreaterThan(0);
const addedNodeId = added.memory_nodes![0]!.memory_node_id;
expect(addedNodeId.length).toBeGreaterThan(0);
const listRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--page-size",
"50",
"--output",
"json",
]);
expect(listRes.exitCode, listRes.stderr).toBe(0);
const listed = parseStdoutJson<MemoryNodeListBody>(listRes.stdout);
const listedNode = listed.memory_nodes?.find((node) => node.memory_node_id === addedNodeId);
if (!listedNode) {
throw new Error(
`memory list 未包含刚写入的节点 ${addedNodeId}。请确认 BAILIAN_E2E_MEMORY_LIBRARY_ID 与控制台「记忆库」ID 一致。stdout=${listRes.stdout}`,
);
}
expect(listedNode.meta_data?.marker).toBe(marker);
const searchRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"search",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--query",
marker,
"--top-k",
"5",
"--min-score",
"0",
"--plan-version",
"lite",
"--output",
"json",
]);
expect(searchRes.exitCode, searchRes.stderr).toBe(0);
const searched = parseStdoutJson<MemoryNodeListBody>(searchRes.stdout);
expect(searched.memory_nodes?.length ?? 0, searchRes.stdout).toBeGreaterThan(0);
const updateRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"update",
...memoryScopeCliArgs(),
"--node-id",
addedNodeId,
"--user-id",
userId,
"--content",
contentAfter,
"--meta-data",
'{"updated":"true"}',
"--output",
"json",
]);
expect(updateRes.exitCode, updateRes.stderr).toBe(0);
// Verify the update landed on the server rather than trusting the 200
const afterUpdateRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--page-size",
"50",
"--output",
"json",
]);
expect(afterUpdateRes.exitCode, afterUpdateRes.stderr).toBe(0);
const afterUpdate = parseStdoutJson<MemoryNodeListBody>(afterUpdateRes.stdout);
const updatedNode = afterUpdate.memory_nodes?.find(
(node) => node.memory_node_id === addedNodeId,
);
expect(updatedNode?.content, afterUpdateRes.stdout).toBe(contentAfter);
const deleteRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"delete",
...memoryScopeCliArgs(),
"--node-id",
addedNodeId,
"--user-id",
userId,
"--output",
"json",
]);
expect(deleteRes.exitCode, deleteRes.stderr).toBe(0);
const afterDeleteRes = await runCommandE2e(MEMORY_DELETE_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
userId,
"--page-size",
"50",
"--output",
"json",
]);
expect(afterDeleteRes.exitCode, afterDeleteRes.stderr).toBe(0);
const afterDelete = parseStdoutJson<MemoryNodeListBody>(afterDeleteRes.stdout);
expect(
afterDelete.memory_nodes?.some((node) => node.memory_node_id === addedNodeId) ?? false,
afterDeleteRes.stdout,
).toBe(false);
}, 240_000);
});
@@ -0,0 +1,167 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_LIST_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
type MemoryNodeListBody,
} from "./shared.ts";
describe("e2e: memory list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--page-size/i);
expect(stderr).toMatch(/--page/i);
expect(stderr).toMatch(/--project-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 workspace 时报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(
MEMORY_LIST_ROUTES,
["memory", "list", "--user-id", "user1", "--api-key", "sk-fake", "--output", "json"],
{ BAILIAN_WORKSPACE_ID: "", BAILIAN_CONFIG_DIR: "/tmp" },
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/workspace.*required/i);
});
test("裸调用(无任何 flag)打 help 并 exit 0", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, ["memory", "list"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--page-size",
"10",
]);
expect(exitCode).toBe(2);
});
test("--page 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--page",
"0",
]);
expect(exitCode).toBe(2);
});
test("--page-size 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--page-size",
"0",
]);
expect(exitCode).toBe(2);
});
test("--page 非数字报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
memoryUserId(),
"--page",
"abc",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / GET / user_id query", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
// The memory API is served on the workspace-specific host, not the model domain
expect(data.endpoint).toMatch(/^https:\/\/ws_test\.cn-beijing\.maas\.aliyuncs\.com\//);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\?user_id=user1$/);
expect(data.method).toBe("GET");
});
test("--dry-run 断言分页 / project-id / memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
"--user-id",
"user1",
"--page-size",
"20",
"--page",
"2",
"--project-id",
"proj_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
const endpoint = data.endpoint ?? "";
expect(endpoint).toMatch(/page_size=20/);
// The API paginates with page_num, not page
expect(endpoint).toMatch(/page_num=2/);
expect(endpoint).toMatch(/project_id=proj_test/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory list (live)", () => {
test("列出不存在的记忆实体返回空列表而非报错", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
`no-such-entity-${Date.now()}`,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryNodeListBody>(stdout);
expect(data.request_id?.length ?? 0).toBeGreaterThan(0);
expect(data.memory_nodes ?? []).toHaveLength(0);
});
test("text 输出对空列表给出明确提示", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_LIST_ROUTES, [
"memory",
"list",
...memoryScopeCliArgs(),
"--user-id",
`no-such-entity-${Date.now()}`,
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(/No memory nodes found/i);
});
});
@@ -0,0 +1,148 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_CREATE_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory profile create", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--name/i);
expect(stderr).toMatch(/--description/i);
expect(stderr).toMatch(/--attributes/i);
expect(stderr).toMatch(/--plan-version/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --name 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--attributes",
'[{"name":"age"}]',
]);
expect(exitCode).toBe(2);
});
test("缺 --attributes 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
]);
expect(exitCode).toBe(2);
});
test("--attributes 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
"[oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--attributes 传对象而非数组报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
'{"name":"age"}',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--plan-version 非法取值报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
'[{"name":"age"}]',
"--plan-version",
"ultra",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/pro|lite/i);
});
test("--dry-run 断言 endpoint / POST / name / attributes", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--attributes",
'[{"name":"age","description":"年龄"},{"name":"hobby"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas$/);
expect(data.method).toBe("POST");
expect(data.request?.name).toBe("user_basic");
expect(data.request?.attributes).toEqual([
{ name: "age", description: "年龄" },
{ name: "hobby" },
]);
expect(data.request?.description).toBeUndefined();
expect(data.request?.plan_version).toBeUndefined();
});
test("--dry-run 断言 --description/--plan-version/--memory-library-id 映射", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_CREATE_ROUTES, [
"memory",
"profile",
"create",
"--name",
"user_basic",
"--description",
"基础画像",
"--attributes",
'[{"name":"age","default_value":"18"}]',
"--plan-version",
"lite",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.description).toBe("基础画像");
expect(data.request?.plan_version).toBe("lite");
expect(data.request?.memory_library_id).toBe("lib_test");
expect(data.request?.attributes).toEqual([{ name: "age", default_value: "18" }]);
});
});
// The live self-cleaning chain (create → list → show → update → get → delete)
// lives in memory-profile-delete.e2e.test.ts.
@@ -0,0 +1,278 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_DELETE_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
type ProfileSchemaCreateBody,
type ProfileSchemaDetailBody,
type ProfileSchemaListBody,
type UserProfileBody,
} from "./shared.ts";
describe("e2e: memory profile delete", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--yes/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--yes",
]);
expect(exitCode).toBe(2);
});
test("缺 --yes 报 CONFIRMATION_REQUIRED (7)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
"--api-key",
"sk-fake",
...TEST_WORKSPACE_ARGS,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(7);
expect(stderr).toMatch(/--yes/i);
});
test("--dry-run 断言 endpoint / DELETE,且不触发确认", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
// dry-run must short-circuit before the runtime confirmation gate, otherwise
// this non-TTY run would fail asking for --yes
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas\/schema_test$/);
expect(data.method).toBe("DELETE");
});
test("--dry-run 断言 --memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
"--schema-id",
"schema_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/profile_schemas\/schema_test\?memory_library_id=lib_test$/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile delete (live)", () => {
test("删除不存在的模板时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
...memoryScopeCliArgs(),
"--schema-id",
`no-such-schema-${Date.now()}`,
"--yes",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
test("画像模板自清理闭环:create → list → show → update → show → get → delete → list", async () => {
const marker = `vp-${Date.now()}`;
const schemaName = `vp_profile_${marker}`;
// plan_version lite keeps the per-call price at the cheaper tier
const createRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"create",
...memoryScopeCliArgs(),
"--name",
schemaName,
"--description",
`CLI vp test ${marker}(可删)`,
"--attributes",
'[{"name":"plan","description":"套餐"},{"name":"tier","description":"等级"}]',
"--plan-version",
"lite",
"--output",
"json",
]);
expect(createRes.exitCode, createRes.stderr).toBe(0);
const created = parseStdoutJson<ProfileSchemaCreateBody>(createRes.stdout);
const schemaId = created.profile_schema_id ?? "";
expect(schemaId.length, createRes.stdout).toBeGreaterThan(0);
try {
const listRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"list",
...memoryScopeCliArgs(),
"--page-size",
"100",
"--output",
"json",
]);
expect(listRes.exitCode, listRes.stderr).toBe(0);
const listed = parseStdoutJson<ProfileSchemaListBody>(listRes.stdout);
const listedSchema = listed.profile_schemas?.find(
(schema) => schema.profile_schema_id === schemaId,
);
if (!listedSchema) {
throw new Error(
`memory profile list 未包含刚创建的模板 ${schemaId}。请确认 BAILIAN_E2E_MEMORY_LIBRARY_ID 与控制台「记忆库」ID 一致。stdout=${listRes.stdout}`,
);
}
expect(listedSchema.name).toBe(schemaName);
const showRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"show",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--output",
"json",
]);
expect(showRes.exitCode, showRes.stderr).toBe(0);
const detail = parseStdoutJson<ProfileSchemaDetailBody>(showRes.stdout);
expect(detail.name).toBe(schemaName);
const planAttribute = detail.attributes?.find((attribute) => attribute.name === "plan");
const tierAttribute = detail.attributes?.find((attribute) => attribute.name === "tier");
// attribute_id is the handle profile update needs; without it the
// update / delete operations below cannot be expressed at all
expect(planAttribute?.attribute_id, showRes.stdout).toBeTruthy();
expect(tierAttribute?.attribute_id, showRes.stdout).toBeTruthy();
const renamed = `${schemaName}_v2`;
const updateRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"update",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--name",
renamed,
"--attributes-operations",
JSON.stringify([
{ op: "add", name: "region", description: "地域" },
{
op: "update",
attribute_id: planAttribute!.attribute_id,
description: "套餐(已更新)",
},
{ op: "delete", attribute_id: tierAttribute!.attribute_id },
]),
"--output",
"json",
]);
expect(updateRes.exitCode, updateRes.stderr).toBe(0);
// Verify the mutation landed on the server rather than trusting the 200
const afterUpdateRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"show",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--output",
"json",
]);
expect(afterUpdateRes.exitCode, afterUpdateRes.stderr).toBe(0);
const afterUpdate = parseStdoutJson<ProfileSchemaDetailBody>(afterUpdateRes.stdout);
expect(afterUpdate.name, afterUpdateRes.stdout).toBe(renamed);
const attributeNames = (afterUpdate.attributes ?? []).map((attribute) => attribute.name);
expect(attributeNames, afterUpdateRes.stdout).toContain("region");
expect(attributeNames, afterUpdateRes.stdout).not.toContain("tier");
expect(
afterUpdate.attributes?.find((attribute) => attribute.name === "plan")?.description,
afterUpdateRes.stdout,
).toBe("套餐(已更新)");
// No memory was written against this schema, so every attribute value
// is expected to be empty — the point is that the endpoint resolves
const getRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"get",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--user-id",
memoryUserId(),
"--output",
"json",
]);
expect(getRes.exitCode, getRes.stderr).toBe(0);
const profile = parseStdoutJson<UserProfileBody>(getRes.stdout);
expect(profile.request_id?.length ?? 0, getRes.stdout).toBeGreaterThan(0);
} finally {
const deleteRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"delete",
...memoryScopeCliArgs(),
"--schema-id",
schemaId,
"--yes",
"--output",
"json",
]);
expect(deleteRes.exitCode, deleteRes.stderr).toBe(0);
}
const afterDeleteRes = await runCommandE2e(MEMORY_PROFILE_DELETE_ROUTES, [
"memory",
"profile",
"list",
...memoryScopeCliArgs(),
"--page-size",
"100",
"--output",
"json",
]);
expect(afterDeleteRes.exitCode, afterDeleteRes.stderr).toBe(0);
const afterDelete = parseStdoutJson<ProfileSchemaListBody>(afterDeleteRes.stdout);
expect(
afterDelete.profile_schemas?.some((schema) => schema.profile_schema_id === schemaId) ?? false,
afterDeleteRes.stdout,
).toBe(false);
}, 240_000);
});
@@ -0,0 +1,110 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_GET_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
} from "./shared.ts";
describe("e2e: memory profile get", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--user-id",
"user1",
]);
expect(exitCode).toBe(2);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
]);
expect(exitCode).toBe(2);
});
test("--dry-run endpoint 命中 /user_profile 子资源", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
"--user-id",
"user1",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
// Regression guard: this path used to point at /profiles, which the API rejects
expect(data.endpoint).toMatch(
/api\/v2\/apps\/memory\/profile_schemas\/schema_test\/user_profile\?user_id=user1$/,
);
expect(data.method).toBe("GET");
});
test("--dry-run 断言 --memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
"--schema-id",
"schema_test",
"--user-id",
"user1",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const endpoint = parseStdoutJson<MemoryDryRunBody>(stdout).endpoint ?? "";
expect(endpoint).toMatch(/user_id=user1/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile get (live)", () => {
test("不存在的 schema 被服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_GET_ROUTES, [
"memory",
"profile",
"get",
...memoryScopeCliArgs(),
"--schema-id",
`no-such-schema-${Date.now()}`,
"--user-id",
memoryUserId(),
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,133 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_LIST_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
type MemoryDryRunBody,
type ProfileSchemaListBody,
} from "./shared.ts";
describe("e2e: memory profile list", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--page-size/i);
expect(stderr).toMatch(/--page/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("--page 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--page",
"0",
]);
expect(exitCode).toBe(2);
});
test("--page-size 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--page-size",
"0",
]);
expect(exitCode).toBe(2);
});
test("未知 flag 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--user-id",
"user1",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 无参数时 endpoint 不带 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas$/);
expect(data.method).toBe("GET");
});
test("--dry-run 断言分页与 memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--page-size",
"20",
"--page",
"2",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
const endpoint = data.endpoint ?? "";
expect(endpoint).toMatch(/page_size=20/);
expect(endpoint).toMatch(/page_num=2/);
expect(endpoint).toMatch(/memory_library_id=lib_test/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile list (live)", () => {
test("列出画像模板返回 request_id 与 total", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
...memoryScopeCliArgs(),
"--page-size",
"5",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<ProfileSchemaListBody>(stdout);
expect(data.request_id?.length ?? 0).toBeGreaterThan(0);
for (const schema of data.profile_schemas ?? []) {
expect(schema.profile_schema_id.length).toBeGreaterThan(0);
expect(schema.name.length).toBeGreaterThan(0);
}
});
test("--memory-library-id 不存在时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_LIST_ROUTES, [
"memory",
"profile",
"list",
"--memory-library-id",
"no-such-library-000000000000000",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,100 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_SHOW_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, memoryScopeCliArgs, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory profile show", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--memory-library-id",
"lib_test",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / GET", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"schema_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas\/schema_test$/);
expect(data.method).toBe("GET");
});
test("--dry-run 断言 --memory-library-id 进 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"schema_test",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/profile_schemas\/schema_test\?memory_library_id=lib_test$/);
});
test("--dry-run schema id 做 URL 编码", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
"--schema-id",
"a/b c",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/profile_schemas\/a%2Fb%20c$/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory profile show (live)", () => {
test("不存在的 schema 被服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_SHOW_ROUTES, [
"memory",
"profile",
"show",
...memoryScopeCliArgs(),
"--schema-id",
`no-such-schema-${Date.now()}`,
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,214 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_PROFILE_UPDATE_ROUTES } from "../topic-routes.ts";
import { TEST_WORKSPACE_ARGS, type MemoryDryRunBody } from "./shared.ts";
describe("e2e: memory profile update", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--schema-id/i);
expect(stderr).toMatch(/--name/i);
expect(stderr).toMatch(/--description/i);
expect(stderr).toMatch(/--attributes-operations/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --schema-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--name",
"user_basic_v2",
]);
expect(exitCode).toBe(2);
});
test("三个可改字段全不传报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--attributes-operations/i);
});
test("--attributes-operations 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
"[oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--attributes-operations 传对象而非数组报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'{"op":"add","name":"plan"}',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("op 非法取值报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"upsert","name":"plan"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/op/i);
});
test("op=add 缺 name 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"add","description":"套餐"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/name/i);
});
test("op=delete 缺 attribute_id 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"delete"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/attribute_id/i);
});
test("op=update 缺 attribute_id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"update","name":"plan"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / PATCH / name 与 description", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--name",
"user_basic_v2",
"--description",
"升级后的画像",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/profile_schemas\/schema_test$/);
expect(data.method).toBe("PATCH");
expect(data.request?.name).toBe("user_basic_v2");
expect(data.request?.description).toBe("升级后的画像");
expect(data.request?.attributes_operations).toBeUndefined();
});
test("--dry-run 断言三种 op 原样进 body(含 default_value: null)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--attributes-operations",
'[{"op":"add","name":"plan","default_value":"free"},' +
'{"op":"update","attribute_id":"attr_1","default_value":null},' +
'{"op":"delete","attribute_id":"attr_2"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.attributes_operations).toEqual([
{ op: "add", name: "plan", default_value: "free" },
{ op: "update", attribute_id: "attr_1", default_value: null },
{ op: "delete", attribute_id: "attr_2" },
]);
});
test("--dry-run 断言 memory_library_id 进 body 而不是 query string", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_PROFILE_UPDATE_ROUTES, [
"memory",
"profile",
"update",
"--schema-id",
"schema_test",
"--name",
"user_basic_v2",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
// UpdateProfileSchema is the only profile endpoint carrying the library in the body
expect(data.request?.memory_library_id).toBe("lib_test");
expect(data.endpoint).not.toMatch(/memory_library_id/);
});
});
// The live update path is exercised inside the self-cleaning chain in
// memory-profile-delete.e2e.test.ts, which owns the schema it mutates.
@@ -0,0 +1,284 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_SEARCH_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
type MemoryNodeListBody,
} from "./shared.ts";
describe("e2e: memory search", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--query/i);
expect(stderr).toMatch(/--messages/i);
expect(stderr).toMatch(/--top-k/i);
expect(stderr).toMatch(/--min-score/i);
expect(stderr).toMatch(/--enable-rerank/i);
expect(stderr).toMatch(/--enable-judge/i);
expect(stderr).toMatch(/--enable-rewrite/i);
expect(stderr).toMatch(/--plan-version/i);
expect(stderr).toMatch(/--workspace-id/i);
expect(stderr).toMatch(/--project-id/i);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--query",
"编程偏好",
]);
expect(exitCode).toBe(2);
});
test("缺 --query 与 --messages 报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/query|messages/i);
});
test("--top-k 0 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--top-k",
"0",
]);
expect(exitCode).toBe(2);
});
test("--top-k 101 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--top-k",
"101",
]);
expect(exitCode).toBe(2);
});
test("--min-score 1.5 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--min-score",
"1.5",
]);
expect(exitCode).toBe(2);
});
test("--plan-version 非法取值报 USAGE (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--plan-version",
"ultra",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/pro|lite/i);
});
test("--enable-rerank 非 true/false 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--query",
"x",
"--enable-rerank",
"yes",
]);
expect(exitCode).toBe(2);
});
test("--messages 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
memoryUserId(),
"--messages",
"[oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run + --query 断言 endpoint 与 query 被包成单条 user message", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"编程偏好",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\/search$/);
expect(data.method).toBe("POST");
expect(data.request?.messages).toEqual([{ role: "user", content: "编程偏好" }]);
// `query` is not part of the API contract — it must never reach the body
expect(data.request?.query).toBeUndefined();
});
test("--dry-run + --messages 覆盖 --query", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"被忽略",
"--messages",
'[{"role":"user","content":"推荐一本书"}]',
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.messages).toEqual([{ role: "user", content: "推荐一本书" }]);
});
test("--dry-run 断言检索调参全部映射进 body", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"提醒事项",
"--top-k",
"5",
"--min-score",
"0",
"--enable-rerank",
"true",
"--enable-judge",
"true",
"--enable-rewrite",
"false",
"--plan-version",
"lite",
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.top_k).toBe(5);
// 0 is a meaningful threshold — it must not be dropped as falsy
expect(data.request?.min_score).toBe(0);
expect(data.request?.enable_rerank).toBe(true);
expect(data.request?.enable_judge).toBe(true);
expect(data.request?.enable_rewrite).toBe(false);
expect(data.request?.plan_version).toBe("lite");
expect(data.request?.memory_library_id).toBe("lib_test");
});
test("--dry-run 断言可重复 --project-id 汇成 project_ids", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--user-id",
"user1",
"--query",
"多规则混检",
"--project-id",
"proj_a",
"--project-id",
"proj_b",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.project_ids).toEqual(["proj_a", "proj_b"]);
expect(data.request?.project_id).toBeUndefined();
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory search (live)", () => {
test("检索不存在的记忆实体返回空列表而非报错", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
...memoryScopeCliArgs(),
"--user-id",
`no-such-entity-${Date.now()}`,
"--query",
"任何内容",
"--top-k",
"3",
"--plan-version",
"lite",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryNodeListBody>(stdout);
expect(data.request_id?.length ?? 0).toBeGreaterThan(0);
expect(data.memory_nodes ?? []).toHaveLength(0);
});
test("--memory-library-id 不存在时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_SEARCH_ROUTES, [
"memory",
"search",
"--memory-library-id",
"no-such-library-000000000000000",
"--user-id",
memoryUserId(),
"--query",
"任何内容",
"--plan-version",
"lite",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
@@ -0,0 +1,205 @@
import { describe, expect, test } from "vite-plus/test";
import { isMemoryE2EReady, parseStdoutJson, runCommandE2e, runCommandHelp } from "../helpers.ts";
import { MEMORY_UPDATE_ROUTES } from "../topic-routes.ts";
import {
TEST_WORKSPACE_ARGS,
memoryScopeCliArgs,
memoryUserId,
type MemoryDryRunBody,
} from "./shared.ts";
describe("e2e: memory update", () => {
test("--help 展示 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--node-id/i);
expect(stderr).toMatch(/--user-id/i);
expect(stderr).toMatch(/--content/i);
expect(stderr).toMatch(/--timestamp/i);
expect(stderr).toMatch(/--meta-data/i);
expect(stderr).toMatch(/--memory-library-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("缺 --node-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--user-id",
memoryUserId(),
"--content",
"新内容",
]);
expect(exitCode).toBe(2);
});
test("缺 --user-id 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--content",
"新内容",
]);
expect(exitCode).toBe(2);
});
test("缺 --content 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
]);
expect(exitCode).toBe(2);
});
test("--content 513 字符报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"x".repeat(513),
]);
expect(exitCode).toBe(2);
});
test("--timestamp 负数报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"新内容",
"--timestamp",
"-1",
]);
expect(exitCode).toBe(2);
});
test("--meta-data 非法 JSON 报 USAGE (2)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
memoryUserId(),
"--content",
"新内容",
"--meta-data",
"{oops",
...TEST_WORKSPACE_ARGS,
"--dry-run",
]);
expect(exitCode).toBe(2);
});
test("--dry-run 断言 endpoint / PATCH / custom_content", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
"user1",
"--content",
"更新后的记忆内容",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/api\/v2\/apps\/memory\/memory_nodes\/node_test$/);
expect(data.method).toBe("PATCH");
expect(data.request?.user_id).toBe("user1");
expect(data.request?.custom_content).toBe("更新后的记忆内容");
expect(data.request?.timestamp).toBeUndefined();
expect(data.request?.meta_data).toBeUndefined();
});
test("--dry-run 断言 --timestamp/--meta-data/--memory-library-id 映射", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node_test",
"--user-id",
"user1",
"--content",
"在 WAIC 见面",
"--timestamp",
"1747278460",
"--meta-data",
'{"city":"上海"}',
"--memory-library-id",
"lib_test",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.request?.timestamp).toBe(1747278460);
expect(data.request?.meta_data).toEqual({ city: "上海" });
expect(data.request?.memory_library_id).toBe("lib_test");
});
test("--dry-run 断言 node id 被 URL 编码", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
"--node-id",
"node/with space",
"--user-id",
"user1",
"--content",
"编码校验",
...TEST_WORKSPACE_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<MemoryDryRunBody>(stdout);
expect(data.endpoint).toMatch(/memory_nodes\/node%2Fwith%20space$/);
});
});
describe.skipIf(!isMemoryE2EReady())("e2e: memory update (live)", () => {
test("更新不存在的节点时服务端拒绝(非 0 退出)", async () => {
const { exitCode } = await runCommandE2e(MEMORY_UPDATE_ROUTES, [
"memory",
"update",
...memoryScopeCliArgs(),
"--node-id",
`no-such-node-${Date.now()}`,
"--user-id",
memoryUserId(),
"--content",
"不存在的节点",
"--output",
"json",
]);
expect(exitCode).not.toBe(0);
});
});
// The live update-in-place assertion lives in the CRUD chain in memory-delete.e2e.test.ts.
@@ -0,0 +1,126 @@
/**
* Shared fixtures for the memory E2E suite.
*
* The memory API has no "list memory libraries" endpoint, so the library ID
* cannot be discovered from the CLI — it has to be copied from the console
* (Bailian → 应用 → 记忆 → 记忆库 card) into `BAILIAN_E2E_MEMORY_LIBRARY_ID`.
* No hardcoded fallback on purpose: a stale ID would make live cases fail with
* a confusing InvalidParameter instead of skipping via `isMemoryE2EReady()`.
*
* The memory API is served on a workspace-specific host, so every case that
* reaches `run()` — including `--dry-run` — needs a workspace ID.
*/
const DEFAULT_E2E_MEMORY_USER_ID = "e2e-vp-test";
/** Memory entity ID used by every live case; env var wins over the default. */
export function memoryUserId(): string {
return process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID;
}
/**
* Fixed workspace for offline cases: it only shapes the endpoint host, so a
* literal keeps `--dry-run` assertions deterministic on any machine.
*/
export const TEST_WORKSPACE_ARGS = ["--workspace-id", "ws_test"];
/**
* Scope argv for live cases: memory library + workspace. `isMemoryE2EReady()`
* guarantees both env vars are set.
*/
export function memoryScopeCliArgs(): string[] {
const libraryId = process.env.BAILIAN_E2E_MEMORY_LIBRARY_ID?.trim();
const workspaceId = process.env.BAILIAN_WORKSPACE_ID?.trim();
return [
...(libraryId ? ["--memory-library-id", libraryId] : []),
...(workspaceId ? ["--workspace-id", workspaceId] : []),
];
}
/** `--dry-run --output json` payload shape shared by the memory commands. */
export interface MemoryDryRunBody {
endpoint?: string;
method?: string;
request?: {
user_id?: string;
custom_content?: string;
messages?: Array<{ role?: string; content?: string }>;
meta_data?: Record<string, unknown>;
project_id?: string;
project_ids?: string[];
profile_schema?: string;
memory_library_id?: string;
timestamp?: number;
top_k?: number;
min_score?: number;
enable_rerank?: boolean;
enable_judge?: boolean;
enable_rewrite?: boolean;
plan_version?: string;
query?: string;
name?: string;
description?: string;
attributes?: Array<{ name?: string; description?: string; default_value?: string }>;
attributes_operations?: Array<{
op?: string;
attribute_id?: string;
name?: string;
description?: string;
default_value?: string | null;
}>;
};
}
export interface MemoryNodeListBody {
request_id?: string;
total?: number;
memory_nodes?: Array<{
memory_node_id: string;
content: string;
meta_data?: Record<string, unknown>;
created_at?: number;
updated_at?: number;
}>;
}
export interface MemoryAddBody {
request_id?: string;
memory_nodes?: Array<{
memory_node_id: string;
content: string;
event?: string;
old_content?: string;
}>;
}
export interface ProfileSchemaCreateBody {
request_id?: string;
profile_schema_id?: string;
}
export interface ProfileSchemaListBody {
request_id?: string;
total?: number;
profile_schemas?: Array<{ profile_schema_id: string; name: string; description?: string }>;
}
export interface ProfileSchemaDetailBody {
request_id?: string;
name?: string;
description?: string;
attributes?: Array<{
attribute_id: string;
name: string;
description?: string;
default_value?: string;
}>;
}
export interface UserProfileBody {
request_id?: string;
profile?: {
schema_name?: string;
schema_description?: string;
attributes?: Array<{ id: string; name: string; value?: string }>;
};
}
+44 -1
View File
@@ -25,16 +25,59 @@ export const CONFIG_ROUTES: E2eRouteExports = {
"config agent": "configAgent",
};
export const MEMORY_ROUTES: E2eRouteExports = {
export const MEMORY_ADD_ROUTES: E2eRouteExports = {
"memory add": "memoryAdd",
};
export const MEMORY_SEARCH_ROUTES: E2eRouteExports = {
"memory search": "memorySearch",
};
export const MEMORY_LIST_ROUTES: E2eRouteExports = {
"memory list": "memoryList",
};
export const MEMORY_UPDATE_ROUTES: E2eRouteExports = {
"memory update": "memoryUpdate",
};
export const MEMORY_DELETE_ROUTES: E2eRouteExports = {
"memory delete": "memoryDelete",
"memory add": "memoryAdd", // live self-cleaning chain
"memory list": "memoryList", // live: resolve the node id add just produced
"memory search": "memorySearch", // live: verify the node is retrievable
"memory update": "memoryUpdate", // live update step in the chain
};
export const MEMORY_PROFILE_CREATE_ROUTES: E2eRouteExports = {
"memory profile create": "memoryProfileCreate",
};
export const MEMORY_PROFILE_LIST_ROUTES: E2eRouteExports = {
"memory profile list": "memoryProfileList",
};
export const MEMORY_PROFILE_SHOW_ROUTES: E2eRouteExports = {
"memory profile show": "memoryProfileShow",
};
export const MEMORY_PROFILE_UPDATE_ROUTES: E2eRouteExports = {
"memory profile update": "memoryProfileUpdate",
};
export const MEMORY_PROFILE_GET_ROUTES: E2eRouteExports = {
"memory profile get": "memoryProfileGet",
};
export const MEMORY_PROFILE_DELETE_ROUTES: E2eRouteExports = {
"memory profile delete": "memoryProfileDelete",
"memory profile create": "memoryProfileCreate", // live self-cleaning chain
"memory profile list": "memoryProfileList", // live: the new schema shows up in the list
"memory profile show": "memoryProfileShow", // live: attribute_id handles for update
"memory profile update": "memoryProfileUpdate", // live update step in the chain
"memory profile get": "memoryProfileGet", // live: user profile reachable through the schema
};
export const KNOWLEDGE_ROUTES: E2eRouteExports = {
"knowledge retrieve": "knowledgeRetrieve",
"knowledge search": "knowledgeSearch",