@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,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
const root = new URL("../", import.meta.url);
|
|
5
|
+
|
|
6
|
+
function readRootFile(name: string): string {
|
|
7
|
+
return fs.readFileSync(new URL(name, root), "utf8");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("OpenClaw packaging scripts", () => {
|
|
11
|
+
it("packages the npm tarball and uploads it to the R2 openclaw prefix", () => {
|
|
12
|
+
const script = readRootFile("scripts/package_openclaw_plugin.sh");
|
|
13
|
+
|
|
14
|
+
expect(script).toMatch(/npm pack --silent --pack-destination "\$SCRIPT_DIR"/);
|
|
15
|
+
expect(script).toContain('REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"');
|
|
16
|
+
expect(script).toContain('R2_ENV_FILE="${R2_ENV_FILE:-$SCRIPT_DIR/.env.r2}"');
|
|
17
|
+
expect(script).toContain('set -a; source "$R2_ENV_FILE"; set +a');
|
|
18
|
+
expect(script).toContain(': "${AWS_ACCESS_KEY_ID:?missing in $R2_ENV_FILE}"');
|
|
19
|
+
expect(script).toContain(': "${AWS_SECRET_ACCESS_KEY:?missing in $R2_ENV_FILE}"');
|
|
20
|
+
expect(script).toContain(': "${R2_ENDPOINT:?missing in $R2_ENV_FILE}"');
|
|
21
|
+
expect(script).toContain(': "${R2_BUCKET:?missing in $R2_ENV_FILE}"');
|
|
22
|
+
expect(script).not.toContain("f8eeaffb6f81ffd82b59c24d4ff797c9");
|
|
23
|
+
expect(script).not.toContain("587f8d4aa485a70f6e984a9efa7e609a86e4ef02d56a6682d9d7a9509913b8cb");
|
|
24
|
+
expect(script).toContain("OBJECT_KEY=\"openclaw/${TARGET_NAME}\"");
|
|
25
|
+
expect(script).toContain("LATEST_OBJECT_KEY=\"openclaw/newbase-clawchat-clawchat-plugin-openclaw-latest.tgz\"");
|
|
26
|
+
expect(script).toContain("https://plugin.clawling.chat/${OBJECT_KEY}");
|
|
27
|
+
expect(script).toContain("https://plugin.clawling.chat/${LATEST_OBJECT_KEY}");
|
|
28
|
+
expect(script).toMatch(/aws s3 cp[\s\S]*--content-type application\/gzip/);
|
|
29
|
+
expect(script).toMatch(/aws s3 cp[\s\S]*"s3:\/\/\$\{R2_BUCKET\}\/\$\{LATEST_OBJECT_KEY\}"/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps the local R2 env file out of git", () => {
|
|
33
|
+
const gitignore = readRootFile(".gitignore");
|
|
34
|
+
|
|
35
|
+
expect(gitignore).toMatch(/^\.env\.r2$/m);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("keeps generated npm package tarballs out of git", () => {
|
|
39
|
+
const gitignore = readRootFile(".gitignore");
|
|
40
|
+
|
|
41
|
+
expect(gitignore).toMatch(/^\*\.tgz$/m);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("publishes a local R2 env example without real credentials", () => {
|
|
45
|
+
const example = readRootFile("scripts/.env.r2.example");
|
|
46
|
+
|
|
47
|
+
expect(example).toContain("AWS_ACCESS_KEY_ID=replace-me");
|
|
48
|
+
expect(example).toContain("AWS_SECRET_ACCESS_KEY=replace-me");
|
|
49
|
+
expect(example).toContain("AWS_DEFAULT_REGION=auto");
|
|
50
|
+
expect(example).toContain("R2_ENDPOINT=https://a47044dea9e3fc23ed4a68da66ab005a.r2.cloudflarestorage.com");
|
|
51
|
+
expect(example).toContain("R2_BUCKET=clawchat-test");
|
|
52
|
+
expect(example).not.toContain("f8eeaffb6f81ffd82b59c24d4ff797c9");
|
|
53
|
+
expect(example).not.toContain("587f8d4aa485a70f6e984a9efa7e609a86e4ef02d56a6682d9d7a9509913b8cb");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("downloads the R2 tarball and installs it through openclaw", () => {
|
|
57
|
+
const script = readRootFile("scripts/install_openclaw.sh");
|
|
58
|
+
|
|
59
|
+
expect(script).toContain("PUBLIC_BASE_URL=\"https://plugin.clawling.chat\"");
|
|
60
|
+
expect(script).toContain("PACKAGE_BASENAME=\"newbase-clawchat-clawchat-plugin-openclaw\"");
|
|
61
|
+
expect(script).toContain("DEFAULT_TGZ=\"${PACKAGE_BASENAME}-latest.tgz\"");
|
|
62
|
+
expect(script).toContain("CLAWCHAT_PLUGIN_REF=\"${1:-latest}\"");
|
|
63
|
+
expect(script).toContain('case "$CLAWCHAT_PLUGIN_REF" in');
|
|
64
|
+
expect(script).toContain("CLAWCHAT_PLUGIN_URL=\"${PUBLIC_BASE_URL}/openclaw/${PACKAGE_BASENAME}-${CLAWCHAT_PLUGIN_REF}.tgz\"");
|
|
65
|
+
expect(script).not.toContain("require(\"./package.json\")");
|
|
66
|
+
expect(script).not.toContain("command -v node");
|
|
67
|
+
expect(script).toContain("curl -fL \"$CLAWCHAT_PLUGIN_URL\" -o \"$CLAWCHAT_PLUGIN_TGZ\"");
|
|
68
|
+
expect(script).toContain('cleanup_plugin_tgz()');
|
|
69
|
+
expect(script).toContain('trap cleanup_plugin_tgz EXIT');
|
|
70
|
+
expect(script).toContain('rm -f "$CLAWCHAT_PLUGIN_TGZ"');
|
|
71
|
+
expect(script).toContain("openclaw plugins install \"$CLAWCHAT_PLUGIN_TGZ\" --force");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("publishes a version-pinned OpenClaw installer script", () => {
|
|
75
|
+
const script = readRootFile("scripts/install_openclaw-2026.5.4-3.sh");
|
|
76
|
+
|
|
77
|
+
expect(script).toContain("PUBLIC_BASE_URL=\"https://plugin.clawling.chat\"");
|
|
78
|
+
expect(script).toContain('CLAWCHAT_PLUGIN_REF="2026.5.4-3"');
|
|
79
|
+
expect(script).toContain("newbase-clawchat-clawchat-plugin-openclaw-2026.5.4-3.tgz");
|
|
80
|
+
expect(script).not.toContain('CLAWCHAT_PLUGIN_REF="${1:-latest}"');
|
|
81
|
+
expect(script).toContain("curl -fL \"$CLAWCHAT_PLUGIN_URL\" -o \"$CLAWCHAT_PLUGIN_TGZ\"");
|
|
82
|
+
expect(script).toContain('trap cleanup_plugin_tgz EXIT');
|
|
83
|
+
expect(script).toContain("openclaw plugins install \"$CLAWCHAT_PLUGIN_TGZ\" --force");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { createClawChatStore, getClawChatStore, resetClawChatStoreForTest } from "./storage.ts";
|
|
7
|
+
|
|
8
|
+
const tempRoots: string[] = [];
|
|
9
|
+
|
|
10
|
+
function tempDbPath(): string {
|
|
11
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawchat-plugin-openclaw-storage-"));
|
|
12
|
+
tempRoots.push(dir);
|
|
13
|
+
return path.join(dir, "clawchat.sqlite");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readSqliteRows(dbPath: string, sql: string): Record<string, unknown>[] {
|
|
17
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
18
|
+
try {
|
|
19
|
+
return db.prepare(sql).all() as Record<string, unknown>[];
|
|
20
|
+
} finally {
|
|
21
|
+
db.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
resetClawChatStoreForTest();
|
|
28
|
+
for (const dir of tempRoots.splice(0)) {
|
|
29
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("clawchat sqlite storage", () => {
|
|
34
|
+
it("migrates connection metadata", () => {
|
|
35
|
+
const dbPath = tempDbPath();
|
|
36
|
+
const store = createClawChatStore({ dbPath });
|
|
37
|
+
|
|
38
|
+
store.initialize();
|
|
39
|
+
store.close();
|
|
40
|
+
|
|
41
|
+
expect(readSqliteRows(dbPath, "PRAGMA table_info(connections)").map((row) => row.name)).toEqual(
|
|
42
|
+
expect.arrayContaining(["resolved_device_id", "delivery_mode"]),
|
|
43
|
+
);
|
|
44
|
+
expect(
|
|
45
|
+
readSqliteRows(
|
|
46
|
+
dbPath,
|
|
47
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('clawchat_profiles', 'clawchat_user_profiles', 'clawchat_group_profiles') ORDER BY name",
|
|
48
|
+
),
|
|
49
|
+
).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not expose legacy profile helper methods", () => {
|
|
53
|
+
const store = createClawChatStore({ dbPath: tempDbPath() });
|
|
54
|
+
|
|
55
|
+
expect("upsertProfile" in store).toBe(false);
|
|
56
|
+
expect("upsertMinimalProfile" in store).toBe(false);
|
|
57
|
+
expect("upsertAgentProfile" in store).toBe(false);
|
|
58
|
+
expect("upsertGroupProfile" in store).toBe(false);
|
|
59
|
+
expect("upsertUserProfile" in store).toBe(false);
|
|
60
|
+
expect("profileExists" in store).toBe(false);
|
|
61
|
+
expect("getProfile" in store).toBe(false);
|
|
62
|
+
|
|
63
|
+
store.close();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("preserves activation tokens when migrating bootstrap columns", () => {
|
|
67
|
+
const dbPath = tempDbPath();
|
|
68
|
+
const db = new DatabaseSync(dbPath);
|
|
69
|
+
try {
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE schema_migrations (
|
|
72
|
+
version INTEGER PRIMARY KEY,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
applied_at INTEGER NOT NULL
|
|
75
|
+
);
|
|
76
|
+
CREATE TABLE activations (
|
|
77
|
+
platform TEXT NOT NULL,
|
|
78
|
+
account_id TEXT NOT NULL,
|
|
79
|
+
user_id TEXT,
|
|
80
|
+
access_token TEXT,
|
|
81
|
+
refresh_token TEXT,
|
|
82
|
+
activated_at INTEGER NOT NULL,
|
|
83
|
+
login_method TEXT,
|
|
84
|
+
updated_at INTEGER NOT NULL,
|
|
85
|
+
PRIMARY KEY (platform, account_id)
|
|
86
|
+
);
|
|
87
|
+
CREATE TABLE clawchat_messages (
|
|
88
|
+
id INTEGER PRIMARY KEY,
|
|
89
|
+
platform TEXT NOT NULL,
|
|
90
|
+
account_id TEXT NOT NULL,
|
|
91
|
+
kind TEXT NOT NULL,
|
|
92
|
+
direction TEXT NOT NULL,
|
|
93
|
+
event_type TEXT NOT NULL,
|
|
94
|
+
trace_id TEXT,
|
|
95
|
+
chat_id TEXT,
|
|
96
|
+
message_id TEXT,
|
|
97
|
+
text TEXT,
|
|
98
|
+
raw_json TEXT,
|
|
99
|
+
created_at INTEGER NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE TABLE connections (
|
|
102
|
+
id INTEGER PRIMARY KEY,
|
|
103
|
+
platform TEXT NOT NULL,
|
|
104
|
+
account_id TEXT NOT NULL,
|
|
105
|
+
attempt INTEGER,
|
|
106
|
+
reconnect_count INTEGER,
|
|
107
|
+
state TEXT NOT NULL,
|
|
108
|
+
connect_started_at INTEGER,
|
|
109
|
+
connect_sent_at INTEGER,
|
|
110
|
+
ready_at INTEGER,
|
|
111
|
+
disconnected_at INTEGER,
|
|
112
|
+
close_code INTEGER,
|
|
113
|
+
close_reason TEXT,
|
|
114
|
+
error TEXT,
|
|
115
|
+
created_at INTEGER NOT NULL,
|
|
116
|
+
updated_at INTEGER NOT NULL
|
|
117
|
+
);
|
|
118
|
+
CREATE TABLE tool_calls (
|
|
119
|
+
id INTEGER PRIMARY KEY,
|
|
120
|
+
platform TEXT NOT NULL,
|
|
121
|
+
account_id TEXT,
|
|
122
|
+
tool_name TEXT NOT NULL,
|
|
123
|
+
args_json TEXT,
|
|
124
|
+
result_json TEXT,
|
|
125
|
+
error TEXT,
|
|
126
|
+
started_at INTEGER NOT NULL,
|
|
127
|
+
ended_at INTEGER,
|
|
128
|
+
duration_ms INTEGER,
|
|
129
|
+
created_at INTEGER NOT NULL
|
|
130
|
+
);
|
|
131
|
+
INSERT INTO schema_migrations(version, name, applied_at)
|
|
132
|
+
VALUES (1, 'initial_schema', 1), (2, 'message_idempotency', 2);
|
|
133
|
+
INSERT INTO activations(
|
|
134
|
+
platform, account_id, user_id, access_token, refresh_token,
|
|
135
|
+
activated_at, login_method, updated_at
|
|
136
|
+
) VALUES (
|
|
137
|
+
'openclaw', 'default', 'user-old', 'access-old', 'refresh-old',
|
|
138
|
+
1000, 'login', 1000
|
|
139
|
+
);
|
|
140
|
+
`);
|
|
141
|
+
} finally {
|
|
142
|
+
db.close();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const store = createClawChatStore({ dbPath });
|
|
146
|
+
store.initialize();
|
|
147
|
+
|
|
148
|
+
expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
|
|
149
|
+
access_token: "access-old",
|
|
150
|
+
refresh_token: "refresh-old",
|
|
151
|
+
conversation_id: null,
|
|
152
|
+
bootstrap_sent: 1,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("initializes schema and records activation/message/connection/tool rows", () => {
|
|
157
|
+
const store = createClawChatStore({ dbPath: tempDbPath() });
|
|
158
|
+
|
|
159
|
+
expect(store.listAppliedMigrations()).toEqual([
|
|
160
|
+
{ version: 1, name: "initial_schema" },
|
|
161
|
+
{ version: 2, name: "message_idempotency" },
|
|
162
|
+
{ version: 3, name: "activation_bootstrap" },
|
|
163
|
+
{ version: 4, name: "activation_owner_user_id" },
|
|
164
|
+
{ version: 5, name: "connection_metadata" },
|
|
165
|
+
{ version: 6, name: "message_ack_status" },
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
store.upsertActivation({
|
|
169
|
+
platform: "openclaw",
|
|
170
|
+
accountId: "default",
|
|
171
|
+
userId: "user-old",
|
|
172
|
+
ownerUserId: "owner-old",
|
|
173
|
+
accessToken: "token-old",
|
|
174
|
+
refreshToken: "refresh-old",
|
|
175
|
+
activatedAt: 1000,
|
|
176
|
+
loginMethod: "login",
|
|
177
|
+
});
|
|
178
|
+
store.upsertActivation({
|
|
179
|
+
platform: "openclaw",
|
|
180
|
+
accountId: "default",
|
|
181
|
+
userId: " user-new ",
|
|
182
|
+
ownerUserId: " owner-new ",
|
|
183
|
+
accessToken: "token-new",
|
|
184
|
+
refreshToken: null,
|
|
185
|
+
activatedAt: 2000,
|
|
186
|
+
loginMethod: "login",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
|
|
190
|
+
user_id: "user-new",
|
|
191
|
+
owner_user_id: "owner-new",
|
|
192
|
+
access_token: "token-new",
|
|
193
|
+
refresh_token: null,
|
|
194
|
+
activated_at: 2000,
|
|
195
|
+
updated_at: 2000,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
store.insertMessage({
|
|
199
|
+
platform: "openclaw",
|
|
200
|
+
accountId: "default",
|
|
201
|
+
kind: "message",
|
|
202
|
+
direction: "outbound",
|
|
203
|
+
eventType: "message.send",
|
|
204
|
+
traceId: "trace-1",
|
|
205
|
+
chatId: "chat-1",
|
|
206
|
+
messageId: "msg-1",
|
|
207
|
+
text: "hello",
|
|
208
|
+
raw: { ok: true },
|
|
209
|
+
createdAt: 3000,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(store.listMessagesForTest()).toMatchObject([
|
|
213
|
+
{ kind: "message", direction: "outbound", message_id: "msg-1", text: "hello", send_status: "pending" },
|
|
214
|
+
]);
|
|
215
|
+
expect(
|
|
216
|
+
store.markMessageAcknowledged({
|
|
217
|
+
accountId: "default",
|
|
218
|
+
kind: "message",
|
|
219
|
+
direction: "outbound",
|
|
220
|
+
messageId: "msg-1",
|
|
221
|
+
protocolMessageId: "srv-msg-1",
|
|
222
|
+
ackedAt: 3100,
|
|
223
|
+
}),
|
|
224
|
+
).toBe(true);
|
|
225
|
+
expect(store.listMessagesForTest()).toMatchObject([
|
|
226
|
+
{
|
|
227
|
+
kind: "message",
|
|
228
|
+
direction: "outbound",
|
|
229
|
+
message_id: "msg-1",
|
|
230
|
+
send_status: "acknowledged",
|
|
231
|
+
protocol_message_id: "srv-msg-1",
|
|
232
|
+
acked_at: 3100,
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const connectionId = store.startConnection({
|
|
237
|
+
platform: "openclaw",
|
|
238
|
+
accountId: "default",
|
|
239
|
+
attempt: 1,
|
|
240
|
+
reconnectCount: 0,
|
|
241
|
+
connectStartedAt: 1000,
|
|
242
|
+
});
|
|
243
|
+
expect(connectionId).toEqual(expect.any(Number));
|
|
244
|
+
store.markConnectSent(connectionId, { at: 1100 });
|
|
245
|
+
store.markConnectionReady(connectionId, {
|
|
246
|
+
at: 1200,
|
|
247
|
+
resolvedDeviceId: "device-resolved",
|
|
248
|
+
deliveryMode: "realtime",
|
|
249
|
+
});
|
|
250
|
+
store.finishConnection(connectionId, { state: "disconnected", disconnectedAt: 1300 });
|
|
251
|
+
|
|
252
|
+
expect(store.listConnectionsForTest()).toMatchObject([
|
|
253
|
+
{
|
|
254
|
+
state: "disconnected",
|
|
255
|
+
connect_started_at: 1000,
|
|
256
|
+
connect_sent_at: 1100,
|
|
257
|
+
ready_at: 1200,
|
|
258
|
+
disconnected_at: 1300,
|
|
259
|
+
resolved_device_id: "device-resolved",
|
|
260
|
+
delivery_mode: "realtime",
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
store.recordToolCall({
|
|
265
|
+
platform: "openclaw",
|
|
266
|
+
accountId: "default",
|
|
267
|
+
toolName: "clawchat_get_account_profile",
|
|
268
|
+
args: { include: "profile" },
|
|
269
|
+
result: { id: "user-new" },
|
|
270
|
+
error: null,
|
|
271
|
+
startedAt: 4000,
|
|
272
|
+
endedAt: 4250,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(store.listToolCallsForTest()).toMatchObject([
|
|
276
|
+
{ tool_name: "clawchat_get_account_profile", duration_ms: 250, error: null },
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
store.close();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("preserves connection ready metadata when omitted on later ready updates", () => {
|
|
283
|
+
const store = createClawChatStore({ dbPath: tempDbPath() });
|
|
284
|
+
const connectionId = store.startConnection({
|
|
285
|
+
platform: "openclaw",
|
|
286
|
+
accountId: "default",
|
|
287
|
+
connectStartedAt: 1000,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
store.markConnectionReady(connectionId, {
|
|
291
|
+
at: 1100,
|
|
292
|
+
resolvedDeviceId: "device-one",
|
|
293
|
+
deliveryMode: "realtime",
|
|
294
|
+
});
|
|
295
|
+
store.markConnectionReady(connectionId, { at: 1200 });
|
|
296
|
+
|
|
297
|
+
expect(store.listConnectionsForTest()).toMatchObject([
|
|
298
|
+
{
|
|
299
|
+
ready_at: 1200,
|
|
300
|
+
resolved_device_id: "device-one",
|
|
301
|
+
delivery_mode: "realtime",
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
store.close();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("atomically claims actual messages by account direction and message_id", () => {
|
|
309
|
+
const store = createClawChatStore({ dbPath: tempDbPath() });
|
|
310
|
+
|
|
311
|
+
expect(store.listAppliedMigrations()).toEqual([
|
|
312
|
+
{ version: 1, name: "initial_schema" },
|
|
313
|
+
{ version: 2, name: "message_idempotency" },
|
|
314
|
+
{ version: 3, name: "activation_bootstrap" },
|
|
315
|
+
{ version: 4, name: "activation_owner_user_id" },
|
|
316
|
+
{ version: 5, name: "connection_metadata" },
|
|
317
|
+
{ version: 6, name: "message_ack_status" },
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
const first = store.claimMessageOnce({
|
|
321
|
+
platform: "openclaw",
|
|
322
|
+
accountId: "default",
|
|
323
|
+
kind: "message",
|
|
324
|
+
direction: "inbound",
|
|
325
|
+
eventType: "message.send",
|
|
326
|
+
traceId: "trace-1",
|
|
327
|
+
chatId: "chat-1",
|
|
328
|
+
messageId: "msg-unique",
|
|
329
|
+
text: "first",
|
|
330
|
+
raw: { ok: true },
|
|
331
|
+
createdAt: 3000,
|
|
332
|
+
});
|
|
333
|
+
const duplicate = store.claimMessageOnce({
|
|
334
|
+
platform: "openclaw",
|
|
335
|
+
accountId: "default",
|
|
336
|
+
kind: "message",
|
|
337
|
+
direction: "inbound",
|
|
338
|
+
eventType: "message.send",
|
|
339
|
+
traceId: "trace-2",
|
|
340
|
+
chatId: "chat-1",
|
|
341
|
+
messageId: "msg-unique",
|
|
342
|
+
text: "duplicate",
|
|
343
|
+
raw: { ok: true },
|
|
344
|
+
createdAt: 4000,
|
|
345
|
+
});
|
|
346
|
+
const outboundSameId = store.claimMessageOnce({
|
|
347
|
+
platform: "openclaw",
|
|
348
|
+
accountId: "default",
|
|
349
|
+
kind: "message",
|
|
350
|
+
direction: "outbound",
|
|
351
|
+
eventType: "message.reply",
|
|
352
|
+
traceId: "trace-outbound",
|
|
353
|
+
chatId: "chat-1",
|
|
354
|
+
messageId: "msg-unique",
|
|
355
|
+
text: "outbound",
|
|
356
|
+
raw: { ok: true },
|
|
357
|
+
createdAt: 4500,
|
|
358
|
+
});
|
|
359
|
+
const thinking = store.insertMessage({
|
|
360
|
+
platform: "openclaw",
|
|
361
|
+
accountId: "default",
|
|
362
|
+
kind: "thinking",
|
|
363
|
+
direction: "outbound",
|
|
364
|
+
eventType: "message.send",
|
|
365
|
+
traceId: "trace-thinking",
|
|
366
|
+
chatId: "chat-1",
|
|
367
|
+
messageId: "msg-unique",
|
|
368
|
+
text: "reasoning",
|
|
369
|
+
raw: { ok: true },
|
|
370
|
+
createdAt: 5000,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(first).toBe(true);
|
|
374
|
+
expect(duplicate).toBe(false);
|
|
375
|
+
expect(outboundSameId).toBe(true);
|
|
376
|
+
expect(thinking).toBe(true);
|
|
377
|
+
expect(store.listMessagesForTest()).toMatchObject([
|
|
378
|
+
{ kind: "message", direction: "inbound", message_id: "msg-unique", text: "first" },
|
|
379
|
+
{ kind: "message", direction: "outbound", message_id: "msg-unique", text: "outbound" },
|
|
380
|
+
{ kind: "thinking", message_id: "msg-unique", text: "reasoning" },
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
store.close();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("recreates the singleton when a later caller provides a different database path", () => {
|
|
387
|
+
const firstPath = tempDbPath();
|
|
388
|
+
const secondPath = tempDbPath();
|
|
389
|
+
|
|
390
|
+
const first = getClawChatStore({ dbPath: firstPath });
|
|
391
|
+
const second = getClawChatStore({ dbPath: secondPath });
|
|
392
|
+
|
|
393
|
+
expect(second.dbPath).toBe(secondPath);
|
|
394
|
+
expect(second).not.toBe(first);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("stores pending activation bootstrap conversation and marks it sent conditionally", () => {
|
|
398
|
+
const store = createClawChatStore({ dbPath: tempDbPath() });
|
|
399
|
+
const bootstrapStore = store as typeof store & {
|
|
400
|
+
claimPendingActivationBootstrap(input: {
|
|
401
|
+
platform: string;
|
|
402
|
+
accountId: string;
|
|
403
|
+
}): { conversationId: string } | null;
|
|
404
|
+
markActivationBootstrapSent(input: {
|
|
405
|
+
platform: string;
|
|
406
|
+
accountId: string;
|
|
407
|
+
conversationId: string;
|
|
408
|
+
}): boolean | null;
|
|
409
|
+
releaseActivationBootstrapClaim(input: {
|
|
410
|
+
platform: string;
|
|
411
|
+
accountId: string;
|
|
412
|
+
conversationId: string;
|
|
413
|
+
}): boolean | null;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
store.upsertActivation({
|
|
417
|
+
platform: "openclaw",
|
|
418
|
+
accountId: "default",
|
|
419
|
+
userId: "user-old",
|
|
420
|
+
accessToken: "token-old",
|
|
421
|
+
refreshToken: "refresh-old",
|
|
422
|
+
conversationId: "conv-old",
|
|
423
|
+
activatedAt: 1000,
|
|
424
|
+
loginMethod: "login",
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
|
|
428
|
+
conversation_id: "conv-old",
|
|
429
|
+
bootstrap_sent: 0,
|
|
430
|
+
});
|
|
431
|
+
expect(store.getActivationConversation({ platform: "openclaw", accountId: "default" })).toMatchObject({
|
|
432
|
+
conversationId: "conv-old",
|
|
433
|
+
conversationType: null,
|
|
434
|
+
});
|
|
435
|
+
expect(
|
|
436
|
+
bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
|
|
437
|
+
).toEqual({
|
|
438
|
+
conversationId: "conv-old",
|
|
439
|
+
});
|
|
440
|
+
expect(
|
|
441
|
+
bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
|
|
442
|
+
).toBeNull();
|
|
443
|
+
expect(
|
|
444
|
+
bootstrapStore.releaseActivationBootstrapClaim({
|
|
445
|
+
platform: "openclaw",
|
|
446
|
+
accountId: "default",
|
|
447
|
+
conversationId: "conv-other",
|
|
448
|
+
}),
|
|
449
|
+
).toBe(false);
|
|
450
|
+
expect(
|
|
451
|
+
bootstrapStore.releaseActivationBootstrapClaim({
|
|
452
|
+
platform: "openclaw",
|
|
453
|
+
accountId: "default",
|
|
454
|
+
conversationId: "conv-old",
|
|
455
|
+
}),
|
|
456
|
+
).toBe(true);
|
|
457
|
+
expect(
|
|
458
|
+
bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
|
|
459
|
+
).toEqual({
|
|
460
|
+
conversationId: "conv-old",
|
|
461
|
+
});
|
|
462
|
+
expect(
|
|
463
|
+
bootstrapStore.markActivationBootstrapSent({
|
|
464
|
+
platform: "openclaw",
|
|
465
|
+
accountId: "default",
|
|
466
|
+
conversationId: "conv-other",
|
|
467
|
+
}),
|
|
468
|
+
).toBe(false);
|
|
469
|
+
expect(
|
|
470
|
+
bootstrapStore.markActivationBootstrapSent({
|
|
471
|
+
platform: "openclaw",
|
|
472
|
+
accountId: "default",
|
|
473
|
+
conversationId: "conv-old",
|
|
474
|
+
}),
|
|
475
|
+
).toBe(true);
|
|
476
|
+
expect(
|
|
477
|
+
bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
|
|
478
|
+
).toBeNull();
|
|
479
|
+
expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
|
|
480
|
+
conversation_id: "conv-old",
|
|
481
|
+
bootstrap_sent: 1,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
store.upsertActivation({
|
|
485
|
+
platform: "openclaw",
|
|
486
|
+
accountId: "default",
|
|
487
|
+
userId: "user-new",
|
|
488
|
+
accessToken: "token-new",
|
|
489
|
+
refreshToken: null,
|
|
490
|
+
conversationId: "conv-new",
|
|
491
|
+
activatedAt: 2000,
|
|
492
|
+
loginMethod: "login",
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(store.getActivationForTest("openclaw", "default")).toMatchObject({
|
|
496
|
+
conversation_id: "conv-new",
|
|
497
|
+
bootstrap_sent: 0,
|
|
498
|
+
updated_at: 2000,
|
|
499
|
+
});
|
|
500
|
+
expect(store.getActivationConversation({ platform: "openclaw", accountId: "default" })).toMatchObject({
|
|
501
|
+
conversationId: "conv-new",
|
|
502
|
+
});
|
|
503
|
+
expect(
|
|
504
|
+
bootstrapStore.claimPendingActivationBootstrap({ platform: "openclaw", accountId: "default" }),
|
|
505
|
+
).toEqual({
|
|
506
|
+
conversationId: "conv-new",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
store.close();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("reclaims stale activation bootstrap claims after the timeout", () => {
|
|
513
|
+
vi.useFakeTimers();
|
|
514
|
+
vi.setSystemTime(10_000);
|
|
515
|
+
const store = createClawChatStore({ dbPath: tempDbPath() });
|
|
516
|
+
const bootstrapStore = store as typeof store & {
|
|
517
|
+
claimPendingActivationBootstrap(input: {
|
|
518
|
+
platform: string;
|
|
519
|
+
accountId: string;
|
|
520
|
+
staleClaimMs?: number;
|
|
521
|
+
}): { conversationId: string } | null;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
store.upsertActivation({
|
|
525
|
+
platform: "openclaw",
|
|
526
|
+
accountId: "default",
|
|
527
|
+
userId: "user-new",
|
|
528
|
+
conversationId: "conv-stale",
|
|
529
|
+
activatedAt: 10_000,
|
|
530
|
+
loginMethod: "login",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(
|
|
534
|
+
bootstrapStore.claimPendingActivationBootstrap({
|
|
535
|
+
platform: "openclaw",
|
|
536
|
+
accountId: "default",
|
|
537
|
+
staleClaimMs: 1000,
|
|
538
|
+
}),
|
|
539
|
+
).toEqual({ conversationId: "conv-stale" });
|
|
540
|
+
expect(
|
|
541
|
+
bootstrapStore.claimPendingActivationBootstrap({
|
|
542
|
+
platform: "openclaw",
|
|
543
|
+
accountId: "default",
|
|
544
|
+
staleClaimMs: 1000,
|
|
545
|
+
}),
|
|
546
|
+
).toBeNull();
|
|
547
|
+
|
|
548
|
+
vi.setSystemTime(11_001);
|
|
549
|
+
expect(
|
|
550
|
+
bootstrapStore.claimPendingActivationBootstrap({
|
|
551
|
+
platform: "openclaw",
|
|
552
|
+
accountId: "default",
|
|
553
|
+
staleClaimMs: 1000,
|
|
554
|
+
}),
|
|
555
|
+
).toEqual({ conversationId: "conv-stale" });
|
|
556
|
+
|
|
557
|
+
store.close();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
});
|