@chainlesschain/personal-data-hub 0.4.29 → 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.
Files changed (198) hide show
  1. package/lib/prompt-builder.js +15 -1
  2. package/package.json +4 -1
  3. package/__tests__/adapter-guide.test.js +0 -47
  4. package/__tests__/adapter-spec.test.js +0 -78
  5. package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
  6. package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
  7. package/__tests__/adapters/ai-chat-history.test.js +0 -396
  8. package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
  9. package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
  10. package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
  11. package/__tests__/adapters/apple-health.test.js +0 -95
  12. package/__tests__/adapters/bank-family.test.js +0 -125
  13. package/__tests__/adapters/biz-tianyancha.test.js +0 -159
  14. package/__tests__/adapters/browser-history-chrome.test.js +0 -377
  15. package/__tests__/adapters/browser-history-edge.test.js +0 -159
  16. package/__tests__/adapters/car-mercedesme.test.js +0 -74
  17. package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
  18. package/__tests__/adapters/doc-camscanner.test.js +0 -147
  19. package/__tests__/adapters/doc-platforms.test.js +0 -177
  20. package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
  21. package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
  22. package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
  23. package/__tests__/adapters/email-adapter.test.js +0 -742
  24. package/__tests__/adapters/email-classifier.test.js +0 -347
  25. package/__tests__/adapters/email-imap-session.test.js +0 -334
  26. package/__tests__/adapters/email-parser.test.js +0 -244
  27. package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
  28. package/__tests__/adapters/email-providers.test.js +0 -84
  29. package/__tests__/adapters/email-retry-progress.test.js +0 -294
  30. package/__tests__/adapters/email-templates.test.js +0 -822
  31. package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
  32. package/__tests__/adapters/finance-alipay-live.test.js +0 -258
  33. package/__tests__/adapters/finance-dcep.test.js +0 -74
  34. package/__tests__/adapters/fitness-joyrun.test.js +0 -82
  35. package/__tests__/adapters/game-genshin-live.test.js +0 -238
  36. package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
  37. package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
  38. package/__tests__/adapters/git-activity.test.js +0 -222
  39. package/__tests__/adapters/gov-12123.test.js +0 -103
  40. package/__tests__/adapters/gov-ixiamen.test.js +0 -150
  41. package/__tests__/adapters/gov-tax.test.js +0 -135
  42. package/__tests__/adapters/health-meiyou.test.js +0 -125
  43. package/__tests__/adapters/local-files.test.js +0 -264
  44. package/__tests__/adapters/local-im-pc.test.js +0 -154
  45. package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
  46. package/__tests__/adapters/music-kugou.test.js +0 -187
  47. package/__tests__/adapters/music-qq.test.js +0 -112
  48. package/__tests__/adapters/netease-music-live.test.js +0 -244
  49. package/__tests__/adapters/netease-music.test.js +0 -74
  50. package/__tests__/adapters/pc-local-discovery.test.js +0 -141
  51. package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
  52. package/__tests__/adapters/reading-family.test.js +0 -108
  53. package/__tests__/adapters/recruit-boss.test.js +0 -180
  54. package/__tests__/adapters/shell-history.test.js +0 -180
  55. package/__tests__/adapters/shopping-base.test.js +0 -179
  56. package/__tests__/adapters/shopping-dianping.test.js +0 -239
  57. package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
  58. package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
  59. package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
  60. package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
  61. package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
  62. package/__tests__/adapters/social-csdn.test.js +0 -175
  63. package/__tests__/adapters/social-dongchedi.test.js +0 -165
  64. package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
  65. package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
  66. package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
  67. package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
  68. package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
  69. package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
  70. package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
  71. package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
  72. package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
  73. package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
  74. package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
  75. package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
  76. package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
  77. package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
  78. package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
  79. package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
  80. package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
  81. package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
  82. package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
  83. package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
  84. package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
  85. package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
  86. package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
  87. package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
  88. package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
  89. package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
  90. package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
  91. package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
  92. package/__tests__/adapters/social-zhihu.test.js +0 -246
  93. package/__tests__/adapters/system-data-adapter.test.js +0 -443
  94. package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
  95. package/__tests__/adapters/system-data-android.test.js +0 -519
  96. package/__tests__/adapters/system-data-disclosure.test.js +0 -153
  97. package/__tests__/adapters/travel-12306.test.js +0 -512
  98. package/__tests__/adapters/travel-amap.test.js +0 -219
  99. package/__tests__/adapters/travel-baidu-map.test.js +0 -305
  100. package/__tests__/adapters/travel-base.test.js +0 -205
  101. package/__tests__/adapters/travel-ctrip.test.js +0 -377
  102. package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
  103. package/__tests__/adapters/travel-didi.test.js +0 -204
  104. package/__tests__/adapters/travel-tencent-map.test.js +0 -207
  105. package/__tests__/adapters/travel-tongcheng.test.js +0 -289
  106. package/__tests__/adapters/video-platforms.test.js +0 -152
  107. package/__tests__/adapters/video-xigua.test.js +0 -106
  108. package/__tests__/adapters/vscode.test.js +0 -299
  109. package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
  110. package/__tests__/adapters/wechat-env-probe.test.js +0 -162
  111. package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
  112. package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
  113. package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
  114. package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
  115. package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
  116. package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
  117. package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
  118. package/__tests__/adapters/weread.test.js +0 -123
  119. package/__tests__/adapters/wework-pc.test.js +0 -124
  120. package/__tests__/adapters/win-recent.test.js +0 -192
  121. package/__tests__/analysis-skills.test.js +0 -754
  122. package/__tests__/analysis.test.js +0 -1845
  123. package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
  124. package/__tests__/batch.test.js +0 -133
  125. package/__tests__/bridges-cc-kg.test.js +0 -231
  126. package/__tests__/bridges-cc-llm.test.js +0 -191
  127. package/__tests__/bridges-cc-rag.test.js +0 -162
  128. package/__tests__/categories.test.js +0 -92
  129. package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
  130. package/__tests__/e2e/full-user-journey.test.js +0 -188
  131. package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
  132. package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
  133. package/__tests__/entity-resolver-stages.test.js +0 -411
  134. package/__tests__/entity-resolver-vault.test.js +0 -249
  135. package/__tests__/entity-resolver.test.js +0 -526
  136. package/__tests__/fitness-keep-snapshot.test.js +0 -224
  137. package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
  138. package/__tests__/ids.test.js +0 -45
  139. package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
  140. package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
  141. package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
  142. package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
  143. package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
  144. package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
  145. package/__tests__/key-providers.test.js +0 -126
  146. package/__tests__/kg-derive.test.js +0 -219
  147. package/__tests__/llm-client.test.js +0 -122
  148. package/__tests__/longtail-adapters.test.js +0 -281
  149. package/__tests__/messaging-qq-snapshot.test.js +0 -294
  150. package/__tests__/mobile-extractor-encrypted.test.js +0 -460
  151. package/__tests__/mobile-extractor.test.js +0 -288
  152. package/__tests__/mock-adapter.test.js +0 -93
  153. package/__tests__/prompt-builder.test.js +0 -249
  154. package/__tests__/query-parser.test.js +0 -365
  155. package/__tests__/rag-derive.test.js +0 -169
  156. package/__tests__/registry-readiness.test.js +0 -292
  157. package/__tests__/registry.test.js +0 -420
  158. package/__tests__/salvage-ingest.test.js +0 -97
  159. package/__tests__/schemas.test.js +0 -331
  160. package/__tests__/shopping-adapters.test.js +0 -392
  161. package/__tests__/shopping-eleme-snapshot.test.js +0 -454
  162. package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
  163. package/__tests__/shopping-snapshot.test.js +0 -438
  164. package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
  165. package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
  166. package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
  167. package/__tests__/sidecar-supervisor.test.js +0 -128
  168. package/__tests__/sign-providers.test.js +0 -62
  169. package/__tests__/social-adapters.test.js +0 -280
  170. package/__tests__/social-bilibili-snapshot.test.js +0 -278
  171. package/__tests__/social-douban-snapshot.test.js +0 -351
  172. package/__tests__/social-douyin-im-direct-read.test.js +0 -377
  173. package/__tests__/social-douyin-salvage-collector.test.js +0 -98
  174. package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
  175. package/__tests__/social-douyin-snapshot.test.js +0 -256
  176. package/__tests__/social-kuaishou-snapshot.test.js +0 -362
  177. package/__tests__/social-toutiao-snapshot.test.js +0 -366
  178. package/__tests__/social-weibo-snapshot.test.js +0 -234
  179. package/__tests__/social-weibo-sqlite-device.test.js +0 -174
  180. package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
  181. package/__tests__/sqlite-leaf-salvage.test.js +0 -97
  182. package/__tests__/travel-adapters.test.js +0 -483
  183. package/__tests__/travel-maps-snapshot.test.js +0 -426
  184. package/__tests__/vault-driver-error.test.js +0 -74
  185. package/__tests__/vault-search-helpers.test.js +0 -104
  186. package/__tests__/vault-search.test.js +0 -423
  187. package/__tests__/vault.test.js +0 -767
  188. package/__tests__/wechat-adapter.test.js +0 -594
  189. package/__tests__/whatsapp-adapter.test.js +0 -138
  190. package/scripts/_make-fixture-all.js +0 -126
  191. package/scripts/_make-fixture-contacts.js +0 -84
  192. package/scripts/evaluate-entity-resolver.js +0 -213
  193. package/scripts/run-native-tests-sandbox.sh +0 -55
  194. package/scripts/smoke-phase-5-5.js +0 -196
  195. package/scripts/smoke-phase-5-7.js +0 -181
  196. package/scripts/smoke-system-data-contacts.js +0 -309
  197. package/scripts/smoke-system-data.js +0 -312
  198. package/vitest.config.js +0 -88
