mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(memory): 完善记忆及画像相关命令支持并统一参数
- 在 CLI 文档中新增记忆 live 场景说明与变量需求 - 扩展 CLI 命令集,添加记忆画像 Profile 的增删改查功能 - 统一所有记忆相关命令加入 workspace 和 library 参数支持 - 记忆添加命令支持自定义 meta_data,完善输入校验和互斥说明 - 记忆删除、列表、获取用户画像等命令增加工作空间参数支持 - 画像模板相关命令新增分页参数及属性操作接口 - 重构知识命令共享模块,移除重复的 workspace 解析实现,统一导出 - 优化命令输出格式,文本模式下显示更多详细信息和异常提示 - 提供完整的示例参数,增强命令使用说明和提示文档
This commit is contained in:
@@ -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 }>;
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user