@clawling/clawchat-plugin-openclaw 2026.5.12-28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
editClawChatMemoryBody,
|
|
7
|
+
ensureClawChatMemoryTargetSafe,
|
|
8
|
+
readClawChatMemoryFile,
|
|
9
|
+
resolveClawChatMemoryPath,
|
|
10
|
+
searchClawChatMemory,
|
|
11
|
+
writeClawChatMemoryBody,
|
|
12
|
+
writeClawChatMetadata,
|
|
13
|
+
} from "./clawchat-memory.ts";
|
|
14
|
+
|
|
15
|
+
const metadataStart = "<!-- clawchat:metadata:start -->";
|
|
16
|
+
const metadataEnd = "<!-- clawchat:metadata:end -->";
|
|
17
|
+
|
|
18
|
+
const tempRoots: string[] = [];
|
|
19
|
+
|
|
20
|
+
function tempMemoryRoot(): string {
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawchat-plugin-openclaw-memory-"));
|
|
22
|
+
tempRoots.push(dir);
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
for (const dir of tempRoots.splice(0)) {
|
|
29
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("clawchat memory path safety", () => {
|
|
34
|
+
it("resolves owner target to owner.md", () => {
|
|
35
|
+
const root = tempMemoryRoot();
|
|
36
|
+
|
|
37
|
+
expect(resolveClawChatMemoryPath(root, { targetType: "owner", targetId: "owner" })).toBe(
|
|
38
|
+
path.join(root, "owner.md"),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("resolves user target to users file", () => {
|
|
43
|
+
const root = tempMemoryRoot();
|
|
44
|
+
|
|
45
|
+
expect(resolveClawChatMemoryPath(root, { targetType: "user", targetId: "usr_1" })).toBe(
|
|
46
|
+
path.join(root, "users", "usr_1.md"),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("resolves group target to groups file", () => {
|
|
51
|
+
const root = tempMemoryRoot();
|
|
52
|
+
|
|
53
|
+
expect(resolveClawChatMemoryPath(root, { targetType: "group", targetId: "grp_1" })).toBe(
|
|
54
|
+
path.join(root, "groups", "grp_1.md"),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("rejects owner target with non-owner target id", () => {
|
|
59
|
+
const root = tempMemoryRoot();
|
|
60
|
+
|
|
61
|
+
expect(() => resolveClawChatMemoryPath(root, { targetType: "owner", targetId: "usr_1" })).toThrow(
|
|
62
|
+
/owner targetId/i,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects invalid target types at runtime", () => {
|
|
67
|
+
const root = tempMemoryRoot();
|
|
68
|
+
const target = { targetType: "channel", targetId: "chn_1" } as unknown as Parameters<
|
|
69
|
+
typeof resolveClawChatMemoryPath
|
|
70
|
+
>[1];
|
|
71
|
+
|
|
72
|
+
expect(() => resolveClawChatMemoryPath(root, target)).toThrow(/targetType/i);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it.each([
|
|
76
|
+
["empty", ""],
|
|
77
|
+
["dot", "."],
|
|
78
|
+
["dot-dot", ".."],
|
|
79
|
+
["embedded dot-dot", "usr_.._1"],
|
|
80
|
+
["slash", "usr/1"],
|
|
81
|
+
["backslash", "usr\\1"],
|
|
82
|
+
["NUL", "usr\0 1"],
|
|
83
|
+
["control char", "usr\n1"],
|
|
84
|
+
["C1 control char", "usr\u00851"],
|
|
85
|
+
])("rejects %s target ids", (_label, targetId) => {
|
|
86
|
+
const root = tempMemoryRoot();
|
|
87
|
+
|
|
88
|
+
expect(() => resolveClawChatMemoryPath(root, { targetType: "user", targetId })).toThrow(/targetId/i);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it.each([
|
|
92
|
+
["number", 123],
|
|
93
|
+
["null", null],
|
|
94
|
+
["object", { id: "usr_1" }],
|
|
95
|
+
])("rejects %s target ids at runtime", (_label, targetId) => {
|
|
96
|
+
const root = tempMemoryRoot();
|
|
97
|
+
const target = { targetType: "user", targetId } as unknown as Parameters<typeof resolveClawChatMemoryPath>[1];
|
|
98
|
+
|
|
99
|
+
expect(() => resolveClawChatMemoryPath(root, target)).toThrow(/targetId must be a string/i);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects an existing target symlink before reading or writing", async () => {
|
|
103
|
+
const root = tempMemoryRoot();
|
|
104
|
+
const outside = tempMemoryRoot();
|
|
105
|
+
fs.mkdirSync(path.join(root, "users"));
|
|
106
|
+
fs.writeFileSync(path.join(outside, "usr_1.md"), "outside");
|
|
107
|
+
fs.symlinkSync(path.join(outside, "usr_1.md"), path.join(root, "users", "usr_1.md"));
|
|
108
|
+
|
|
109
|
+
await expect(ensureClawChatMemoryTargetSafe(root, { targetType: "user", targetId: "usr_1" })).rejects.toThrow(
|
|
110
|
+
/symlink/i,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it.each([
|
|
115
|
+
["users", "user", "usr_1"],
|
|
116
|
+
["groups", "group", "grp_1"],
|
|
117
|
+
] as const)("rejects an existing %s symlink", async (_dirName, targetType, targetId) => {
|
|
118
|
+
const root = tempMemoryRoot();
|
|
119
|
+
const outside = tempMemoryRoot();
|
|
120
|
+
fs.symlinkSync(outside, path.join(root, targetType === "user" ? "users" : "groups"), "dir");
|
|
121
|
+
|
|
122
|
+
await expect(ensureClawChatMemoryTargetSafe(root, { targetType, targetId })).rejects.toThrow(/symlink/i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects users parent when it exists as a regular file", async () => {
|
|
126
|
+
const root = tempMemoryRoot();
|
|
127
|
+
fs.writeFileSync(path.join(root, "users"), "not a directory");
|
|
128
|
+
|
|
129
|
+
await expect(ensureClawChatMemoryTargetSafe(root, { targetType: "user", targetId: "usr_1" })).rejects.toThrow(
|
|
130
|
+
/not a directory/i,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("rejects an existing target directory", async () => {
|
|
135
|
+
const root = tempMemoryRoot();
|
|
136
|
+
fs.mkdirSync(path.join(root, "groups"), { recursive: true });
|
|
137
|
+
fs.mkdirSync(path.join(root, "groups", "grp_1.md"));
|
|
138
|
+
|
|
139
|
+
await expect(ensureClawChatMemoryTargetSafe(root, { targetType: "group", targetId: "grp_1" })).rejects.toThrow(
|
|
140
|
+
/not a regular file/i,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects a resolved target path outside the root", async () => {
|
|
145
|
+
const root = tempMemoryRoot();
|
|
146
|
+
const outside = tempMemoryRoot();
|
|
147
|
+
const targetPath = path.join(root, "owner.md");
|
|
148
|
+
fs.writeFileSync(targetPath, "owner");
|
|
149
|
+
|
|
150
|
+
const realpath = fs.promises.realpath.bind(fs.promises);
|
|
151
|
+
vi.spyOn(fs.promises, "realpath").mockImplementation(async (candidate) => {
|
|
152
|
+
if (String(candidate) === targetPath) {
|
|
153
|
+
return path.join(outside, "owner.md");
|
|
154
|
+
}
|
|
155
|
+
return realpath(candidate);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await expect(ensureClawChatMemoryTargetSafe(root, { targetType: "owner", targetId: "owner" })).rejects.toThrow(
|
|
159
|
+
/outside root/i,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("clawchat memory file metadata and body", () => {
|
|
165
|
+
it("returns empty content for missing files without creating them", async () => {
|
|
166
|
+
const root = tempMemoryRoot();
|
|
167
|
+
|
|
168
|
+
await expect(readClawChatMemoryFile(root, { targetType: "user", targetId: "usr_1" })).resolves.toEqual({
|
|
169
|
+
exists: false,
|
|
170
|
+
metadata: {},
|
|
171
|
+
body: "",
|
|
172
|
+
raw: "",
|
|
173
|
+
});
|
|
174
|
+
expect(fs.existsSync(path.join(root, "users", "usr_1.md"))).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns the full body and empty metadata when the file has no metadata block", async () => {
|
|
178
|
+
const root = tempMemoryRoot();
|
|
179
|
+
fs.mkdirSync(path.join(root, "groups"));
|
|
180
|
+
fs.writeFileSync(path.join(root, "groups", "grp_1.md"), "A note\nwith two lines");
|
|
181
|
+
|
|
182
|
+
await expect(readClawChatMemoryFile(root, { targetType: "group", targetId: "grp_1" })).resolves.toEqual({
|
|
183
|
+
exists: true,
|
|
184
|
+
metadata: {},
|
|
185
|
+
body: "A note\nwith two lines",
|
|
186
|
+
raw: "A note\nwith two lines",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("parses metadata only between exact marker lines", async () => {
|
|
191
|
+
const root = tempMemoryRoot();
|
|
192
|
+
fs.writeFileSync(
|
|
193
|
+
path.join(root, "owner.md"),
|
|
194
|
+
[
|
|
195
|
+
metadataStart,
|
|
196
|
+
"nickname: Alice",
|
|
197
|
+
"malformed line",
|
|
198
|
+
"bio: hello: world",
|
|
199
|
+
metadataEnd,
|
|
200
|
+
"",
|
|
201
|
+
"nickname: Body Value",
|
|
202
|
+
"Body text",
|
|
203
|
+
].join("\n"),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await expect(readClawChatMemoryFile(root, { targetType: "owner", targetId: "owner" })).resolves.toMatchObject({
|
|
207
|
+
metadata: { nickname: "Alice", bio: "hello: world" },
|
|
208
|
+
body: "nickname: Body Value\nBody text",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
path.join(root, "owner.md"),
|
|
213
|
+
[`${metadataStart} `, "nickname: Alice", metadataEnd, "", "Body text"].join("\n"),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await expect(readClawChatMemoryFile(root, { targetType: "owner", targetId: "owner" })).resolves.toMatchObject({
|
|
217
|
+
metadata: {},
|
|
218
|
+
body: [`${metadataStart} `, "nickname: Alice", metadataEnd, "", "Body text"].join("\n"),
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("replaces the metadata block while preserving the body", async () => {
|
|
223
|
+
const root = tempMemoryRoot();
|
|
224
|
+
fs.writeFileSync(
|
|
225
|
+
path.join(root, "owner.md"),
|
|
226
|
+
[metadataStart, "nickname: Old", metadataEnd, "", "Remember this.", "And this."].join("\n"),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await writeClawChatMetadata(root, { targetType: "owner", targetId: "owner" }, { nickname: "New" });
|
|
230
|
+
|
|
231
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
232
|
+
[metadataStart, "nickname: New", metadataEnd, "", "Remember this.", "And this."].join("\n"),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("replaces metadata without normalizing CRLF body content", async () => {
|
|
237
|
+
const root = tempMemoryRoot();
|
|
238
|
+
const body = "\r\nFirst line\r\nSecond line\r\n";
|
|
239
|
+
fs.writeFileSync(path.join(root, "owner.md"), `${metadataStart}\nowner_id: old\n${metadataEnd}${body}`);
|
|
240
|
+
|
|
241
|
+
await writeClawChatMetadata(root, { targetType: "owner", targetId: "owner" }, { owner_id: "new" });
|
|
242
|
+
|
|
243
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
244
|
+
`${metadataStart}\nowner_id: new\n${metadataEnd}${body}`,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("replaces the body while preserving the metadata block", async () => {
|
|
249
|
+
const root = tempMemoryRoot();
|
|
250
|
+
fs.writeFileSync(path.join(root, "owner.md"), [metadataStart, "nickname: Alice", metadataEnd, "", "Old"].join("\n"));
|
|
251
|
+
|
|
252
|
+
await writeClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "replace", "New body");
|
|
253
|
+
|
|
254
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
255
|
+
[metadataStart, "nickname: Alice", metadataEnd, "", "New body"].join("\n"),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it.each([
|
|
260
|
+
["owner", { targetType: "owner", targetId: "owner" }, "owner.md"],
|
|
261
|
+
["user", { targetType: "user", targetId: "usr_1" }, path.join("users", "usr_1.md")],
|
|
262
|
+
["group", { targetType: "group", targetId: "grp_1" }, path.join("groups", "grp_1.md")],
|
|
263
|
+
] as const)("creates missing %s file on replace", async (_label, target, relativePath) => {
|
|
264
|
+
const root = tempMemoryRoot();
|
|
265
|
+
const targetPath = path.join(root, relativePath);
|
|
266
|
+
|
|
267
|
+
await writeClawChatMemoryBody(root, target, "replace", "New body");
|
|
268
|
+
|
|
269
|
+
expect(fs.readFileSync(targetPath, "utf8")).toBe("New body");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("appends body content while preserving a CRLF metadata block", async () => {
|
|
273
|
+
const root = tempMemoryRoot();
|
|
274
|
+
const rawMetadataBlock = `${metadataStart}\r\nnickname: Alice\r\n${metadataEnd}`;
|
|
275
|
+
fs.writeFileSync(path.join(root, "owner.md"), `${rawMetadataBlock}\r\n\r\nOld`);
|
|
276
|
+
|
|
277
|
+
await writeClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "append", "New");
|
|
278
|
+
|
|
279
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(`${rawMetadataBlock}\r\n\r\nOld\nNew`);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("replaces body content while preserving a CRLF metadata block", async () => {
|
|
283
|
+
const root = tempMemoryRoot();
|
|
284
|
+
const rawMetadataBlock = `${metadataStart}\r\nnickname: Alice\r\n${metadataEnd}`;
|
|
285
|
+
fs.writeFileSync(path.join(root, "owner.md"), `${rawMetadataBlock}\r\n\r\nOld\r\nBody`);
|
|
286
|
+
|
|
287
|
+
await writeClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "replace", "New body");
|
|
288
|
+
|
|
289
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(`${rawMetadataBlock}\r\n\r\nNew body`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("normalizes metadata values to a single line", async () => {
|
|
293
|
+
const root = tempMemoryRoot();
|
|
294
|
+
|
|
295
|
+
await writeClawChatMetadata(root, { targetType: "owner", targetId: "owner" }, {
|
|
296
|
+
bio: "Line 1\r\n\r\nLine 2\nLine 3\rLine 4",
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
300
|
+
[metadataStart, "bio: Line 1 Line 2 Line 3 Line 4", metadataEnd].join("\n"),
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("fills default agent behavior when owner.md is first created with empty behavior", async () => {
|
|
305
|
+
const root = tempMemoryRoot();
|
|
306
|
+
|
|
307
|
+
await writeClawChatMetadata(root, { targetType: "owner", targetId: "owner" }, {
|
|
308
|
+
agent_id: "usr_self",
|
|
309
|
+
agent_behavior: "",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const file = await readClawChatMemoryFile(root, { targetType: "owner", targetId: "owner" });
|
|
313
|
+
expect(file.metadata.agent_behavior).toContain("【鼓励】");
|
|
314
|
+
expect(file.metadata.agent_behavior).toContain("公开发布内容、修改个人资料");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("fills default group description when group metadata is first created with empty description", async () => {
|
|
318
|
+
const root = tempMemoryRoot();
|
|
319
|
+
|
|
320
|
+
await writeClawChatMetadata(root, { targetType: "group", targetId: "grp_1" }, {
|
|
321
|
+
group_id: "grp_1",
|
|
322
|
+
group_description: "",
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const file = await readClawChatMemoryFile(root, { targetType: "group", targetId: "grp_1" });
|
|
326
|
+
expect(file.metadata.group_description).toContain("【群目标】");
|
|
327
|
+
expect(file.metadata.group_description).toContain("Agent 参与范围");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("does not fill default owner behavior when owner.md already exists", async () => {
|
|
331
|
+
const root = tempMemoryRoot();
|
|
332
|
+
fs.writeFileSync(
|
|
333
|
+
path.join(root, "owner.md"),
|
|
334
|
+
[metadataStart, "agent_behavior: ", metadataEnd].join("\n"),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
await writeClawChatMetadata(root, { targetType: "owner", targetId: "owner" }, {
|
|
338
|
+
agent_behavior: "",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const file = await readClawChatMemoryFile(root, { targetType: "owner", targetId: "owner" });
|
|
342
|
+
expect(file.metadata.agent_behavior).toBe("");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("preserves non-empty owner behavior and group description on first creation", async () => {
|
|
346
|
+
const root = tempMemoryRoot();
|
|
347
|
+
|
|
348
|
+
await writeClawChatMetadata(root, { targetType: "owner", targetId: "owner" }, {
|
|
349
|
+
agent_behavior: "Use the configured behavior.",
|
|
350
|
+
});
|
|
351
|
+
await writeClawChatMetadata(root, { targetType: "group", targetId: "grp_1" }, {
|
|
352
|
+
group_description: "Existing group bio.",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await expect(readClawChatMemoryFile(root, { targetType: "owner", targetId: "owner" }))
|
|
356
|
+
.resolves.toMatchObject({ metadata: { agent_behavior: "Use the configured behavior." } });
|
|
357
|
+
await expect(readClawChatMemoryFile(root, { targetType: "group", targetId: "grp_1" }))
|
|
358
|
+
.resolves.toMatchObject({ metadata: { group_description: "Existing group bio." } });
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("appends body content with a joining newline and no trailing newline", async () => {
|
|
362
|
+
const root = tempMemoryRoot();
|
|
363
|
+
fs.writeFileSync(path.join(root, "owner.md"), [metadataStart, "nickname: Alice", metadataEnd, "", "Old"].join("\n"));
|
|
364
|
+
|
|
365
|
+
await writeClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "append", "New");
|
|
366
|
+
|
|
367
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
368
|
+
[metadataStart, "nickname: Alice", metadataEnd, "", "Old", "New"].join("\n"),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it.each([
|
|
373
|
+
["overwrite", "overwrite"],
|
|
374
|
+
["undefined", undefined],
|
|
375
|
+
])("rejects invalid body write mode %s without mutating", async (_label, mode) => {
|
|
376
|
+
const root = tempMemoryRoot();
|
|
377
|
+
const target = { targetType: "owner", targetId: "owner" } as const;
|
|
378
|
+
const targetPath = path.join(root, "owner.md");
|
|
379
|
+
fs.writeFileSync(targetPath, "Old");
|
|
380
|
+
|
|
381
|
+
await expect(writeClawChatMemoryBody(root, target, mode as "append", "New")).rejects.toThrow(/mode/i);
|
|
382
|
+
expect(fs.readFileSync(targetPath, "utf8")).toBe("Old");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("appends body content while preserving pre-existing CRLF body content", async () => {
|
|
386
|
+
const root = tempMemoryRoot();
|
|
387
|
+
fs.writeFileSync(path.join(root, "owner.md"), [metadataStart, "nickname: Alice", metadataEnd].join("\n") + "\n\nOne\r\nTwo");
|
|
388
|
+
|
|
389
|
+
await writeClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "append", "Three");
|
|
390
|
+
|
|
391
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
392
|
+
[metadataStart, "nickname: Alice", metadataEnd].join("\n") + "\n\nOne\r\nTwo\nThree",
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("edits exactly one normalized body match while preserving metadata", async () => {
|
|
397
|
+
const root = tempMemoryRoot();
|
|
398
|
+
fs.writeFileSync(
|
|
399
|
+
path.join(root, "owner.md"),
|
|
400
|
+
[metadataStart, "nickname: Alice", metadataEnd, "", "one", "two", "three"].join("\n"),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
await editClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "one\r\ntwo", "ONE\r\nTWO");
|
|
404
|
+
|
|
405
|
+
expect(fs.readFileSync(path.join(root, "owner.md"), "utf8")).toBe(
|
|
406
|
+
[metadataStart, "nickname: Alice", metadataEnd, "", "ONE", "TWO", "three"].join("\n"),
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("rejects body edits with empty or non-unique old text", async () => {
|
|
411
|
+
const root = tempMemoryRoot();
|
|
412
|
+
fs.writeFileSync(path.join(root, "owner.md"), "same\nsame");
|
|
413
|
+
|
|
414
|
+
await expect(
|
|
415
|
+
editClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "", "new"),
|
|
416
|
+
).rejects.toThrow(/oldText/i);
|
|
417
|
+
await expect(
|
|
418
|
+
editClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "same", "new"),
|
|
419
|
+
).rejects.toThrow(/exactly one/i);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("rejects body edits with overlapping old text matches", async () => {
|
|
423
|
+
const root = tempMemoryRoot();
|
|
424
|
+
fs.writeFileSync(path.join(root, "owner.md"), "aaa");
|
|
425
|
+
|
|
426
|
+
await expect(
|
|
427
|
+
editClawChatMemoryBody(root, { targetType: "owner", targetId: "owner" }, "aa", "b"),
|
|
428
|
+
).rejects.toThrow(/exactly one/i);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("clawchat memory search", () => {
|
|
433
|
+
it("finds local user memory by alias in metadata or body without returning paths", async () => {
|
|
434
|
+
const root = tempMemoryRoot();
|
|
435
|
+
fs.mkdirSync(path.join(root, "users"));
|
|
436
|
+
fs.writeFileSync(
|
|
437
|
+
path.join(root, "users", "usr_1.md"),
|
|
438
|
+
[
|
|
439
|
+
metadataStart,
|
|
440
|
+
"nickname: 小糖糖",
|
|
441
|
+
metadataEnd,
|
|
442
|
+
"",
|
|
443
|
+
"known_as: 才哥",
|
|
444
|
+
"likes product design",
|
|
445
|
+
].join("\n"),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
await expect(searchClawChatMemory(root, { query: "才哥", targetTypes: ["user"] })).resolves.toMatchObject({
|
|
449
|
+
query: "才哥",
|
|
450
|
+
matches: [
|
|
451
|
+
{
|
|
452
|
+
targetType: "user",
|
|
453
|
+
targetId: "usr_1",
|
|
454
|
+
matchedFields: ["body"],
|
|
455
|
+
snippets: ["known_as: 才哥"],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
truncated: false,
|
|
459
|
+
});
|
|
460
|
+
const result = await searchClawChatMemory(root, { query: "才哥", targetTypes: ["user"] });
|
|
461
|
+
expect(JSON.stringify(result)).not.toContain(root);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("returns bounded ranked matches across owner users and groups", async () => {
|
|
465
|
+
const root = tempMemoryRoot();
|
|
466
|
+
fs.mkdirSync(path.join(root, "users"));
|
|
467
|
+
fs.mkdirSync(path.join(root, "groups"));
|
|
468
|
+
fs.writeFileSync(path.join(root, "owner.md"), [metadataStart, "agent_owner_nickname: Colin", metadataEnd].join("\n"));
|
|
469
|
+
fs.writeFileSync(path.join(root, "users", "usr_1.md"), "known_as: Colin");
|
|
470
|
+
fs.writeFileSync(path.join(root, "groups", "grp_1.md"), "Colin group note");
|
|
471
|
+
|
|
472
|
+
const result = await searchClawChatMemory(root, { query: "Colin", maxResults: 2 });
|
|
473
|
+
|
|
474
|
+
expect(result.matches.map((match) => `${match.targetType}:${match.targetId}`)).toEqual([
|
|
475
|
+
"owner:owner",
|
|
476
|
+
"user:usr_1",
|
|
477
|
+
]);
|
|
478
|
+
expect(result.truncated).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
});
|