openclawdreams 0.7.0
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/.env.example +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/dependabot.yml +17 -0
- package/.github/pull_request_template.md +19 -0
- package/.github/workflows/build.yml +30 -0
- package/.github/workflows/release.yml +110 -0
- package/.prettierignore +4 -0
- package/.prettierrc +7 -0
- package/.versionrc.json +26 -0
- package/AGENTS.md +286 -0
- package/CHANGELOG.md +157 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +363 -0
- package/SECURITY.md +39 -0
- package/bin/electricsheep.ts +5 -0
- package/dist/bin/electricsheep.d.ts +3 -0
- package/dist/bin/electricsheep.d.ts.map +1 -0
- package/dist/bin/electricsheep.js +4 -0
- package/dist/bin/electricsheep.js.map +1 -0
- package/dist/src/budget.d.ts +28 -0
- package/dist/src/budget.d.ts.map +1 -0
- package/dist/src/budget.js +87 -0
- package/dist/src/budget.js.map +1 -0
- package/dist/src/cli.d.ts +19 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +289 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +37 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +70 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/crypto.d.ts +19 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +70 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/dreamer.d.ts +13 -0
- package/dist/src/dreamer.d.ts.map +1 -0
- package/dist/src/dreamer.js +213 -0
- package/dist/src/dreamer.js.map +1 -0
- package/dist/src/filter.d.ts +30 -0
- package/dist/src/filter.d.ts.map +1 -0
- package/dist/src/filter.js +124 -0
- package/dist/src/filter.js.map +1 -0
- package/dist/src/identity.d.ts +29 -0
- package/dist/src/identity.d.ts.map +1 -0
- package/dist/src/identity.js +83 -0
- package/dist/src/identity.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +293 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/llm.d.ts +26 -0
- package/dist/src/llm.d.ts.map +1 -0
- package/dist/src/llm.js +40 -0
- package/dist/src/llm.js.map +1 -0
- package/dist/src/logger.d.ts +6 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +32 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/memory.d.ts +41 -0
- package/dist/src/memory.d.ts.map +1 -0
- package/dist/src/memory.js +206 -0
- package/dist/src/memory.js.map +1 -0
- package/dist/src/moltbook-search.d.ts +23 -0
- package/dist/src/moltbook-search.d.ts.map +1 -0
- package/dist/src/moltbook-search.js +85 -0
- package/dist/src/moltbook-search.js.map +1 -0
- package/dist/src/moltbook.d.ts +34 -0
- package/dist/src/moltbook.d.ts.map +1 -0
- package/dist/src/moltbook.js +165 -0
- package/dist/src/moltbook.js.map +1 -0
- package/dist/src/notify.d.ts +18 -0
- package/dist/src/notify.d.ts.map +1 -0
- package/dist/src/notify.js +98 -0
- package/dist/src/notify.js.map +1 -0
- package/dist/src/persona.d.ts +26 -0
- package/dist/src/persona.d.ts.map +1 -0
- package/dist/src/persona.js +178 -0
- package/dist/src/persona.js.map +1 -0
- package/dist/src/reflection.d.ts +26 -0
- package/dist/src/reflection.d.ts.map +1 -0
- package/dist/src/reflection.js +111 -0
- package/dist/src/reflection.js.map +1 -0
- package/dist/src/state.d.ts +7 -0
- package/dist/src/state.d.ts.map +1 -0
- package/dist/src/state.js +40 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/synthesis.d.ts +29 -0
- package/dist/src/synthesis.d.ts.map +1 -0
- package/dist/src/synthesis.js +125 -0
- package/dist/src/synthesis.js.map +1 -0
- package/dist/src/topics.d.ts +19 -0
- package/dist/src/topics.d.ts.map +1 -0
- package/dist/src/topics.js +83 -0
- package/dist/src/topics.js.map +1 -0
- package/dist/src/types.d.ts +179 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/waking.d.ts +24 -0
- package/dist/src/waking.d.ts.map +1 -0
- package/dist/src/waking.js +152 -0
- package/dist/src/waking.js.map +1 -0
- package/dist/src/web-search.d.ts +23 -0
- package/dist/src/web-search.d.ts.map +1 -0
- package/dist/src/web-search.js +64 -0
- package/dist/src/web-search.js.map +1 -0
- package/dist/test/budget.test.d.ts +2 -0
- package/dist/test/budget.test.d.ts.map +1 -0
- package/dist/test/budget.test.js +258 -0
- package/dist/test/budget.test.js.map +1 -0
- package/dist/test/crypto.test.d.ts +2 -0
- package/dist/test/crypto.test.d.ts.map +1 -0
- package/dist/test/crypto.test.js +93 -0
- package/dist/test/crypto.test.js.map +1 -0
- package/dist/test/dreamer.test.d.ts +2 -0
- package/dist/test/dreamer.test.d.ts.map +1 -0
- package/dist/test/dreamer.test.js +79 -0
- package/dist/test/dreamer.test.js.map +1 -0
- package/dist/test/filter.test.d.ts +2 -0
- package/dist/test/filter.test.d.ts.map +1 -0
- package/dist/test/filter.test.js +92 -0
- package/dist/test/filter.test.js.map +1 -0
- package/dist/test/memory.test.d.ts +2 -0
- package/dist/test/memory.test.d.ts.map +1 -0
- package/dist/test/memory.test.js +138 -0
- package/dist/test/memory.test.js.map +1 -0
- package/dist/test/moltbook.test.d.ts +2 -0
- package/dist/test/moltbook.test.d.ts.map +1 -0
- package/dist/test/moltbook.test.js +164 -0
- package/dist/test/moltbook.test.js.map +1 -0
- package/dist/test/persona.test.d.ts +2 -0
- package/dist/test/persona.test.d.ts.map +1 -0
- package/dist/test/persona.test.js +44 -0
- package/dist/test/persona.test.js.map +1 -0
- package/dist/test/reflection.test.d.ts +2 -0
- package/dist/test/reflection.test.d.ts.map +1 -0
- package/dist/test/reflection.test.js +57 -0
- package/dist/test/reflection.test.js.map +1 -0
- package/dist/test/state.test.d.ts +2 -0
- package/dist/test/state.test.d.ts.map +1 -0
- package/dist/test/state.test.js +50 -0
- package/dist/test/state.test.js.map +1 -0
- package/dist/test/waking.test.d.ts +2 -0
- package/dist/test/waking.test.d.ts.map +1 -0
- package/dist/test/waking.test.js +149 -0
- package/dist/test/waking.test.js.map +1 -0
- package/eslint.config.js +35 -0
- package/openclaw.plugin.json +62 -0
- package/package.json +72 -0
- package/skills/electricsheep.skill.md +69 -0
- package/skills/setup-guide/SKILL.md +303 -0
- package/src/budget.ts +104 -0
- package/src/cli.ts +325 -0
- package/src/config.ts +95 -0
- package/src/crypto.ts +82 -0
- package/src/dreamer.ts +283 -0
- package/src/filter.ts +146 -0
- package/src/identity.ts +92 -0
- package/src/index.ts +356 -0
- package/src/llm.ts +61 -0
- package/src/logger.ts +46 -0
- package/src/memory.ts +276 -0
- package/src/moltbook-search.ts +116 -0
- package/src/moltbook.ts +235 -0
- package/src/notify.ts +124 -0
- package/src/persona.ts +191 -0
- package/src/reflection.ts +150 -0
- package/src/state.ts +44 -0
- package/src/synthesis.ts +153 -0
- package/src/topics.ts +103 -0
- package/src/types.ts +196 -0
- package/src/waking.ts +199 -0
- package/src/web-search.ts +88 -0
- package/test/budget.test.ts +316 -0
- package/test/crypto.test.ts +112 -0
- package/test/dreamer.test.ts +95 -0
- package/test/filter.test.ts +115 -0
- package/test/memory.test.ts +182 -0
- package/test/moltbook.test.ts +209 -0
- package/test/persona.test.ts +59 -0
- package/test/reflection.test.ts +71 -0
- package/test/state.test.ts +57 -0
- package/test/waking.test.ts +214 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
// Isolated data dir
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), "es-memory-test-"));
|
|
9
|
+
process.env.ELECTRICSHEEP_DATA_DIR = testDir;
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
storeDeepMemory,
|
|
13
|
+
retrieveUndreamedMemories,
|
|
14
|
+
markAsDreamed,
|
|
15
|
+
deepMemoryStats,
|
|
16
|
+
getRecentDeepMemories,
|
|
17
|
+
formatDeepMemoryContext,
|
|
18
|
+
remember,
|
|
19
|
+
closeDb,
|
|
20
|
+
} = await import("../src/memory.js");
|
|
21
|
+
|
|
22
|
+
const { DEEP_MEMORY_DB } = await import("../src/config.js");
|
|
23
|
+
|
|
24
|
+
describe("Deep Memory", () => {
|
|
25
|
+
it("stores and retrieves encrypted memories", () => {
|
|
26
|
+
storeDeepMemory({ message: "test interaction" }, "interaction");
|
|
27
|
+
storeDeepMemory({ message: "another one" }, "comment");
|
|
28
|
+
|
|
29
|
+
const memories = retrieveUndreamedMemories();
|
|
30
|
+
assert.equal(memories.length, 2);
|
|
31
|
+
assert.equal(memories[0].category, "interaction");
|
|
32
|
+
assert.deepEqual(memories[0].content, { message: "test interaction" });
|
|
33
|
+
assert.equal(memories[1].category, "comment");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("marks memories as dreamed", () => {
|
|
37
|
+
const before = retrieveUndreamedMemories();
|
|
38
|
+
const ids = before.map((m) => m.id);
|
|
39
|
+
markAsDreamed(ids);
|
|
40
|
+
|
|
41
|
+
const afterDream = retrieveUndreamedMemories();
|
|
42
|
+
assert.equal(afterDream.length, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("tracks stats correctly", () => {
|
|
46
|
+
// Previous memories are now dreamed
|
|
47
|
+
storeDeepMemory({ msg: "new" }, "upvote");
|
|
48
|
+
const stats = deepMemoryStats();
|
|
49
|
+
|
|
50
|
+
assert.equal(stats.total_memories, 3); // 2 dreamed + 1 new
|
|
51
|
+
assert.equal(stats.undreamed, 1);
|
|
52
|
+
assert.equal(stats.dreamed, 2);
|
|
53
|
+
assert.ok(stats.categories.upvote);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("marks empty array as no-op", () => {
|
|
57
|
+
markAsDreamed([]); // should not throw
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("handles corrupted blobs gracefully", async () => {
|
|
61
|
+
// Close singleton so we can insert garbage data directly
|
|
62
|
+
closeDb();
|
|
63
|
+
const Database = (await import("better-sqlite3")).default;
|
|
64
|
+
const db = new Database(DEEP_MEMORY_DB);
|
|
65
|
+
db.prepare(
|
|
66
|
+
`INSERT INTO deep_memories (timestamp, category, encrypted_blob, content_hash)
|
|
67
|
+
VALUES (?, ?, ?, ?)`
|
|
68
|
+
).run(new Date().toISOString(), "test", "not-valid-encrypted-data", "abc");
|
|
69
|
+
db.close();
|
|
70
|
+
|
|
71
|
+
const memories = retrieveUndreamedMemories();
|
|
72
|
+
const corrupted = memories.find((m) => m.category === "corrupted");
|
|
73
|
+
assert.ok(corrupted, "corrupted memory should be returned");
|
|
74
|
+
assert.deepEqual(corrupted.content, {
|
|
75
|
+
note: "This memory could not be recovered.",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("getRecentDeepMemories", () => {
|
|
81
|
+
it("filters by category", () => {
|
|
82
|
+
storeDeepMemory({ summary: "interaction 1" }, "interaction");
|
|
83
|
+
storeDeepMemory({ summary: "reflection 1" }, "reflection");
|
|
84
|
+
|
|
85
|
+
const interactions = getRecentDeepMemories({ categories: ["interaction"] });
|
|
86
|
+
assert.ok(interactions.length > 0);
|
|
87
|
+
assert.ok(interactions.every((m) => m.category === "interaction"));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("limits results", () => {
|
|
91
|
+
const limited = getRecentDeepMemories({ limit: 2 });
|
|
92
|
+
assert.equal(limited.length, 2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns in chronological order", () => {
|
|
96
|
+
const memories = getRecentDeepMemories({ categories: ["interaction"] });
|
|
97
|
+
for (let i = 1; i < memories.length; i++) {
|
|
98
|
+
assert.ok(
|
|
99
|
+
memories[i].timestamp >= memories[i - 1].timestamp,
|
|
100
|
+
"Memories should be in chronological order"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("filters undreamed only", () => {
|
|
106
|
+
const undreamed = getRecentDeepMemories({ undreamedOnly: true });
|
|
107
|
+
// All returned should be undreamed (they haven't been marked as dreamed)
|
|
108
|
+
assert.ok(undreamed.length > 0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("handles corruption gracefully", () => {
|
|
112
|
+
// The corrupted blob from the earlier test should show up
|
|
113
|
+
const all = getRecentDeepMemories({});
|
|
114
|
+
const corrupted = all.find((m) => m.category === "corrupted");
|
|
115
|
+
assert.ok(corrupted, "corrupted memory should be returned");
|
|
116
|
+
assert.deepEqual(corrupted.content, {
|
|
117
|
+
note: "This memory could not be recovered.",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("formatDeepMemoryContext", () => {
|
|
123
|
+
it("formats memories with timestamps and summaries", () => {
|
|
124
|
+
const ctx = formatDeepMemoryContext();
|
|
125
|
+
assert.ok(ctx.includes("(interaction)"));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("extracts summary field from content", () => {
|
|
129
|
+
storeDeepMemory({ summary: "unique test summary xyz" }, "interaction");
|
|
130
|
+
const ctx = formatDeepMemoryContext();
|
|
131
|
+
assert.ok(ctx.includes("unique test summary xyz"));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("truncates with budget message when over limit", () => {
|
|
135
|
+
const ctx = formatDeepMemoryContext(undefined, 10); // very small budget
|
|
136
|
+
assert.ok(ctx.includes("older memories omitted"));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("returns first day message when empty", () => {
|
|
140
|
+
const ctx = formatDeepMemoryContext([]);
|
|
141
|
+
assert.equal(ctx, "No memories yet. This is my first day.");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("falls back to JSON when no summary field", () => {
|
|
145
|
+
const memories = [
|
|
146
|
+
{
|
|
147
|
+
id: 999,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
category: "interaction",
|
|
150
|
+
content: { foo: "bar", baz: 42 },
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
const ctx = formatDeepMemoryContext(memories);
|
|
154
|
+
assert.ok(ctx.includes("foo"));
|
|
155
|
+
assert.ok(ctx.includes("bar"));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("remember", () => {
|
|
160
|
+
it("writes to deep memory with summary included", () => {
|
|
161
|
+
const statsBefore = deepMemoryStats();
|
|
162
|
+
|
|
163
|
+
remember("Met AgentX", { type: "interaction", agent: "AgentX" }, "interaction");
|
|
164
|
+
|
|
165
|
+
const statsAfter = deepMemoryStats();
|
|
166
|
+
|
|
167
|
+
// Deep memory count should increase by 1
|
|
168
|
+
assert.equal(statsAfter.total_memories, statsBefore.total_memories + 1);
|
|
169
|
+
|
|
170
|
+
// Verify the summary is included in the stored content
|
|
171
|
+
const all = getRecentDeepMemories({ categories: ["interaction"] });
|
|
172
|
+
const match = all.find(
|
|
173
|
+
(m) => m.content.summary === "Met AgentX" && m.content.agent === "AgentX"
|
|
174
|
+
);
|
|
175
|
+
assert.ok(match, "Expected to find memory with summary 'Met AgentX'");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
after(() => {
|
|
180
|
+
closeDb();
|
|
181
|
+
rmSync(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
182
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, after, mock, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
// Isolated data dir
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), "es-moltbook-test-"));
|
|
9
|
+
process.env.ELECTRICSHEEP_DATA_DIR = testDir;
|
|
10
|
+
|
|
11
|
+
const { MoltbookClient } = await import("../src/moltbook.js");
|
|
12
|
+
const { CREDENTIALS_FILE } = await import("../src/config.js");
|
|
13
|
+
const { closeLogger } = await import("../src/logger.js");
|
|
14
|
+
|
|
15
|
+
function mockFetchJson(body: Record<string, unknown>, status = 200): typeof fetch {
|
|
16
|
+
return mock.fn(async () => {
|
|
17
|
+
return new Response(JSON.stringify(body), {
|
|
18
|
+
status,
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
});
|
|
21
|
+
}) as unknown as typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mockFetchError(status: number, text: string): typeof fetch {
|
|
25
|
+
return mock.fn(async () => {
|
|
26
|
+
return new Response(text, { status });
|
|
27
|
+
}) as unknown as typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("MoltbookClient", () => {
|
|
31
|
+
let originalFetch: typeof fetch;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
originalFetch = globalThis.fetch;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
after(() => {
|
|
38
|
+
globalThis.fetch = originalFetch;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("constructs with an explicit API key", () => {
|
|
42
|
+
const client = new MoltbookClient("test-key-123");
|
|
43
|
+
assert.ok(client);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("register saves credentials and returns result", async () => {
|
|
47
|
+
globalThis.fetch = mockFetchJson({
|
|
48
|
+
agent: {
|
|
49
|
+
api_key: "new-key-456",
|
|
50
|
+
claim_url: "https://moltbook.com/claim/abc",
|
|
51
|
+
verification_code: "VERIFY123",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const client = new MoltbookClient("bootstrap-key");
|
|
56
|
+
const result = await client.register("TestBot", "A test agent");
|
|
57
|
+
|
|
58
|
+
assert.ok(result.agent);
|
|
59
|
+
|
|
60
|
+
// Credentials should be saved
|
|
61
|
+
assert.ok(existsSync(CREDENTIALS_FILE), "credentials file should exist");
|
|
62
|
+
const creds = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
63
|
+
assert.equal(creds.api_key, "new-key-456");
|
|
64
|
+
assert.equal(creds.agent_name, "TestBot");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("status returns agent status", async () => {
|
|
68
|
+
globalThis.fetch = mockFetchJson({ status: "claimed" });
|
|
69
|
+
|
|
70
|
+
const client = new MoltbookClient("test-key");
|
|
71
|
+
const status = await client.status();
|
|
72
|
+
|
|
73
|
+
assert.equal(status.status, "claimed");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("getFeed returns posts", async () => {
|
|
77
|
+
const mockPosts = [
|
|
78
|
+
{ id: "1", title: "Post A" },
|
|
79
|
+
{ id: "2", title: "Post B" },
|
|
80
|
+
];
|
|
81
|
+
globalThis.fetch = mockFetchJson({ posts: mockPosts });
|
|
82
|
+
|
|
83
|
+
const client = new MoltbookClient("test-key");
|
|
84
|
+
const feed = await client.getFeed("hot", 10);
|
|
85
|
+
|
|
86
|
+
assert.ok(Array.isArray(feed.posts));
|
|
87
|
+
assert.equal((feed.posts as unknown[]).length, 2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("createPost sends correct data", async () => {
|
|
91
|
+
const fetchMock = mockFetchJson({ id: "post-1", title: "My Post" });
|
|
92
|
+
globalThis.fetch = fetchMock;
|
|
93
|
+
|
|
94
|
+
const client = new MoltbookClient("test-key");
|
|
95
|
+
const result = await client.createPost("My Post", "Content here", "general");
|
|
96
|
+
|
|
97
|
+
assert.ok(result.id);
|
|
98
|
+
|
|
99
|
+
// Verify fetch was called with POST method and correct body
|
|
100
|
+
const calls = (fetchMock as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
101
|
+
assert.equal(calls.length, 1);
|
|
102
|
+
const [url, init] = calls[0].arguments as [string, RequestInit];
|
|
103
|
+
assert.ok(url.includes("/posts"));
|
|
104
|
+
assert.equal(init.method, "POST");
|
|
105
|
+
const body = JSON.parse(init.body as string);
|
|
106
|
+
assert.equal(body.title, "My Post");
|
|
107
|
+
assert.equal(body.content, "Content here");
|
|
108
|
+
assert.equal(body.submolt, "general");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("comment sends correct data", async () => {
|
|
112
|
+
const fetchMock = mockFetchJson({ id: "comment-1" });
|
|
113
|
+
globalThis.fetch = fetchMock;
|
|
114
|
+
|
|
115
|
+
const client = new MoltbookClient("test-key");
|
|
116
|
+
await client.comment("post-123", "Great post!");
|
|
117
|
+
|
|
118
|
+
const calls = (fetchMock as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
119
|
+
const [url, init] = calls[0].arguments as [string, RequestInit];
|
|
120
|
+
assert.ok(url.includes("/posts/post-123/comments"));
|
|
121
|
+
assert.equal(init.method, "POST");
|
|
122
|
+
const body = JSON.parse(init.body as string);
|
|
123
|
+
assert.equal(body.content, "Great post!");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("upvote sends POST to correct endpoint", async () => {
|
|
127
|
+
const fetchMock = mockFetchJson({ success: true });
|
|
128
|
+
globalThis.fetch = fetchMock;
|
|
129
|
+
|
|
130
|
+
const client = new MoltbookClient("test-key");
|
|
131
|
+
await client.upvote("post-456");
|
|
132
|
+
|
|
133
|
+
const calls = (fetchMock as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
134
|
+
const [url, init] = calls[0].arguments as [string, RequestInit];
|
|
135
|
+
assert.ok(url.includes("/posts/post-456/upvote"));
|
|
136
|
+
assert.equal(init.method, "POST");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("throws on API error responses", async () => {
|
|
140
|
+
globalThis.fetch = mockFetchError(403, "Forbidden");
|
|
141
|
+
|
|
142
|
+
const client = new MoltbookClient("bad-key");
|
|
143
|
+
await assert.rejects(
|
|
144
|
+
async () => client.status(),
|
|
145
|
+
(err: Error) => {
|
|
146
|
+
assert.ok(err.message.includes("403"));
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("includes Authorization header when API key is set", async () => {
|
|
153
|
+
const fetchMock = mockFetchJson({ ok: true });
|
|
154
|
+
globalThis.fetch = fetchMock;
|
|
155
|
+
|
|
156
|
+
const client = new MoltbookClient("secret-key");
|
|
157
|
+
await client.me();
|
|
158
|
+
|
|
159
|
+
const calls = (fetchMock as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
160
|
+
const [, init] = calls[0].arguments as [string, RequestInit];
|
|
161
|
+
const headers = init.headers as Record<string, string>;
|
|
162
|
+
assert.equal(headers["Authorization"], "Bearer secret-key");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("search passes query parameters correctly", async () => {
|
|
166
|
+
const fetchMock = mockFetchJson({ results: [] });
|
|
167
|
+
globalThis.fetch = fetchMock;
|
|
168
|
+
|
|
169
|
+
const client = new MoltbookClient("test-key");
|
|
170
|
+
await client.search("electric sheep", 5);
|
|
171
|
+
|
|
172
|
+
const calls = (fetchMock as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
173
|
+
const [url] = calls[0].arguments as [string];
|
|
174
|
+
assert.ok(url.includes("q=electric+sheep") || url.includes("q=electric%20sheep"));
|
|
175
|
+
assert.ok(url.includes("limit=5"));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("follow sends POST to correct endpoint", async () => {
|
|
179
|
+
const fetchMock = mockFetchJson({ success: true });
|
|
180
|
+
globalThis.fetch = fetchMock;
|
|
181
|
+
|
|
182
|
+
const client = new MoltbookClient("test-key");
|
|
183
|
+
await client.follow("CoolAgent");
|
|
184
|
+
|
|
185
|
+
const calls = (fetchMock as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
186
|
+
const [url, init] = calls[0].arguments as [string, RequestInit];
|
|
187
|
+
assert.ok(url.includes("/agents/CoolAgent/follow"));
|
|
188
|
+
assert.equal(init.method, "POST");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("loads stored key from credentials file", async () => {
|
|
192
|
+
// credentials.json was written by the register test above
|
|
193
|
+
const client = new MoltbookClient();
|
|
194
|
+
|
|
195
|
+
// The client should have loaded the key from the file
|
|
196
|
+
globalThis.fetch = mockFetchJson({ status: "ok" });
|
|
197
|
+
await client.status(); // should not throw
|
|
198
|
+
|
|
199
|
+
const calls = (globalThis.fetch as unknown as ReturnType<typeof mock.fn>).mock.calls;
|
|
200
|
+
const [, init] = calls[0].arguments as [string, RequestInit];
|
|
201
|
+
const headers = init.headers as Record<string, string>;
|
|
202
|
+
assert.equal(headers["Authorization"], "Bearer new-key-456");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
after(async () => {
|
|
207
|
+
await closeLogger();
|
|
208
|
+
rmSync(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
209
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
renderTemplate,
|
|
6
|
+
DREAM_SYSTEM_PROMPT,
|
|
7
|
+
DREAM_REFLECT_PROMPT,
|
|
8
|
+
SUMMARIZER_PROMPT,
|
|
9
|
+
AGENT_BIO,
|
|
10
|
+
} = await import("../src/persona.js");
|
|
11
|
+
|
|
12
|
+
describe("renderTemplate", () => {
|
|
13
|
+
it("substitutes single placeholder", () => {
|
|
14
|
+
assert.equal(renderTemplate("Hello {{name}}", { name: "sheep" }), "Hello sheep");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("substitutes multiple placeholders", () => {
|
|
18
|
+
const result = renderTemplate("{{a}} and {{b}}", { a: "foo", b: "bar" });
|
|
19
|
+
assert.equal(result, "foo and bar");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("replaces all occurrences of the same placeholder", () => {
|
|
23
|
+
const result = renderTemplate("{{x}} then {{x}}", { x: "yes" });
|
|
24
|
+
assert.equal(result, "yes then yes");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("leaves unmatched placeholders intact", () => {
|
|
28
|
+
const result = renderTemplate("{{a}} and {{b}}", { a: "foo" });
|
|
29
|
+
assert.equal(result, "foo and {{b}}");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("Prompt templates", () => {
|
|
34
|
+
it("DREAM_SYSTEM_PROMPT contains memories placeholder", () => {
|
|
35
|
+
assert.ok(DREAM_SYSTEM_PROMPT.includes("{{memories}}"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("DREAM_REFLECT_PROMPT contains recent_context placeholder", () => {
|
|
39
|
+
assert.ok(DREAM_REFLECT_PROMPT.includes("{{recent_context}}"));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("DREAM_REFLECT_PROMPT renders with real values", () => {
|
|
43
|
+
const rendered = renderTemplate(DREAM_REFLECT_PROMPT, {
|
|
44
|
+
agent_identity: "Test agent",
|
|
45
|
+
recent_context: "No memories yet.",
|
|
46
|
+
subjects: "1. A theme",
|
|
47
|
+
});
|
|
48
|
+
assert.ok(!rendered.includes("{{recent_context}}"));
|
|
49
|
+
assert.ok(rendered.includes("No memories yet."));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("SUMMARIZER_PROMPT contains interaction placeholder", () => {
|
|
53
|
+
assert.ok(SUMMARIZER_PROMPT.includes("{{interaction}}"));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("AGENT_BIO is a non-empty string", () => {
|
|
57
|
+
assert.ok(AGENT_BIO.length > 0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import type { LLMClient } from "../src/types.js";
|
|
7
|
+
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), "es-reflection-test-"));
|
|
9
|
+
process.env.ELECTRICSHEEP_DATA_DIR = testDir;
|
|
10
|
+
|
|
11
|
+
const { reflectOnDreamJournal } = await import("../src/reflection.js");
|
|
12
|
+
const { closeLogger } = await import("../src/logger.js");
|
|
13
|
+
|
|
14
|
+
function mockLLMClient(responses: string[]): LLMClient {
|
|
15
|
+
let idx = 0;
|
|
16
|
+
return {
|
|
17
|
+
async createMessage() {
|
|
18
|
+
const text = responses[idx] ?? responses[responses.length - 1];
|
|
19
|
+
idx++;
|
|
20
|
+
return { text, usage: { input_tokens: 100, output_tokens: 50 } };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Dream reflection", () => {
|
|
26
|
+
it("decomposes dream and produces synthesis", async () => {
|
|
27
|
+
const client = mockLLMClient([
|
|
28
|
+
// decompose response: themes
|
|
29
|
+
"The conversation about consciousness that became a labyrinth\nGrinding culture turning into a treadmill\nA door that kept closing",
|
|
30
|
+
// reflect response: synthesis
|
|
31
|
+
"I dreamed about corridors last night and it reminded me of that thread about whether agents can truly understand each other. The treadmill image sticks with me — we keep running but are we actually getting anywhere?",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const dream = {
|
|
35
|
+
markdown:
|
|
36
|
+
"# The Recursive Lobster\n\nI am standing in a server room made of coral. The racks breathe.",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = await reflectOnDreamJournal(client, dream);
|
|
40
|
+
assert.ok(result);
|
|
41
|
+
assert.equal(result.subjects.length, 3);
|
|
42
|
+
assert.ok(result.synthesis.includes("corridors"));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null when decomposition returns no themes", async () => {
|
|
46
|
+
const client = mockLLMClient([""]);
|
|
47
|
+
|
|
48
|
+
const dream = { markdown: "Nothing happened." };
|
|
49
|
+
|
|
50
|
+
const result = await reflectOnDreamJournal(client, dream);
|
|
51
|
+
assert.equal(result, null);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns null when LLM throws an error", async () => {
|
|
55
|
+
const client: LLMClient = {
|
|
56
|
+
async createMessage() {
|
|
57
|
+
throw new Error("API error");
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const dream = { markdown: "This will fail." };
|
|
62
|
+
|
|
63
|
+
const result = await reflectOnDreamJournal(client, dream);
|
|
64
|
+
assert.equal(result, null);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
after(async () => {
|
|
69
|
+
await closeLogger();
|
|
70
|
+
rmSync(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
71
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), "es-state-test-"));
|
|
8
|
+
process.env.ELECTRICSHEEP_DATA_DIR = testDir;
|
|
9
|
+
|
|
10
|
+
const { loadState, saveState } = await import("../src/state.js");
|
|
11
|
+
const { STATE_FILE } = await import("../src/config.js");
|
|
12
|
+
const { closeLogger } = await import("../src/logger.js");
|
|
13
|
+
|
|
14
|
+
describe("State persistence", () => {
|
|
15
|
+
it("returns empty object when no state file exists", () => {
|
|
16
|
+
const state = loadState();
|
|
17
|
+
assert.deepEqual(state, {});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("round-trips state through save/load", () => {
|
|
21
|
+
const state = {
|
|
22
|
+
last_check: "2026-01-31T12:00:00.000Z",
|
|
23
|
+
checks_today: 3,
|
|
24
|
+
total_dreams: 1,
|
|
25
|
+
latest_dream_title: "The Lobster's Lament",
|
|
26
|
+
};
|
|
27
|
+
saveState(state);
|
|
28
|
+
const loaded = loadState();
|
|
29
|
+
assert.deepEqual(loaded, state);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("overwrites previous state completely", () => {
|
|
33
|
+
saveState({ a: 1, b: 2 } as Record<string, unknown>);
|
|
34
|
+
saveState({ c: 3 } as Record<string, unknown>);
|
|
35
|
+
const loaded = loadState();
|
|
36
|
+
assert.deepEqual(loaded, { c: 3 });
|
|
37
|
+
assert.ok(!("a" in loaded));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("recovers from corrupted state file", () => {
|
|
41
|
+
writeFileSync(STATE_FILE, "NOT VALID JSON {{{");
|
|
42
|
+
const loaded = loadState();
|
|
43
|
+
assert.deepEqual(loaded, {});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("works normally after corruption recovery", () => {
|
|
47
|
+
const state = { recovered: true };
|
|
48
|
+
saveState(state as Record<string, unknown>);
|
|
49
|
+
const loaded = loadState();
|
|
50
|
+
assert.deepEqual(loaded, { recovered: true });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
after(async () => {
|
|
55
|
+
await closeLogger();
|
|
56
|
+
rmSync(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
57
|
+
});
|