@chainlesschain/personal-data-hub 0.4.29 → 0.4.31

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.
Files changed (199) hide show
  1. package/lib/forensics/qq-nt-collect.js +190 -0
  2. package/lib/prompt-builder.js +15 -1
  3. package/package.json +8 -3
  4. package/__tests__/adapter-guide.test.js +0 -47
  5. package/__tests__/adapter-spec.test.js +0 -78
  6. package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
  7. package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
  8. package/__tests__/adapters/ai-chat-history.test.js +0 -396
  9. package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
  10. package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
  11. package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
  12. package/__tests__/adapters/apple-health.test.js +0 -95
  13. package/__tests__/adapters/bank-family.test.js +0 -125
  14. package/__tests__/adapters/biz-tianyancha.test.js +0 -159
  15. package/__tests__/adapters/browser-history-chrome.test.js +0 -377
  16. package/__tests__/adapters/browser-history-edge.test.js +0 -159
  17. package/__tests__/adapters/car-mercedesme.test.js +0 -74
  18. package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
  19. package/__tests__/adapters/doc-camscanner.test.js +0 -147
  20. package/__tests__/adapters/doc-platforms.test.js +0 -177
  21. package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
  22. package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
  23. package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
  24. package/__tests__/adapters/email-adapter.test.js +0 -742
  25. package/__tests__/adapters/email-classifier.test.js +0 -347
  26. package/__tests__/adapters/email-imap-session.test.js +0 -334
  27. package/__tests__/adapters/email-parser.test.js +0 -244
  28. package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
  29. package/__tests__/adapters/email-providers.test.js +0 -84
  30. package/__tests__/adapters/email-retry-progress.test.js +0 -294
  31. package/__tests__/adapters/email-templates.test.js +0 -822
  32. package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
  33. package/__tests__/adapters/finance-alipay-live.test.js +0 -258
  34. package/__tests__/adapters/finance-dcep.test.js +0 -74
  35. package/__tests__/adapters/fitness-joyrun.test.js +0 -82
  36. package/__tests__/adapters/game-genshin-live.test.js +0 -238
  37. package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
  38. package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
  39. package/__tests__/adapters/git-activity.test.js +0 -222
  40. package/__tests__/adapters/gov-12123.test.js +0 -103
  41. package/__tests__/adapters/gov-ixiamen.test.js +0 -150
  42. package/__tests__/adapters/gov-tax.test.js +0 -135
  43. package/__tests__/adapters/health-meiyou.test.js +0 -125
  44. package/__tests__/adapters/local-files.test.js +0 -264
  45. package/__tests__/adapters/local-im-pc.test.js +0 -154
  46. package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
  47. package/__tests__/adapters/music-kugou.test.js +0 -187
  48. package/__tests__/adapters/music-qq.test.js +0 -112
  49. package/__tests__/adapters/netease-music-live.test.js +0 -244
  50. package/__tests__/adapters/netease-music.test.js +0 -74
  51. package/__tests__/adapters/pc-local-discovery.test.js +0 -141
  52. package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
  53. package/__tests__/adapters/reading-family.test.js +0 -108
  54. package/__tests__/adapters/recruit-boss.test.js +0 -180
  55. package/__tests__/adapters/shell-history.test.js +0 -180
  56. package/__tests__/adapters/shopping-base.test.js +0 -179
  57. package/__tests__/adapters/shopping-dianping.test.js +0 -239
  58. package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
  59. package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
  60. package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
  61. package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
  62. package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
  63. package/__tests__/adapters/social-csdn.test.js +0 -175
  64. package/__tests__/adapters/social-dongchedi.test.js +0 -165
  65. package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
  66. package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
  67. package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
  68. package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
  69. package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
  70. package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
  71. package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
  72. package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
  73. package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
  74. package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
  75. package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
  76. package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
  77. package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
  78. package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
  79. package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
  80. package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
  81. package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
  82. package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
  83. package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
  84. package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
  85. package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
  86. package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
  87. package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
  88. package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
  89. package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
  90. package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
  91. package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
  92. package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
  93. package/__tests__/adapters/social-zhihu.test.js +0 -246
  94. package/__tests__/adapters/system-data-adapter.test.js +0 -443
  95. package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
  96. package/__tests__/adapters/system-data-android.test.js +0 -519
  97. package/__tests__/adapters/system-data-disclosure.test.js +0 -153
  98. package/__tests__/adapters/travel-12306.test.js +0 -512
  99. package/__tests__/adapters/travel-amap.test.js +0 -219
  100. package/__tests__/adapters/travel-baidu-map.test.js +0 -305
  101. package/__tests__/adapters/travel-base.test.js +0 -205
  102. package/__tests__/adapters/travel-ctrip.test.js +0 -377
  103. package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
  104. package/__tests__/adapters/travel-didi.test.js +0 -204
  105. package/__tests__/adapters/travel-tencent-map.test.js +0 -207
  106. package/__tests__/adapters/travel-tongcheng.test.js +0 -289
  107. package/__tests__/adapters/video-platforms.test.js +0 -152
  108. package/__tests__/adapters/video-xigua.test.js +0 -106
  109. package/__tests__/adapters/vscode.test.js +0 -299
  110. package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
  111. package/__tests__/adapters/wechat-env-probe.test.js +0 -162
  112. package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
  113. package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
  114. package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
  115. package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
  116. package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
  117. package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
  118. package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
  119. package/__tests__/adapters/weread.test.js +0 -123
  120. package/__tests__/adapters/wework-pc.test.js +0 -124
  121. package/__tests__/adapters/win-recent.test.js +0 -192
  122. package/__tests__/analysis-skills.test.js +0 -754
  123. package/__tests__/analysis.test.js +0 -1845
  124. package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
  125. package/__tests__/batch.test.js +0 -133
  126. package/__tests__/bridges-cc-kg.test.js +0 -231
  127. package/__tests__/bridges-cc-llm.test.js +0 -191
  128. package/__tests__/bridges-cc-rag.test.js +0 -162
  129. package/__tests__/categories.test.js +0 -92
  130. package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
  131. package/__tests__/e2e/full-user-journey.test.js +0 -188
  132. package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
  133. package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
  134. package/__tests__/entity-resolver-stages.test.js +0 -411
  135. package/__tests__/entity-resolver-vault.test.js +0 -249
  136. package/__tests__/entity-resolver.test.js +0 -526
  137. package/__tests__/fitness-keep-snapshot.test.js +0 -224
  138. package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
  139. package/__tests__/ids.test.js +0 -45
  140. package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
  141. package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
  142. package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
  143. package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
  144. package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
  145. package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
  146. package/__tests__/key-providers.test.js +0 -126
  147. package/__tests__/kg-derive.test.js +0 -219
  148. package/__tests__/llm-client.test.js +0 -122
  149. package/__tests__/longtail-adapters.test.js +0 -281
  150. package/__tests__/messaging-qq-snapshot.test.js +0 -294
  151. package/__tests__/mobile-extractor-encrypted.test.js +0 -460
  152. package/__tests__/mobile-extractor.test.js +0 -288
  153. package/__tests__/mock-adapter.test.js +0 -93
  154. package/__tests__/prompt-builder.test.js +0 -249
  155. package/__tests__/query-parser.test.js +0 -365
  156. package/__tests__/rag-derive.test.js +0 -169
  157. package/__tests__/registry-readiness.test.js +0 -292
  158. package/__tests__/registry.test.js +0 -420
  159. package/__tests__/salvage-ingest.test.js +0 -97
  160. package/__tests__/schemas.test.js +0 -331
  161. package/__tests__/shopping-adapters.test.js +0 -392
  162. package/__tests__/shopping-eleme-snapshot.test.js +0 -454
  163. package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
  164. package/__tests__/shopping-snapshot.test.js +0 -438
  165. package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
  166. package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
  167. package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
  168. package/__tests__/sidecar-supervisor.test.js +0 -128
  169. package/__tests__/sign-providers.test.js +0 -62
  170. package/__tests__/social-adapters.test.js +0 -280
  171. package/__tests__/social-bilibili-snapshot.test.js +0 -278
  172. package/__tests__/social-douban-snapshot.test.js +0 -351
  173. package/__tests__/social-douyin-im-direct-read.test.js +0 -377
  174. package/__tests__/social-douyin-salvage-collector.test.js +0 -98
  175. package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
  176. package/__tests__/social-douyin-snapshot.test.js +0 -256
  177. package/__tests__/social-kuaishou-snapshot.test.js +0 -362
  178. package/__tests__/social-toutiao-snapshot.test.js +0 -366
  179. package/__tests__/social-weibo-snapshot.test.js +0 -234
  180. package/__tests__/social-weibo-sqlite-device.test.js +0 -174
  181. package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
  182. package/__tests__/sqlite-leaf-salvage.test.js +0 -97
  183. package/__tests__/travel-adapters.test.js +0 -483
  184. package/__tests__/travel-maps-snapshot.test.js +0 -426
  185. package/__tests__/vault-driver-error.test.js +0 -74
  186. package/__tests__/vault-search-helpers.test.js +0 -104
  187. package/__tests__/vault-search.test.js +0 -423
  188. package/__tests__/vault.test.js +0 -767
  189. package/__tests__/wechat-adapter.test.js +0 -594
  190. package/__tests__/whatsapp-adapter.test.js +0 -138
  191. package/scripts/_make-fixture-all.js +0 -126
  192. package/scripts/_make-fixture-contacts.js +0 -84
  193. package/scripts/evaluate-entity-resolver.js +0 -213
  194. package/scripts/run-native-tests-sandbox.sh +0 -55
  195. package/scripts/smoke-phase-5-5.js +0 -196
  196. package/scripts/smoke-phase-5-7.js +0 -181
  197. package/scripts/smoke-system-data-contacts.js +0 -309
  198. package/scripts/smoke-system-data.js +0 -312
  199. 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
- });