@@ -1,216 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Phase 2a — Douyin snapshot-builder unit cover.
5
- *
6
- * Mirrors social-bilibili-adb-snapshot-builder.test.js but with the
7
- * Douyin event shapes (kind=message / kind=contact).
8
- */
9
-
10
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
11
- import { existsSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
12
- import { join } from "node:path";
13
- import { tmpdir } from "node:os";
14
-
15
- const {
16
- buildSnapshot,
17
- writeSnapshotJson,
18
- cleanupSnapshotJson,
19
- SNAPSHOT_SCHEMA_VERSION,
20
- } = require("../../lib/adapters/social-douyin-adb/snapshot-builder");
21
-
22
- let tmpDir;
23
-
24
- beforeEach(() => {
25
- tmpDir = mkdtempSync(join(tmpdir(), "cc-douyin-snap-test-"));
26
- });
27
-
28
- afterEach(() => {
29
- try {
30
- rmSync(tmpDir, { recursive: true, force: true });
31
- } catch (_e) {
32
- // ignore
33
- }
34
- });
35
-
36
- describe("buildSnapshot — schema contract", () => {
37
- it("returns documented schema shape", () => {
38
- const snap = buildSnapshot({
39
- uid: "1234567890123456789",
40
- displayName: "alice",
41
- messages: [],
42
- contacts: [],
43
- snapshottedAt: 1716383021000,
44
- });
45
- expect(snap.schemaVersion).toBe(SNAPSHOT_SCHEMA_VERSION);
46
- expect(snap.schemaVersion).toBe(1);
47
- expect(snap.snapshottedAt).toBe(1716383021000);
48
- expect(snap.account).toEqual({
49
- secUid: null, // unknown via pure-db extraction
50
- shortId: "1234567890123456789",
51
- displayName: "alice",
52
- });
53
- expect(Array.isArray(snap.events)).toBe(true);
54
- });
55
-
56
- it("rejects non-string / empty uid", () => {
57
- expect(() => buildSnapshot({ uid: 123 })).toThrow(TypeError);
58
- expect(() => buildSnapshot({ uid: "" })).toThrow(TypeError);
59
- expect(() => buildSnapshot({})).toThrow(TypeError);
60
- });
61
-
62
- it("defaults snapshottedAt to Date.now() when omitted", () => {
63
- const before = Date.now();
64
- const snap = buildSnapshot({ uid: "1" });
65
- const after = Date.now();
66
- expect(snap.snapshottedAt).toBeGreaterThanOrEqual(before);
67
- expect(snap.snapshottedAt).toBeLessThanOrEqual(after);
68
- });
69
-
70
- it("defaults displayName to empty string", () => {
71
- const snap = buildSnapshot({ uid: "1" });
72
- expect(snap.account.displayName).toBe("");
73
- });
74
- });
75
-
76
- describe("buildSnapshot — events", () => {
77
- it("maps messages → kind=message events with composite id", () => {
78
- const snap = buildSnapshot({
79
- uid: "1",
80
- messages: [
81
- {
82
- senderUid: "10001",
83
- conversationId: "conv-A",
84
- createdTimeMs: 1716383021000,
85
- text: "hi",
86
- readStatus: 1,
87
- contentBlob: '{"text":"hi"}',
88
- },
89
- ],
90
- });
91
- expect(snap.events).toHaveLength(1);
92
- expect(snap.events[0]).toMatchObject({
93
- kind: "message",
94
- id: "msg-conv-A-1716383021000",
95
- capturedAt: 1716383021000,
96
- senderUid: "10001",
97
- conversationId: "conv-A",
98
- text: "hi",
99
- readStatus: 1,
100
- contentBlob: '{"text":"hi"}',
101
- });
102
- });
103
-
104
- it("falls back to senderUid+time when conversationId absent", () => {
105
- const snap = buildSnapshot({
106
- uid: "1",
107
- messages: [
108
- {
109
- senderUid: "10001",
110
- conversationId: null,
111
- createdTimeMs: 1716383021000,
112
- text: "hi",
113
- },
114
- ],
115
- });
116
- expect(snap.events[0].id).toBe("msg-10001-1716383021000");
117
- });
118
-
119
- it("falls back to index when both conversationId + senderUid absent", () => {
120
- const snap = buildSnapshot({
121
- uid: "1",
122
- messages: [{ text: "stray" }],
123
- });
124
- expect(snap.events[0].id).toBe("msg-msg-0");
125
- });
126
-
127
- it("maps contacts → kind=contact events with uid-based id", () => {
128
- const snap = buildSnapshot({
129
- uid: "1",
130
- contacts: [
131
- { uid: "111", shortId: "222", name: "Alice", avatarUrl: "https://a.png", followStatus: 1 },
132
- ],
133
- });
134
- expect(snap.events[0]).toMatchObject({
135
- kind: "contact",
136
- id: "contact-111",
137
- uid: "111",
138
- shortId: "222",
139
- name: "Alice",
140
- followStatus: 1,
141
- });
142
- });
143
-
144
- it("preserves message → contact ordering", () => {
145
- const snap = buildSnapshot({
146
- uid: "1",
147
- messages: [{ senderUid: "a", createdTimeMs: 1 }],
148
- contacts: [{ uid: "b" }],
149
- });
150
- expect(snap.events.map((e) => e.kind)).toEqual(["message", "contact"]);
151
- });
152
-
153
- it("uses snapshottedAt fallback when message has no createdTimeMs", () => {
154
- const snap = buildSnapshot({
155
- uid: "1",
156
- snapshottedAt: 999,
157
- messages: [{ senderUid: "x", text: "no time" }],
158
- });
159
- expect(snap.events[0].capturedAt).toBe(999);
160
- });
161
-
162
- it("skips null / non-object items", () => {
163
- const snap = buildSnapshot({
164
- uid: "1",
165
- messages: [null, { senderUid: "a", createdTimeMs: 1 }, "junk"],
166
- contacts: [undefined, { uid: "b" }],
167
- });
168
- expect(snap.events).toHaveLength(2);
169
- });
170
-
171
- it("handles non-array fields gracefully", () => {
172
- const snap = buildSnapshot({
173
- uid: "1",
174
- messages: "not an array",
175
- contacts: null,
176
- });
177
- expect(snap.events).toEqual([]);
178
- });
179
- });
180
-
181
- describe("writeSnapshotJson + cleanupSnapshotJson", () => {
182
- it("writes JSON to default tmpdir", () => {
183
- const snap = buildSnapshot({ uid: "1" });
184
- const filePath = writeSnapshotJson(snap);
185
- expect(existsSync(filePath)).toBe(true);
186
- expect(filePath).toContain("cc-douyin-snapshot-");
187
- expect(filePath).toMatch(/\.json$/);
188
- const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
189
- expect(parsed.schemaVersion).toBe(1);
190
- cleanupSnapshotJson(filePath);
191
- expect(existsSync(filePath)).toBe(false);
192
- });
193
-
194
- it("respects custom dir + fileName", () => {
195
- const snap = buildSnapshot({ uid: "1" });
196
- const filePath = writeSnapshotJson(snap, {
197
- dir: tmpDir,
198
- fileName: "custom.json",
199
- });
200
- expect(filePath).toBe(join(tmpDir, "custom.json"));
201
- });
202
-
203
- it("rejects path separators in fileName", () => {
204
- const snap = buildSnapshot({ uid: "1" });
205
- expect(() => writeSnapshotJson(snap, { fileName: "../evil.json" })).toThrow(
206
- /must be a basename/,
207
- );
208
- });
209
-
210
- it("cleanupSnapshotJson tolerates missing / null", () => {
211
- cleanupSnapshotJson(null);
212
- cleanupSnapshotJson(undefined);
213
- cleanupSnapshotJson(join(tmpDir, "nonexistent.json"));
214
- // does not throw
215
- });
216
- });
@@ -1,229 +0,0 @@
1
- /**
2
- * Douyin usage-profile reader tests (real-device-driven 2026-06-18: the user's
3
- * exported 1128_feature_engineering.db has FEInternalUserActivityTable = 81
4
- * rows ≈ 24 days, 175 opens, ~108h, peak 12-17h).
5
- *
6
- * Two layers:
7
- * - pure aggregation via an injected fake Database (no native driver needed);
8
- * - a real better-sqlite3 db + real LocalVault round-trip proving the
9
- * hand-built event passes schema validation, is searchable, and re-ingest
10
- * dedups on the stable originalId.
11
- */
12
- "use strict";
13
-
14
- import { describe, it, expect, beforeAll, afterAll } from "vitest";
15
-
16
- const fs = require("node:fs");
17
- const path = require("node:path");
18
- const os = require("node:os");
19
-
20
- const { LocalVault } = require("../../lib/vault");
21
- const { generateKeyHex } = require("../../lib/key-providers");
22
- const {
23
- USAGE_TABLE,
24
- readDouyinUsageProfile,
25
- summarizeUsageProfile,
26
- buildUsageProfileEvents,
27
- usageProfileToVault,
28
- _internals,
29
- } = require("../../lib/adapters/social-douyin-adb/usage-profile-reader");
30
-
31
- // ── pure aggregation with an injected fake Database ───────────────────
32
- function makeFakeDb(rows, { table = USAGE_TABLE } = {}) {
33
- const cols = [
34
- "id",
35
- "timestamp",
36
- "open_app_count",
37
- "total_duration",
38
- ...Array.from({ length: 24 }, (_v, h) => `launch_hour_${h}`),
39
- ];
40
- return class FakeDb {
41
- constructor() {}
42
- prepare(sql) {
43
- return {
44
- get: (arg) => {
45
- if (/sqlite_master/.test(sql)) {
46
- return arg === table ? { name: table } : undefined;
47
- }
48
- return undefined;
49
- },
50
- all: () => {
51
- if (/table_info/.test(sql)) return cols.map((name) => ({ name }));
52
- if (/FROM "/.test(sql)) return rows;
53
- return [];
54
- },
55
- };
56
- }
57
- close() {}
58
- };
59
- }
60
-
61
- function row({ ts, opens = 1, durMs = 0, hours = {} }) {
62
- const r = { id: 1, timestamp: ts, open_app_count: opens, total_duration: durMs };
63
- for (let h = 0; h < 24; h++) r[`launch_hour_${h}`] = hours[h] || 0;
64
- return r;
65
- }
66
-
67
- const DAY = 86_400_000;
68
-
69
- describe("readDouyinUsageProfile (injected fake db)", () => {
70
- it("aggregates opens, duration, hour histogram, peak hour + bucket, distinct days", () => {
71
- const base = Math.floor(1781000000000 / 1000); // seconds epoch
72
- const Db = makeFakeDb([
73
- row({ ts: base, opens: 2, durMs: 3_600_000, hours: { 13: 3, 9: 1 } }),
74
- row({ ts: base + 86_400, opens: 1, durMs: 1_800_000, hours: { 14: 2, 20: 1 } }),
75
- ]);
76
- const p = readDouyinUsageProfile("x.db", { _databaseClass: Db });
77
- expect(p.sessions).toBe(2);
78
- expect(p.days).toBe(2);
79
- expect(p.totalOpens).toBe(3);
80
- expect(p.totalDurationMs).toBe(5_400_000);
81
- expect(p.hourHistogram[13]).toBe(3);
82
- expect(p.hourHistogram[14]).toBe(2);
83
- expect(p.peakHour).toBe(13); // 3 launches is the single max hour
84
- expect(p.peakBucket).toBe("12-17h"); // 13+14 = 5 dominates
85
- expect(p.bucketTotals["12-17h"]).toBe(5);
86
- expect(p.bucketTotals["18-23h"]).toBe(1);
87
- expect(p.from).toBe(base * 1000);
88
- expect(p.to).toBe((base + 86_400) * 1000);
89
- });
90
-
91
- it("returns an empty profile when the table is absent", () => {
92
- const Db = makeFakeDb([], { table: "SomeOtherTable" });
93
- const p = readDouyinUsageProfile("x.db", { _databaseClass: Db });
94
- expect(p.sessions).toBe(0);
95
- expect(p.peakBucket).toBe(null);
96
- expect(p.hourHistogram).toHaveLength(24);
97
- expect(p.totalDurationMs).toBe(0);
98
- });
99
-
100
- it("counts the same calendar day once even across multiple sessions", () => {
101
- const base = Math.floor(1781000000000 / 1000);
102
- const Db = makeFakeDb([
103
- row({ ts: base, hours: { 10: 1 } }),
104
- row({ ts: base + 3600, hours: { 11: 1 } }), // same UTC day
105
- ]);
106
- const p = readDouyinUsageProfile("x.db", { _databaseClass: Db });
107
- expect(p.days).toBe(1);
108
- expect(p.sessions).toBe(2);
109
- });
110
-
111
- it("toEpochMs treats >1e12 as ms else seconds; rejects junk", () => {
112
- expect(_internals.toEpochMs(1781000000)).toBe(1781000000000);
113
- expect(_internals.toEpochMs(1781000000000)).toBe(1781000000000);
114
- expect(_internals.toEpochMs(0)).toBe(null);
115
- expect(_internals.toEpochMs("nope")).toBe(null);
116
- });
117
- });
118
-
119
- describe("summarizeUsageProfile + buildUsageProfileEvents", () => {
120
- it("summary is empty-safe and renders hours + peak", () => {
121
- expect(summarizeUsageProfile(null)).toMatch(/无数据/);
122
- expect(summarizeUsageProfile({ sessions: 0 })).toMatch(/无数据/);
123
- const txt = summarizeUsageProfile({
124
- sessions: 81,
125
- days: 24,
126
- totalOpens: 175,
127
- totalDurationMs: 388_440_000, // 107.9h
128
- peakBucket: "12-17h",
129
- });
130
- expect(txt).toMatch(/24 天/);
131
- expect(txt).toMatch(/175 次启动/);
132
- expect(txt).toMatch(/107\.9 小时/);
133
- expect(txt).toMatch(/12-17h/);
134
- });
135
-
136
- it("builds no events for an empty profile", () => {
137
- expect(buildUsageProfileEvents({ sessions: 0 }).events).toHaveLength(0);
138
- expect(buildUsageProfileEvents(null).events).toHaveLength(0);
139
- });
140
-
141
- it("builds one app-usage-profile event with stable originalId + histogram in extra", () => {
142
- const profile = {
143
- sessions: 81, days: 24, from: 1, to: 1781800000000,
144
- totalOpens: 175, totalDurationMs: 388_440_000,
145
- hourHistogram: new Array(24).fill(0), peakHour: 13, peakBucket: "12-17h",
146
- bucketTotals: { "0-5h": 1, "6-11h": 81, "12-17h": 107, "18-23h": 75 },
147
- };
148
- const { events } = buildUsageProfileEvents(profile, { now: 1781900000000 });
149
- expect(events).toHaveLength(1);
150
- const e = events[0];
151
- expect(e.subtype).toBe("other");
152
- expect(e.source.adapter).toBe("social-douyin");
153
- expect(e.source.originalId).toBe("social-douyin:usage-profile");
154
- expect(e.source.capturedBy).toBe("sqlite");
155
- expect(e.occurredAt).toBe(1781800000000); // profile.to
156
- expect(e.extra.kind).toBe("app-usage-profile");
157
- expect(e.extra.peakBucket).toBe("12-17h");
158
- expect(e.extra.bucketTotals["12-17h"]).toBe(107);
159
- expect(Array.isArray(e.extra.hourHistogram)).toBe(true);
160
- });
161
- });
162
-
163
- // ── real db + real vault round-trip (schema validation + dedup) ───────
164
- describe("usageProfileToVault — real sqlite + real vault", () => {
165
- let dir, dbPath, vdir, vault;
166
-
167
- beforeAll(() => {
168
- const Database = require("better-sqlite3-multiple-ciphers");
169
- dir = fs.mkdtempSync(path.join(os.tmpdir(), "douyin-usage-"));
170
- dbPath = path.join(dir, "1128_feature_engineering.db");
171
- const db = new Database(dbPath);
172
- const hourCols = Array.from({ length: 24 }, (_v, h) => `launch_hour_${h} INTEGER`).join(", ");
173
- db.exec(
174
- `CREATE TABLE "${USAGE_TABLE}" (id INTEGER, timestamp INTEGER, ` +
175
- `start_timestamp_ms INTEGER, end_timestamp_ms INTEGER, ` +
176
- `open_app_count INTEGER, ${hourCols}, total_duration INTEGER)`,
177
- );
178
- const hzero = Array.from({ length: 24 }, () => 0);
179
- const insCols = ["id", "timestamp", "start_timestamp_ms", "end_timestamp_ms", "open_app_count",
180
- ...Array.from({ length: 24 }, (_v, h) => `launch_hour_${h}`), "total_duration"];
181
- const ph = insCols.map(() => "?").join(",");
182
- const ins = db.prepare(`INSERT INTO "${USAGE_TABLE}" (${insCols.join(",")}) VALUES (${ph})`);
183
- const baseSec = Math.floor(1781000000000 / 1000);
184
- // two sessions on two different days; 13h is the peak hour
185
- const h1 = [...hzero]; h1[13] = 3; h1[9] = 1;
186
- const h2 = [...hzero]; h2[14] = 2;
187
- ins.run(1, baseSec, baseSec * 1000, baseSec * 1000 + 1000, 2, ...h1, 3_600_000);
188
- ins.run(2, baseSec + 86_400, 0, 0, 1, ...h2, 1_800_000);
189
- db.close();
190
-
191
- vdir = fs.mkdtempSync(path.join(os.tmpdir(), "douyin-usage-vault-"));
192
- vault = new LocalVault({ path: path.join(vdir, "v.db"), key: generateKeyHex() });
193
- vault.open();
194
- });
195
-
196
- afterAll(() => {
197
- try { vault.close(); } catch (_e) { /* best-effort */ }
198
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
199
- try { fs.rmSync(vdir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
200
- });
201
-
202
- it("reads the real table, aggregates, and ingests one valid event", () => {
203
- const profile = readDouyinUsageProfile(dbPath, {});
204
- expect(profile.sessions).toBe(2);
205
- expect(profile.days).toBe(2);
206
- expect(profile.peakHour).toBe(13);
207
- expect(profile.peakBucket).toBe("12-17h");
208
-
209
- const r = usageProfileToVault(vault, dbPath, { now: 1781900000000 });
210
- expect(r.ingested).toBe(1); // proves the hand-built event passed schema validation
211
- expect(r.sessions).toBe(2);
212
-
213
- const events = vault.queryEvents({ limit: 100 }) || [];
214
- const mine = events.filter(
215
- (e) => e.extra && e.extra.kind === "app-usage-profile",
216
- );
217
- expect(mine.length).toBe(1);
218
- expect(mine[0].source.adapter).toBe("social-douyin");
219
- });
220
-
221
- it("re-ingest dedups on the stable originalId (no duplicate baseline)", () => {
222
- usageProfileToVault(vault, dbPath, { now: 1781999999999 });
223
- const events = vault.queryEvents({ limit: 100 }) || [];
224
- const mine = events.filter(
225
- (e) => e.extra && e.extra.kind === "app-usage-profile",
226
- );
227
- expect(mine.length).toBe(1); // still one — updated, not duplicated
228
- });
229
- });