@mingxy/cerebro 1.20.4 → 1.20.6
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/package.json +7 -3
- package/src/client.test.ts +373 -0
- package/src/config.test.ts +405 -0
- package/src/hooks-tier1.test.ts +220 -0
- package/src/hooks-tier2.test.ts +275 -0
- package/src/hooks-tier3.test.ts +461 -0
- package/src/hooks.ts +48 -12
- package/src/index.test.ts +190 -0
- package/src/index.ts +12 -2
- package/src/keywords.test.ts +283 -0
- package/src/logger.test.ts +640 -0
- package/src/privacy.test.ts +128 -0
- package/src/tags.test.ts +86 -0
- package/src/tools.test.ts +508 -0
- package/src/tools.ts +34 -0
- package/src/updater.test.ts +380 -0
- package/src/web-server.test.ts +740 -0
- package/src/web-server.ts +8 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stripPrivateContent, isFullyPrivate } from "./privacy.js";
|
|
3
|
+
|
|
4
|
+
describe("stripPrivateContent", () => {
|
|
5
|
+
it("replaces a single <private> block with [REDACTED]", () => {
|
|
6
|
+
const input = "Hello <private>secret</private> world";
|
|
7
|
+
expect(stripPrivateContent(input)).toBe("Hello [REDACTED] world");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("handles multiline private content", () => {
|
|
11
|
+
const input = "before <private>line1\nline2\nline3</private> after";
|
|
12
|
+
expect(stripPrivateContent(input)).toBe("before [REDACTED] after");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("handles multiple private sections", () => {
|
|
16
|
+
const input = "a <private>x</private> b <private>y</private> c";
|
|
17
|
+
expect(stripPrivateContent(input)).toBe("a [REDACTED] b [REDACTED] c");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("is case-insensitive (PRIVATE, Private)", () => {
|
|
21
|
+
expect(stripPrivateContent("<PRIVATE>secret</PRIVATE>")).toBe("[REDACTED]");
|
|
22
|
+
expect(stripPrivateContent("<Private>secret</Private>")).toBe("[REDACTED]");
|
|
23
|
+
expect(stripPrivateContent("<PrIvAtE>secret</PrIvAtE>")).toBe("[REDACTED]");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns original text when no private tags present", () => {
|
|
27
|
+
const input = "nothing private here";
|
|
28
|
+
expect(stripPrivateContent(input)).toBe(input);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("handles empty private block", () => {
|
|
32
|
+
expect(stripPrivateContent("<private></private>")).toBe("[REDACTED]");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("handles private block with only whitespace", () => {
|
|
36
|
+
expect(stripPrivateContent("<private> \n\t </private>")).toBe("[REDACTED]");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("handles adjacent private blocks", () => {
|
|
40
|
+
expect(stripPrivateContent("<private>a</private><private>b</private>")).toBe("[REDACTED][REDACTED]");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles entire string being private", () => {
|
|
44
|
+
expect(stripPrivateContent("<private>everything</private>")).toBe("[REDACTED]");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("preserves content outside of private tags", () => {
|
|
48
|
+
const input = "keep this <private>hide</private> and this too";
|
|
49
|
+
expect(stripPrivateContent(input)).toBe("keep this [REDACTED] and this too");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("handles unclosed private tag gracefully (no match)", () => {
|
|
53
|
+
const input = "text <private>never closes";
|
|
54
|
+
expect(stripPrivateContent(input)).toBe(input);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handles nested-looking tags (non-greedy matches first pair)", () => {
|
|
58
|
+
// The regex is non-greedy, so <private>outer<private>inner</private></private>
|
|
59
|
+
// will match <private>outer<private>inner</private> as first match,
|
|
60
|
+
// leaving </private> unmatched
|
|
61
|
+
const input = "<private>outer<private>inner</private></private>";
|
|
62
|
+
const result = stripPrivateContent(input);
|
|
63
|
+
expect(result).toBe("[REDACTED]</private>");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles empty string", () => {
|
|
67
|
+
expect(stripPrivateContent("")).toBe("");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("handles private tag with special characters inside", () => {
|
|
71
|
+
expect(stripPrivateContent("<private>$#@!&*()</private>")).toBe("[REDACTED]");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("handles private tag with unicode/CJK content", () => {
|
|
75
|
+
expect(stripPrivateContent("<private>中文内容🔐</private>")).toBe("[REDACTED]");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("isFullyPrivate", () => {
|
|
80
|
+
it("returns true when entire text is a single private block", () => {
|
|
81
|
+
expect(isFullyPrivate("<private>everything</private>")).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns true for multiple private blocks with no other content", () => {
|
|
85
|
+
expect(isFullyPrivate("<private>a</private><private>b</private>")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns true for private blocks separated only by whitespace", () => {
|
|
89
|
+
expect(isFullyPrivate("<private>a</private> <private>b</private>")).toBe(true);
|
|
90
|
+
expect(isFullyPrivate("<private>a</private>\n\t<private>b</private>")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns false when there is non-private content", () => {
|
|
94
|
+
expect(isFullyPrivate("Hello <private>secret</private> world")).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns false for plain text with no private tags", () => {
|
|
98
|
+
expect(isFullyPrivate("just some text")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns true for empty string (nothing remains after trim)", () => {
|
|
102
|
+
// stripPrivateContent("") = "", replace redacted = "", trim = "" → length 0
|
|
103
|
+
expect(isFullyPrivate("")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns true for only whitespace after stripping", () => {
|
|
107
|
+
expect(isFullyPrivate(" <private>secret</private> ")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("is case-insensitive", () => {
|
|
111
|
+
expect(isFullyPrivate("<PRIVATE>secret</PRIVATE>")).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("handles multiline fully-private content", () => {
|
|
115
|
+
const input = `<private>line1
|
|
116
|
+
line2
|
|
117
|
+
line3</private>`;
|
|
118
|
+
expect(isFullyPrivate(input)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns false when non-private text remains after stripping", () => {
|
|
122
|
+
expect(isFullyPrivate("<private>hidden</private>visible")).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("returns true for empty private block with no other text", () => {
|
|
126
|
+
expect(isFullyPrivate("<private></private>")).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/tags.test.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getUserTag, getProjectTag } from "./tags.js";
|
|
3
|
+
|
|
4
|
+
describe("getUserTag", () => {
|
|
5
|
+
it("returns tag with omem_user_ prefix", () => {
|
|
6
|
+
const result = getUserTag("test@example.com");
|
|
7
|
+
expect(result).toMatch(/^omem_user_[a-f0-9]{16}$/);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("is deterministic (same input → same output)", () => {
|
|
11
|
+
const email = "user@example.com";
|
|
12
|
+
expect(getUserTag(email)).toBe(getUserTag(email));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("produces different outputs for different inputs", () => {
|
|
16
|
+
expect(getUserTag("alice@example.com")).not.toBe(getUserTag("bob@example.com"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("handles empty string", () => {
|
|
20
|
+
const result = getUserTag("");
|
|
21
|
+
expect(result).toMatch(/^omem_user_[a-f0-9]{16}$/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("produces consistent hash for known input", () => {
|
|
25
|
+
// Verify determinism by calling twice
|
|
26
|
+
const first = getUserTag("test@test.com");
|
|
27
|
+
const second = getUserTag("test@test.com");
|
|
28
|
+
expect(first).toBe(second);
|
|
29
|
+
// Verify it's exactly 16 hex chars after prefix
|
|
30
|
+
const hash = first.replace("omem_user_", "");
|
|
31
|
+
expect(hash).toHaveLength(16);
|
|
32
|
+
expect(hash).toMatch(/^[a-f0-9]+$/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("handles unicode email addresses", () => {
|
|
36
|
+
const result = getUserTag("用户@example.com");
|
|
37
|
+
expect(result).toMatch(/^omem_user_[a-f0-9]{16}$/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("handles very long input", () => {
|
|
41
|
+
const longEmail = "a".repeat(1000) + "@example.com";
|
|
42
|
+
const result = getUserTag(longEmail);
|
|
43
|
+
expect(result).toMatch(/^omem_user_[a-f0-9]{16}$/);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("getProjectTag", () => {
|
|
48
|
+
it("returns tag with omem_project_ prefix", () => {
|
|
49
|
+
const result = getProjectTag("/home/user/project");
|
|
50
|
+
expect(result).toMatch(/^omem_project_[a-f0-9]{16}$/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("is deterministic (same input → same output)", () => {
|
|
54
|
+
const dir = "/home/user/project";
|
|
55
|
+
expect(getProjectTag(dir)).toBe(getProjectTag(dir));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("produces different outputs for different inputs", () => {
|
|
59
|
+
expect(getProjectTag("/path/a")).not.toBe(getProjectTag("/path/b"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("handles empty string", () => {
|
|
63
|
+
const result = getProjectTag("");
|
|
64
|
+
expect(result).toMatch(/^omem_project_[a-f0-9]{16}$/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("produces different tags than getUserTag for same input", () => {
|
|
68
|
+
const input = "test";
|
|
69
|
+
expect(getProjectTag(input)).not.toBe(getUserTag(input));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles Windows-style paths", () => {
|
|
73
|
+
const result = getProjectTag("C:\\Users\\dev\\project");
|
|
74
|
+
expect(result).toMatch(/^omem_project_[a-f0-9]{16}$/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles paths with trailing slashes", () => {
|
|
78
|
+
const result = getProjectTag("/home/user/project/");
|
|
79
|
+
expect(result).toMatch(/^omem_project_[a-f0-9]{16}$/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles paths with unicode characters", () => {
|
|
83
|
+
const result = getProjectTag("/home/用户/项目");
|
|
84
|
+
expect(result).toMatch(/^omem_project_[a-f0-9]{16}$/);
|
|
85
|
+
});
|
|
86
|
+
});
|