@chainlesschain/personal-data-hub 0.4.28 → 0.4.30
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/README.md +13 -5
- package/lib/adapters/social-douyin-adb/usage-profile-reader.js +253 -0
- package/lib/adapters/social-douyin-adb/watch-history-reader.js +104 -31
- package/lib/adapters/social-toutiao-adb/article-reader.js +202 -0
- package/lib/analysis-skills/overview.js +24 -4
- package/lib/analysis-skills/spending.js +63 -2
- package/lib/analysis-skills/timeline.js +11 -6
- package/lib/prompt-builder.js +15 -1
- package/lib/query-parser.js +38 -8
- package/package.json +4 -1
- package/__tests__/adapter-guide.test.js +0 -47
- package/__tests__/adapter-spec.test.js +0 -78
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
- package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
- package/__tests__/adapters/ai-chat-history.test.js +0 -396
- package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
- package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
- package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
- package/__tests__/adapters/apple-health.test.js +0 -95
- package/__tests__/adapters/bank-family.test.js +0 -125
- package/__tests__/adapters/biz-tianyancha.test.js +0 -159
- package/__tests__/adapters/browser-history-chrome.test.js +0 -377
- package/__tests__/adapters/browser-history-edge.test.js +0 -159
- package/__tests__/adapters/car-mercedesme.test.js +0 -74
- package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
- package/__tests__/adapters/doc-camscanner.test.js +0 -147
- package/__tests__/adapters/doc-platforms.test.js +0 -177
- package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
- package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
- package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
- package/__tests__/adapters/email-adapter.test.js +0 -742
- package/__tests__/adapters/email-classifier.test.js +0 -347
- package/__tests__/adapters/email-imap-session.test.js +0 -334
- package/__tests__/adapters/email-parser.test.js +0 -244
- package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
- package/__tests__/adapters/email-providers.test.js +0 -84
- package/__tests__/adapters/email-retry-progress.test.js +0 -294
- package/__tests__/adapters/email-templates.test.js +0 -822
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
- package/__tests__/adapters/finance-alipay-live.test.js +0 -258
- package/__tests__/adapters/finance-dcep.test.js +0 -74
- package/__tests__/adapters/fitness-joyrun.test.js +0 -82
- package/__tests__/adapters/game-genshin-live.test.js +0 -238
- package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
- package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
- package/__tests__/adapters/git-activity.test.js +0 -222
- package/__tests__/adapters/gov-12123.test.js +0 -103
- package/__tests__/adapters/gov-ixiamen.test.js +0 -150
- package/__tests__/adapters/gov-tax.test.js +0 -135
- package/__tests__/adapters/health-meiyou.test.js +0 -125
- package/__tests__/adapters/local-files.test.js +0 -264
- package/__tests__/adapters/local-im-pc.test.js +0 -154
- package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
- package/__tests__/adapters/music-kugou.test.js +0 -187
- package/__tests__/adapters/music-qq.test.js +0 -112
- package/__tests__/adapters/netease-music-live.test.js +0 -244
- package/__tests__/adapters/netease-music.test.js +0 -74
- package/__tests__/adapters/pc-local-discovery.test.js +0 -141
- package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
- package/__tests__/adapters/reading-family.test.js +0 -108
- package/__tests__/adapters/recruit-boss.test.js +0 -180
- package/__tests__/adapters/shell-history.test.js +0 -180
- package/__tests__/adapters/shopping-base.test.js +0 -179
- package/__tests__/adapters/shopping-dianping.test.js +0 -239
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
- package/__tests__/adapters/social-csdn.test.js +0 -175
- package/__tests__/adapters/social-dongchedi.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
- package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -192
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
- package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
- package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
- package/__tests__/adapters/social-zhihu.test.js +0 -246
- package/__tests__/adapters/system-data-adapter.test.js +0 -443
- package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
- package/__tests__/adapters/system-data-android.test.js +0 -519
- package/__tests__/adapters/system-data-disclosure.test.js +0 -153
- package/__tests__/adapters/travel-12306.test.js +0 -512
- package/__tests__/adapters/travel-amap.test.js +0 -219
- package/__tests__/adapters/travel-baidu-map.test.js +0 -305
- package/__tests__/adapters/travel-base.test.js +0 -205
- package/__tests__/adapters/travel-ctrip.test.js +0 -377
- package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
- package/__tests__/adapters/travel-didi.test.js +0 -204
- package/__tests__/adapters/travel-tencent-map.test.js +0 -207
- package/__tests__/adapters/travel-tongcheng.test.js +0 -289
- package/__tests__/adapters/video-platforms.test.js +0 -152
- package/__tests__/adapters/video-xigua.test.js +0 -106
- package/__tests__/adapters/vscode.test.js +0 -299
- package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
- package/__tests__/adapters/wechat-env-probe.test.js +0 -162
- package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
- package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
- package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
- package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
- package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
- package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
- package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
- package/__tests__/adapters/weread.test.js +0 -123
- package/__tests__/adapters/wework-pc.test.js +0 -124
- package/__tests__/adapters/win-recent.test.js +0 -192
- package/__tests__/analysis-skills.test.js +0 -679
- package/__tests__/analysis.test.js +0 -1845
- package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
- package/__tests__/batch.test.js +0 -133
- package/__tests__/bridges-cc-kg.test.js +0 -231
- package/__tests__/bridges-cc-llm.test.js +0 -191
- package/__tests__/bridges-cc-rag.test.js +0 -162
- package/__tests__/categories.test.js +0 -92
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
- package/__tests__/e2e/full-user-journey.test.js +0 -188
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
- package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
- package/__tests__/entity-resolver-stages.test.js +0 -411
- package/__tests__/entity-resolver-vault.test.js +0 -249
- package/__tests__/entity-resolver.test.js +0 -526
- package/__tests__/fitness-keep-snapshot.test.js +0 -224
- package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
- package/__tests__/ids.test.js +0 -45
- package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
- package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
- package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
- package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
- package/__tests__/key-providers.test.js +0 -126
- package/__tests__/kg-derive.test.js +0 -219
- package/__tests__/llm-client.test.js +0 -122
- package/__tests__/longtail-adapters.test.js +0 -281
- package/__tests__/messaging-qq-snapshot.test.js +0 -294
- package/__tests__/mobile-extractor-encrypted.test.js +0 -460
- package/__tests__/mobile-extractor.test.js +0 -288
- package/__tests__/mock-adapter.test.js +0 -93
- package/__tests__/prompt-builder.test.js +0 -249
- package/__tests__/query-parser.test.js +0 -302
- package/__tests__/rag-derive.test.js +0 -169
- package/__tests__/registry-readiness.test.js +0 -292
- package/__tests__/registry.test.js +0 -420
- package/__tests__/salvage-ingest.test.js +0 -97
- package/__tests__/schemas.test.js +0 -331
- package/__tests__/shopping-adapters.test.js +0 -392
- package/__tests__/shopping-eleme-snapshot.test.js +0 -454
- package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
- package/__tests__/shopping-snapshot.test.js +0 -438
- package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
- package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
- package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
- package/__tests__/sidecar-supervisor.test.js +0 -128
- package/__tests__/sign-providers.test.js +0 -62
- package/__tests__/social-adapters.test.js +0 -280
- package/__tests__/social-bilibili-snapshot.test.js +0 -278
- package/__tests__/social-douban-snapshot.test.js +0 -351
- package/__tests__/social-douyin-im-direct-read.test.js +0 -377
- package/__tests__/social-douyin-salvage-collector.test.js +0 -98
- package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
- package/__tests__/social-douyin-snapshot.test.js +0 -256
- package/__tests__/social-kuaishou-snapshot.test.js +0 -362
- package/__tests__/social-toutiao-snapshot.test.js +0 -366
- package/__tests__/social-weibo-snapshot.test.js +0 -234
- package/__tests__/social-weibo-sqlite-device.test.js +0 -174
- package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
- package/__tests__/sqlite-leaf-salvage.test.js +0 -97
- package/__tests__/travel-adapters.test.js +0 -483
- package/__tests__/travel-maps-snapshot.test.js +0 -426
- package/__tests__/vault-driver-error.test.js +0 -74
- package/__tests__/vault-search-helpers.test.js +0 -104
- package/__tests__/vault-search.test.js +0 -423
- package/__tests__/vault.test.js +0 -767
- package/__tests__/wechat-adapter.test.js +0 -594
- package/__tests__/whatsapp-adapter.test.js +0 -138
- package/scripts/_make-fixture-all.js +0 -126
- package/scripts/_make-fixture-contacts.js +0 -84
- package/scripts/evaluate-entity-resolver.js +0 -213
- package/scripts/run-native-tests-sandbox.sh +0 -55
- package/scripts/smoke-phase-5-5.js +0 -196
- package/scripts/smoke-phase-5-7.js +0 -181
- package/scripts/smoke-system-data-contacts.js +0 -309
- package/scripts/smoke-system-data.js +0 -312
- package/vitest.config.js +0 -88
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Phase 2a — Douyin C 路径 collector orchestrator unit cover.
|
|
5
|
-
*
|
|
6
|
-
* Same fake-bridge + fake-registry pattern as social-bilibili-adb-collector.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
10
|
-
import { existsSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import Database from "better-sqlite3";
|
|
14
|
-
|
|
15
|
-
const {
|
|
16
|
-
collect,
|
|
17
|
-
collectAndSync,
|
|
18
|
-
} = require("../../lib/adapters/social-douyin-adb/collector");
|
|
19
|
-
|
|
20
|
-
let stagingDir;
|
|
21
|
-
let dbDir;
|
|
22
|
-
let fixtureDbPath;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
stagingDir = mkdtempSync(join(tmpdir(), "cc-douyin-staging-"));
|
|
26
|
-
dbDir = mkdtempSync(join(tmpdir(), "cc-douyin-dbfixture-"));
|
|
27
|
-
fixtureDbPath = join(dbDir, "fixture-1234567890123456789_im.db");
|
|
28
|
-
// Build a small valid IM db so the collector has real data to parse.
|
|
29
|
-
const db = new Database(fixtureDbPath);
|
|
30
|
-
db.exec(`
|
|
31
|
-
CREATE TABLE msg(
|
|
32
|
-
sender INTEGER, created_time INTEGER, content TEXT,
|
|
33
|
-
conversation_id TEXT, read_status INTEGER
|
|
34
|
-
);
|
|
35
|
-
CREATE TABLE SIMPLE_USER(
|
|
36
|
-
UID INTEGER, short_id INTEGER, name TEXT,
|
|
37
|
-
avatar_url TEXT, follow_status INTEGER
|
|
38
|
-
);
|
|
39
|
-
INSERT INTO msg VALUES(123, 1716383021000, '{"text":"hi"}', 'conv-A', 1);
|
|
40
|
-
INSERT INTO msg VALUES(456, 1716383022000, '{"text":"hi back"}', 'conv-A', 0);
|
|
41
|
-
INSERT INTO SIMPLE_USER VALUES(456, 789, 'Friend', 'https://x', 1);
|
|
42
|
-
`);
|
|
43
|
-
db.close();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterEach(() => {
|
|
47
|
-
try {
|
|
48
|
-
rmSync(stagingDir, { recursive: true, force: true });
|
|
49
|
-
rmSync(dbDir, { recursive: true, force: true });
|
|
50
|
-
} catch (_e) {
|
|
51
|
-
// ignore
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
function makeFakeBridge({ pullResult, throwOnInvoke } = {}) {
|
|
56
|
-
return {
|
|
57
|
-
invoke: vi.fn(async (method, _params) => {
|
|
58
|
-
if (throwOnInvoke) throw throwOnInvoke;
|
|
59
|
-
if (method !== "douyin.pull-im-db") {
|
|
60
|
-
throw new Error(`fake bridge: unexpected method ${method}`);
|
|
61
|
-
}
|
|
62
|
-
return pullResult;
|
|
63
|
-
}),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function makeCleanupSpy() {
|
|
68
|
-
return vi.fn();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─── collect() — happy path ─────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
describe("collect — happy path", () => {
|
|
74
|
-
it("invokes bridge, parses db, writes snapshot, returns counts", async () => {
|
|
75
|
-
const cleanup = makeCleanupSpy();
|
|
76
|
-
const bridge = makeFakeBridge({
|
|
77
|
-
pullResult: {
|
|
78
|
-
tempPath: fixtureDbPath,
|
|
79
|
-
uid: "1234567890123456789",
|
|
80
|
-
walPath: null,
|
|
81
|
-
shmPath: null,
|
|
82
|
-
extractedAt: 1716383020000,
|
|
83
|
-
cleanup,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
const result = await collect(bridge, { stagingDir });
|
|
87
|
-
expect(bridge.invoke).toHaveBeenCalledWith("douyin.pull-im-db", {
|
|
88
|
-
uid: undefined,
|
|
89
|
-
});
|
|
90
|
-
expect(result.uid).toBe("1234567890123456789");
|
|
91
|
-
expect(result.eventCounts).toEqual({
|
|
92
|
-
message: 2,
|
|
93
|
-
contact: 1,
|
|
94
|
-
total: 3,
|
|
95
|
-
});
|
|
96
|
-
expect(existsSync(result.snapshotPath)).toBe(true);
|
|
97
|
-
const snap = JSON.parse(readFileSync(result.snapshotPath, "utf-8"));
|
|
98
|
-
expect(snap.schemaVersion).toBe(1);
|
|
99
|
-
expect(snap.events).toHaveLength(3);
|
|
100
|
-
expect(result.parserDiagnostic.hadMsgTable).toBe(true);
|
|
101
|
-
expect(result.parserDiagnostic.hadSimpleUserTable).toBe(true);
|
|
102
|
-
// collect() does NOT cleanup db cohort yet — that's caller's
|
|
103
|
-
// responsibility (collectAndSync runs cleanup after syncAdapter).
|
|
104
|
-
expect(cleanup).not.toHaveBeenCalled();
|
|
105
|
-
expect(typeof result._dbCohortCleanup).toBe("function");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("forwards uid filter to extension", async () => {
|
|
109
|
-
const bridge = makeFakeBridge({
|
|
110
|
-
pullResult: {
|
|
111
|
-
tempPath: fixtureDbPath,
|
|
112
|
-
uid: "1234567890123456789",
|
|
113
|
-
cleanup: () => {},
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
await collect(bridge, { stagingDir, uid: "1234567890123456789" });
|
|
117
|
-
expect(bridge.invoke).toHaveBeenCalledWith("douyin.pull-im-db", {
|
|
118
|
-
uid: "1234567890123456789",
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("forwards limits to parser", async () => {
|
|
123
|
-
const bridge = makeFakeBridge({
|
|
124
|
-
pullResult: {
|
|
125
|
-
tempPath: fixtureDbPath,
|
|
126
|
-
uid: "1234567890123456789",
|
|
127
|
-
cleanup: () => {},
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
const result = await collect(bridge, {
|
|
131
|
-
stagingDir,
|
|
132
|
-
limits: { messages: 1, contacts: 0 },
|
|
133
|
-
});
|
|
134
|
-
expect(result.eventCounts.message).toBe(1);
|
|
135
|
-
// contacts: 0 → fallback to default 5000 (parser uses default when
|
|
136
|
-
// limit is 0; that's intentional per im-db-parser.js).
|
|
137
|
-
expect(result.eventCounts.contact).toBeGreaterThanOrEqual(0);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// ─── collect() — failure modes ──────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
describe("collect — failure modes", () => {
|
|
144
|
-
it("propagates bridge.invoke errors verbatim", async () => {
|
|
145
|
-
const bridge = makeFakeBridge({
|
|
146
|
-
throwOnInvoke: new Error("DOUYIN_NO_ROOT: phone isn't rooted"),
|
|
147
|
-
});
|
|
148
|
-
await expect(collect(bridge, { stagingDir })).rejects.toThrow(
|
|
149
|
-
/DOUYIN_NO_ROOT/,
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("rejects malformed bridge payload", async () => {
|
|
154
|
-
const bridge = makeFakeBridge({
|
|
155
|
-
pullResult: { uid: null, tempPath: null },
|
|
156
|
-
});
|
|
157
|
-
await expect(collect(bridge, { stagingDir })).rejects.toThrow(
|
|
158
|
-
/malformed payload/,
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("rejects bridge missing invoke fn", async () => {
|
|
163
|
-
await expect(collect(null, { stagingDir })).rejects.toThrow(TypeError);
|
|
164
|
-
await expect(collect({}, { stagingDir })).rejects.toThrow(TypeError);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("cleans up db cohort if snapshot building throws", async () => {
|
|
168
|
-
const cleanup = makeCleanupSpy();
|
|
169
|
-
const bridge = makeFakeBridge({
|
|
170
|
-
pullResult: {
|
|
171
|
-
tempPath: "/nonexistent/db/path.db", // parser will throw
|
|
172
|
-
uid: "1234567890123456789",
|
|
173
|
-
cleanup,
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
await expect(collect(bridge, { stagingDir })).rejects.toThrow();
|
|
177
|
-
expect(cleanup).toHaveBeenCalledOnce();
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// ─── collectAndSync() ───────────────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
describe("collectAndSync — pipes to registry + always cleans up", () => {
|
|
184
|
-
it("calls registry.syncAdapter('social-douyin') + merges report", async () => {
|
|
185
|
-
const cleanup = makeCleanupSpy();
|
|
186
|
-
const bridge = makeFakeBridge({
|
|
187
|
-
pullResult: {
|
|
188
|
-
tempPath: fixtureDbPath,
|
|
189
|
-
uid: "1234567890123456789",
|
|
190
|
-
cleanup,
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
let syncedPath = null;
|
|
194
|
-
const registry = {
|
|
195
|
-
syncAdapter: vi.fn(async (name, opts) => {
|
|
196
|
-
if (name !== "social-douyin") throw new Error("wrong name");
|
|
197
|
-
syncedPath = opts.inputPath;
|
|
198
|
-
return {
|
|
199
|
-
adapter: name,
|
|
200
|
-
status: "ok",
|
|
201
|
-
rawCount: 3,
|
|
202
|
-
entityCounts: { events: 3, persons: 0, places: 0, items: 0, topics: 0 },
|
|
203
|
-
};
|
|
204
|
-
}),
|
|
205
|
-
};
|
|
206
|
-
const report = await collectAndSync(bridge, registry, { stagingDir });
|
|
207
|
-
expect(registry.syncAdapter).toHaveBeenCalledWith("social-douyin", {
|
|
208
|
-
inputPath: expect.any(String),
|
|
209
|
-
});
|
|
210
|
-
expect(syncedPath).toBeTruthy();
|
|
211
|
-
expect(report.status).toBe("ok");
|
|
212
|
-
expect(report.douyin.uid).toBe("1234567890123456789");
|
|
213
|
-
expect(report.douyin.eventCounts.total).toBe(3);
|
|
214
|
-
// Both snapshot AND db cohort cleaned up
|
|
215
|
-
expect(existsSync(syncedPath)).toBe(false);
|
|
216
|
-
expect(cleanup).toHaveBeenCalledOnce();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("cleans up both even if syncAdapter throws", async () => {
|
|
220
|
-
const cleanup = makeCleanupSpy();
|
|
221
|
-
const bridge = makeFakeBridge({
|
|
222
|
-
pullResult: {
|
|
223
|
-
tempPath: fixtureDbPath,
|
|
224
|
-
uid: "1234567890123456789",
|
|
225
|
-
cleanup,
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
let syncedPath = null;
|
|
229
|
-
const registry = {
|
|
230
|
-
syncAdapter: vi.fn(async (_name, opts) => {
|
|
231
|
-
syncedPath = opts.inputPath;
|
|
232
|
-
throw new Error("registry exploded");
|
|
233
|
-
}),
|
|
234
|
-
};
|
|
235
|
-
await expect(
|
|
236
|
-
collectAndSync(bridge, registry, { stagingDir }),
|
|
237
|
-
).rejects.toThrow("registry exploded");
|
|
238
|
-
expect(syncedPath).toBeTruthy();
|
|
239
|
-
expect(existsSync(syncedPath)).toBe(false);
|
|
240
|
-
expect(cleanup).toHaveBeenCalledOnce();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it("rejects missing registry.syncAdapter", async () => {
|
|
244
|
-
const bridge = makeFakeBridge({
|
|
245
|
-
pullResult: { tempPath: fixtureDbPath, uid: "1", cleanup: () => {} },
|
|
246
|
-
});
|
|
247
|
-
await expect(collectAndSync(bridge, null, { stagingDir })).rejects.toThrow(
|
|
248
|
-
TypeError,
|
|
249
|
-
);
|
|
250
|
-
await expect(collectAndSync(bridge, {}, { stagingDir })).rejects.toThrow(
|
|
251
|
-
TypeError,
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Phase 2a (Douyin C 路径) — cover for the douyin.pull-im-db ADB extension's
|
|
5
|
-
* IM-db discovery + classification.
|
|
6
|
-
*
|
|
7
|
-
* Real-device verification (2026-06-08, Xiaomi chopin / MIUI 13, Douyin
|
|
8
|
-
* logged in) found CURRENT Douyin no longer keeps a plaintext social-DM IM
|
|
9
|
-
* db. The databases/ dir instead holds:
|
|
10
|
-
* - encrypted_<uid>_im.db → SQLCipher social DM (header NOT `SQLite format 3`)
|
|
11
|
-
* - im_database_<uid> → Room db, but it is the in-app 豆包/Doubao AI
|
|
12
|
-
* assistant chat, not social DMs
|
|
13
|
-
* The extension must classify these and emit a precise typed error rather
|
|
14
|
-
* than the misleading DOUYIN_NO_IM_DB.
|
|
15
|
-
*
|
|
16
|
-
* Strategy: scripted fake `ctx.adb` returns a canned `ls` body modeled on
|
|
17
|
-
* the real device listing — no ADB / device needed.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { describe, it, expect, vi } from "vitest";
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
createDouyinDbExtension,
|
|
24
|
-
ENCRYPTED_IM_DB_PATTERN,
|
|
25
|
-
DOUBAO_IM_DB_PATTERN,
|
|
26
|
-
_internals,
|
|
27
|
-
} = require("../../lib/adapters/social-douyin-adb/db-extension");
|
|
28
|
-
|
|
29
|
-
/** Fake ctx: matches the first substring pattern in `responses`. */
|
|
30
|
-
function fakeCtx(responses) {
|
|
31
|
-
const adb = vi.fn(async (args) => {
|
|
32
|
-
const key = args.join(" ");
|
|
33
|
-
for (const [pattern, body] of responses) {
|
|
34
|
-
if (key.includes(pattern)) {
|
|
35
|
-
return typeof body === "function" ? body(args) : body;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`fake adb: no scripted response for: ${key}`);
|
|
39
|
-
});
|
|
40
|
-
return { adb, pickDevice: vi.fn(async () => "FAKE_SERIAL"), parseContentQueryRows: () => [] };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Real device listing (trimmed to the IM-relevant files).
|
|
44
|
-
const REAL_DEVICE_LS = [
|
|
45
|
-
"aweme_database_92585448288",
|
|
46
|
-
"encrypted_92585448288_im.db",
|
|
47
|
-
"encrypted_92585448288_im_customer_box.db",
|
|
48
|
-
"im_database_",
|
|
49
|
-
"im_database_6951980119394929011",
|
|
50
|
-
"push_message.db",
|
|
51
|
-
].join("\n");
|
|
52
|
-
|
|
53
|
-
describe("patterns", () => {
|
|
54
|
-
it("ENCRYPTED_IM_DB_PATTERN matches encrypted_<uid>_im.db only", () => {
|
|
55
|
-
expect("encrypted_92585448288_im.db".match(ENCRYPTED_IM_DB_PATTERN)?.[1]).toBe(
|
|
56
|
-
"92585448288",
|
|
57
|
-
);
|
|
58
|
-
// customer_box variant must NOT be mistaken for the DM store
|
|
59
|
-
expect("encrypted_92585448288_im_customer_box.db".match(ENCRYPTED_IM_DB_PATTERN)).toBe(
|
|
60
|
-
null,
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("DOUBAO_IM_DB_PATTERN matches im_database_<uid> with a real uid", () => {
|
|
65
|
-
expect("im_database_6951980119394929011".match(DOUBAO_IM_DB_PATTERN)?.[1]).toBe(
|
|
66
|
-
"6951980119394929011",
|
|
67
|
-
);
|
|
68
|
-
// empty-uid `im_database_` must not match (needs ≥6 digits)
|
|
69
|
-
expect("im_database_".match(DOUBAO_IM_DB_PATTERN)).toBe(null);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("listImDbs classification (real-device listing)", () => {
|
|
74
|
-
it("buckets encrypted + doubao, finds no legacy plaintext", async () => {
|
|
75
|
-
const ctx = fakeCtx([["ls ", REAL_DEVICE_LS]]);
|
|
76
|
-
const r = await _internals.listImDbs(ctx.adb, "FAKE_SERIAL", {});
|
|
77
|
-
expect(r.candidates).toEqual([]); // no legacy `<19digit>_im.db`
|
|
78
|
-
expect(r.encryptedCandidates.map((c) => c.fileName)).toEqual([
|
|
79
|
-
"encrypted_92585448288_im.db",
|
|
80
|
-
]);
|
|
81
|
-
expect(r.doubaoCandidates.map((c) => c.fileName)).toEqual([
|
|
82
|
-
"im_database_6951980119394929011",
|
|
83
|
-
]);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("createDouyinDbExtension — precise typed errors", () => {
|
|
88
|
-
it("throws DOUYIN_IM_DB_ENCRYPTED when only the SQLCipher DM db exists", async () => {
|
|
89
|
-
const ctx = fakeCtx([
|
|
90
|
-
["id -u", "0"],
|
|
91
|
-
["ls ", "encrypted_92585448288_im.db\nim_database_6951980119394929011"],
|
|
92
|
-
]);
|
|
93
|
-
const ext = createDouyinDbExtension();
|
|
94
|
-
await expect(ext({}, ctx)).rejects.toThrow(/DOUYIN_IM_DB_ENCRYPTED/);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("throws DOUYIN_ONLY_DOUBAO_AI_CHAT when only the Doubao Room db exists", async () => {
|
|
98
|
-
const ctx = fakeCtx([
|
|
99
|
-
["id -u", "0"],
|
|
100
|
-
["ls ", "im_database_6951980119394929011\npush_message.db"],
|
|
101
|
-
]);
|
|
102
|
-
const ext = createDouyinDbExtension();
|
|
103
|
-
await expect(ext({}, ctx)).rejects.toThrow(/DOUYIN_ONLY_DOUBAO_AI_CHAT/);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("still throws DOUYIN_NO_IM_DB when nothing relevant exists", async () => {
|
|
107
|
-
const ctx = fakeCtx([
|
|
108
|
-
["id -u", "0"],
|
|
109
|
-
["ls ", "push_message.db\naweme.db"],
|
|
110
|
-
]);
|
|
111
|
-
const ext = createDouyinDbExtension();
|
|
112
|
-
await expect(ext({}, ctx)).rejects.toThrow(/DOUYIN_NO_IM_DB/);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Phase 2a — IM db parser unit cover.
|
|
5
|
-
*
|
|
6
|
-
* Builds real Douyin-shaped sqlite fixtures via better-sqlite3 (Node ABI
|
|
7
|
-
* 127 test path, same as social-bilibili-adb-chromium-cookies-reader.test.js).
|
|
8
|
-
* Tests cover:
|
|
9
|
-
* - msg table happy path + schema-drift column aliases
|
|
10
|
-
* - SIMPLE_USER table happy path + missing-column tolerance
|
|
11
|
-
* - Empty db / missing table → diagnostic.hadXxxTable=false
|
|
12
|
-
* - Time normalization: seconds / ms / microseconds
|
|
13
|
-
* - Content blob: JSON {text} / nested .content.text / plain string
|
|
14
|
-
* - limitMessages / limitContacts
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
18
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
import { tmpdir } from "node:os";
|
|
21
|
-
import Database from "better-sqlite3";
|
|
22
|
-
|
|
23
|
-
const {
|
|
24
|
-
parseImDb,
|
|
25
|
-
_internals,
|
|
26
|
-
} = require("../../lib/adapters/social-douyin-adb/im-db-parser");
|
|
27
|
-
|
|
28
|
-
let tmpDir;
|
|
29
|
-
let dbPath;
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
tmpDir = mkdtempSync(join(tmpdir(), "cc-douyin-im-test-"));
|
|
33
|
-
dbPath = join(tmpDir, "test_im.db");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
try {
|
|
38
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
-
} catch (_e) {
|
|
40
|
-
// ignore
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
function buildMsgFixture(rows, columnOverrides = {}) {
|
|
45
|
-
const senderCol = columnOverrides.senderCol || "sender";
|
|
46
|
-
const timeCol = columnOverrides.timeCol || "created_time";
|
|
47
|
-
const contentCol = columnOverrides.contentCol || "content";
|
|
48
|
-
const convCol = columnOverrides.convCol || "conversation_id";
|
|
49
|
-
const readCol = columnOverrides.readCol || "read_status";
|
|
50
|
-
const db = new Database(dbPath);
|
|
51
|
-
db.exec(
|
|
52
|
-
`CREATE TABLE msg(${senderCol} INTEGER, ${timeCol} INTEGER, ${contentCol} TEXT, ${convCol} TEXT, ${readCol} INTEGER);`,
|
|
53
|
-
);
|
|
54
|
-
const insert = db.prepare(
|
|
55
|
-
`INSERT INTO msg(${senderCol}, ${timeCol}, ${contentCol}, ${convCol}, ${readCol}) VALUES(?, ?, ?, ?, ?)`,
|
|
56
|
-
);
|
|
57
|
-
for (const r of rows) {
|
|
58
|
-
insert.run(
|
|
59
|
-
r.sender || 0,
|
|
60
|
-
r.time || 0,
|
|
61
|
-
r.content || "",
|
|
62
|
-
r.convId || "",
|
|
63
|
-
r.read || 0,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
db.close();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function buildSimpleUserFixture(rows) {
|
|
70
|
-
const db = new Database(dbPath, { fileMustExist: false });
|
|
71
|
-
// Open in CREATE mode if not yet
|
|
72
|
-
db.exec(`CREATE TABLE IF NOT EXISTS SIMPLE_USER(
|
|
73
|
-
UID INTEGER, short_id INTEGER, name TEXT, avatar_url TEXT, follow_status INTEGER
|
|
74
|
-
);`);
|
|
75
|
-
const insert = db.prepare(
|
|
76
|
-
"INSERT INTO SIMPLE_USER(UID, short_id, name, avatar_url, follow_status) VALUES(?, ?, ?, ?, ?)",
|
|
77
|
-
);
|
|
78
|
-
for (const r of rows) {
|
|
79
|
-
insert.run(
|
|
80
|
-
r.uid || 0,
|
|
81
|
-
r.shortId || 0,
|
|
82
|
-
r.name || "",
|
|
83
|
-
r.avatar || "",
|
|
84
|
-
r.follow || 0,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
db.close();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ─── internals ──────────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
describe("_internals.normalizeEpochMs", () => {
|
|
93
|
-
it("treats seconds as seconds (× 1000)", () => {
|
|
94
|
-
expect(_internals.normalizeEpochMs(1716383021)).toBe(1716383021000);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("treats milliseconds verbatim", () => {
|
|
98
|
-
expect(_internals.normalizeEpochMs(1716383021000)).toBe(1716383021000);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("treats microseconds (× 1000 epoch) as µs / 1000", () => {
|
|
102
|
-
expect(_internals.normalizeEpochMs(1716383021000000)).toBe(1716383021000);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("rejects zero / negative / non-number", () => {
|
|
106
|
-
expect(_internals.normalizeEpochMs(0)).toBe(null);
|
|
107
|
-
expect(_internals.normalizeEpochMs(-1)).toBe(null);
|
|
108
|
-
expect(_internals.normalizeEpochMs(NaN)).toBe(null);
|
|
109
|
-
expect(_internals.normalizeEpochMs(null)).toBe(null);
|
|
110
|
-
expect(_internals.normalizeEpochMs("123")).toBe(null);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("_internals.extractTextFromContent", () => {
|
|
115
|
-
it("parses {text:'...'} JSON", () => {
|
|
116
|
-
expect(_internals.extractTextFromContent('{"text":"hi"}')).toBe("hi");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("parses nested {content:{text:'...'}} JSON", () => {
|
|
120
|
-
expect(
|
|
121
|
-
_internals.extractTextFromContent('{"content":{"text":"nested"}}'),
|
|
122
|
-
).toBe("nested");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("returns raw string when not valid JSON", () => {
|
|
126
|
-
expect(_internals.extractTextFromContent("legacy plaintext")).toBe(
|
|
127
|
-
"legacy plaintext",
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("returns null for empty / non-string", () => {
|
|
132
|
-
expect(_internals.extractTextFromContent("")).toBe(null);
|
|
133
|
-
expect(_internals.extractTextFromContent(null)).toBe(null);
|
|
134
|
-
expect(_internals.extractTextFromContent(undefined)).toBe(null);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("returns null when JSON parses but no text field", () => {
|
|
138
|
-
expect(_internals.extractTextFromContent('{"type":"sticker"}')).toBe(null);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("_internals.pickCol", () => {
|
|
143
|
-
it("returns first matching column", () => {
|
|
144
|
-
const cols = new Set(["created_time", "sender", "content"]);
|
|
145
|
-
expect(_internals.pickCol(cols, ["create_time", "created_time"])).toBe(
|
|
146
|
-
"created_time",
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("returns null when no candidate matches", () => {
|
|
151
|
-
const cols = new Set(["a", "b"]);
|
|
152
|
-
expect(_internals.pickCol(cols, ["c", "d"])).toBe(null);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// ─── msg table happy path ───────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
describe("parseImDb — msg table", () => {
|
|
159
|
-
it("parses canonical msg rows", () => {
|
|
160
|
-
buildMsgFixture([
|
|
161
|
-
{
|
|
162
|
-
sender: 9007199254740991,
|
|
163
|
-
time: 1716383021000,
|
|
164
|
-
content: '{"text":"hello"}',
|
|
165
|
-
convId: "conv-A",
|
|
166
|
-
read: 1,
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
sender: 8007199254740991,
|
|
170
|
-
time: 1716383022000,
|
|
171
|
-
content: '{"text":"hi back"}',
|
|
172
|
-
convId: "conv-A",
|
|
173
|
-
read: 0,
|
|
174
|
-
},
|
|
175
|
-
]);
|
|
176
|
-
const result = parseImDb(dbPath);
|
|
177
|
-
expect(result.diagnostic.hadMsgTable).toBe(true);
|
|
178
|
-
expect(result.diagnostic.messageCount).toBe(2);
|
|
179
|
-
expect(result.messages).toHaveLength(2);
|
|
180
|
-
// Sorted DESC by time
|
|
181
|
-
expect(result.messages[0].text).toBe("hi back");
|
|
182
|
-
expect(result.messages[1].text).toBe("hello");
|
|
183
|
-
expect(result.messages[0].conversationId).toBe("conv-A");
|
|
184
|
-
expect(result.messages[0].readStatus).toBe(0);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("normalizes time to ms regardless of original unit", () => {
|
|
188
|
-
buildMsgFixture([
|
|
189
|
-
{ sender: 1, time: 1716383021, content: '{"text":"seconds"}' },
|
|
190
|
-
]);
|
|
191
|
-
const result = parseImDb(dbPath);
|
|
192
|
-
expect(result.messages[0].createdTimeMs).toBe(1716383021000);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("preserves contentBlob even when text extracts to null", () => {
|
|
196
|
-
buildMsgFixture([
|
|
197
|
-
{ sender: 1, time: 1716383021000, content: '{"type":"sticker"}' },
|
|
198
|
-
]);
|
|
199
|
-
const result = parseImDb(dbPath);
|
|
200
|
-
expect(result.messages[0].text).toBe(null);
|
|
201
|
-
expect(result.messages[0].contentBlob).toBe('{"type":"sticker"}');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("respects limitMessages", () => {
|
|
205
|
-
const rows = Array.from({ length: 100 }, (_, i) => ({
|
|
206
|
-
sender: i,
|
|
207
|
-
time: 1716383021000 + i,
|
|
208
|
-
content: `{"text":"msg-${i}"}`,
|
|
209
|
-
}));
|
|
210
|
-
buildMsgFixture(rows);
|
|
211
|
-
const result = parseImDb(dbPath, { limitMessages: 10 });
|
|
212
|
-
expect(result.messages).toHaveLength(10);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("handles schema-drift column names (create_time / from_user_id)", () => {
|
|
216
|
-
buildMsgFixture(
|
|
217
|
-
[
|
|
218
|
-
{
|
|
219
|
-
sender: 9007199254740991,
|
|
220
|
-
time: 1716383021,
|
|
221
|
-
content: '{"text":"hi"}',
|
|
222
|
-
convId: "c",
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
{
|
|
226
|
-
senderCol: "from_user_id",
|
|
227
|
-
timeCol: "create_time",
|
|
228
|
-
contentCol: "message_content",
|
|
229
|
-
convCol: "conv_id",
|
|
230
|
-
},
|
|
231
|
-
);
|
|
232
|
-
const result = parseImDb(dbPath);
|
|
233
|
-
expect(result.diagnostic.hadMsgTable).toBe(true);
|
|
234
|
-
expect(result.messages).toHaveLength(1);
|
|
235
|
-
expect(result.messages[0].text).toBe("hi");
|
|
236
|
-
expect(result.messages[0].conversationId).toBe("c");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("returns empty messages array when msg table absent", () => {
|
|
240
|
-
const db = new Database(dbPath);
|
|
241
|
-
db.exec("CREATE TABLE unrelated(x INTEGER);");
|
|
242
|
-
db.close();
|
|
243
|
-
const result = parseImDb(dbPath);
|
|
244
|
-
expect(result.messages).toEqual([]);
|
|
245
|
-
expect(result.diagnostic.hadMsgTable).toBe(false);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// ─── SIMPLE_USER table ──────────────────────────────────────────────────
|
|
250
|
-
|
|
251
|
-
describe("parseImDb — SIMPLE_USER table", () => {
|
|
252
|
-
it("parses canonical contact rows", () => {
|
|
253
|
-
buildSimpleUserFixture([
|
|
254
|
-
{ uid: 111, shortId: 222, name: "Alice", avatar: "https://a.png", follow: 1 },
|
|
255
|
-
{ uid: 333, shortId: 444, name: "Bob", avatar: "https://b.png", follow: 2 },
|
|
256
|
-
]);
|
|
257
|
-
const result = parseImDb(dbPath);
|
|
258
|
-
expect(result.diagnostic.hadSimpleUserTable).toBe(true);
|
|
259
|
-
expect(result.diagnostic.contactCount).toBe(2);
|
|
260
|
-
expect(result.contacts[0].name).toBe("Alice");
|
|
261
|
-
expect(result.contacts[0].followStatus).toBe(1);
|
|
262
|
-
expect(result.contacts[1].followStatus).toBe(2);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("returns empty when SIMPLE_USER table absent", () => {
|
|
266
|
-
const db = new Database(dbPath);
|
|
267
|
-
db.exec("CREATE TABLE msg(x INTEGER);");
|
|
268
|
-
db.close();
|
|
269
|
-
const result = parseImDb(dbPath);
|
|
270
|
-
expect(result.contacts).toEqual([]);
|
|
271
|
-
expect(result.diagnostic.hadSimpleUserTable).toBe(false);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("respects limitContacts", () => {
|
|
275
|
-
const rows = Array.from({ length: 50 }, (_, i) => ({
|
|
276
|
-
uid: i + 1,
|
|
277
|
-
shortId: i,
|
|
278
|
-
name: `user-${i}`,
|
|
279
|
-
}));
|
|
280
|
-
buildSimpleUserFixture(rows);
|
|
281
|
-
const result = parseImDb(dbPath, { limitContacts: 7 });
|
|
282
|
-
expect(result.contacts).toHaveLength(7);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ─── Combined / empty ───────────────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
describe("parseImDb — combined diagnostics", () => {
|
|
289
|
-
it("handles both tables present", () => {
|
|
290
|
-
buildMsgFixture([
|
|
291
|
-
{ sender: 1, time: 1716383021000, content: '{"text":"hi"}' },
|
|
292
|
-
]);
|
|
293
|
-
buildSimpleUserFixture([{ uid: 999, name: "x" }]);
|
|
294
|
-
const result = parseImDb(dbPath);
|
|
295
|
-
expect(result.diagnostic.hadMsgTable).toBe(true);
|
|
296
|
-
expect(result.diagnostic.hadSimpleUserTable).toBe(true);
|
|
297
|
-
expect(result.messages.length + result.contacts.length).toBe(2);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("rejects non-string / empty dbPath", () => {
|
|
301
|
-
expect(() => parseImDb("")).toThrow(TypeError);
|
|
302
|
-
expect(() => parseImDb(null)).toThrow(TypeError);
|
|
303
|
-
});
|
|
304
|
-
});
|