@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,238 +0,0 @@
1
- /**
2
- * FAMILY-23 v0.2 — game-genshin LIVE HoYoLAB fetcher tests.
3
- * DS v1 signing determinism + takumi role fetch + game-record stats fold-in
4
- * + adapter cookie-mode sync + error mapping. All network via injected fetch.
5
- */
6
- import { describe, it, expect } from "vitest";
7
- const { GenshinAdapter } = require("../../lib");
8
- const {
9
- GenshinApiClient,
10
- genDs,
11
- md5Hex,
12
- DEFAULT_DS_SALT,
13
- } = require("../../lib/adapters/game-genshin/api-client");
14
- const { validateBatch } = require("../../lib/batch");
15
-
16
- // Deterministic clock + RNG seams.
17
- const FIXED_NOW = () => 1700000000000; // → t = 1700000000
18
- const FIXED_RAND = () => 0.5; // → r = 100001 + floor(0.5 * 99999) = 150000
19
-
20
- /**
21
- * Build a fake fetch that maps URL substrings to JSON responses.
22
- * Each entry: { match, status?, body }. body is auto-JSON.stringify'd.
23
- * Records every requested URL + headers into `calls`.
24
- */
25
- function makeFetch(routes, calls) {
26
- return async (url, init) => {
27
- calls.push({ url, headers: (init && init.headers) || {} });
28
- const route = routes.find((r) => url.includes(r.match));
29
- if (!route) {
30
- return { ok: false, status: 404, text: async () => "not mapped" };
31
- }
32
- return {
33
- ok: route.status ? route.status >= 200 && route.status < 300 : true,
34
- status: route.status || 200,
35
- text: async () =>
36
- typeof route.body === "string" ? route.body : JSON.stringify(route.body),
37
- };
38
- };
39
- }
40
-
41
- const COOKIE = "account_id_v2=809199; cookie_token_v2=abc; ltoken_v2=xyz";
42
-
43
- describe("genDs — DS v1 dynamic-secret signing", () => {
44
- it("produces `${t},${r},${c}` with c = md5(salt&t&r), deterministic under seams", () => {
45
- const ds = genDs(DEFAULT_DS_SALT, FIXED_NOW, FIXED_RAND);
46
- const [t, r, c] = ds.split(",");
47
- expect(t).toBe("1700000000");
48
- expect(r).toBe("150000");
49
- expect(c).toBe(md5Hex(`salt=${DEFAULT_DS_SALT}&t=1700000000&r=150000`));
50
- expect(c).toMatch(/^[0-9a-f]{32}$/);
51
- });
52
-
53
- it("r stays within the 6-digit [100001,200000) window at RNG extremes", () => {
54
- const low = genDs("s", FIXED_NOW, () => 0).split(",")[1];
55
- const high = genDs("s", FIXED_NOW, () => 0.9999999).split(",")[1];
56
- expect(Number(low)).toBe(100001);
57
- expect(Number(high)).toBeLessThan(200000);
58
- expect(Number(high)).toBeGreaterThanOrEqual(100001);
59
- });
60
- });
61
-
62
- describe("GenshinApiClient.fetchProfiles — live (mocked fetch)", () => {
63
- it("resolves bound roles + folds in active_day_number; sends DS-signed mihoyo headers", async () => {
64
- const calls = [];
65
- const fetch = makeFetch(
66
- [
67
- {
68
- match: "getUserGameRolesByCookie",
69
- body: {
70
- retcode: 0,
71
- message: "OK",
72
- data: {
73
- list: [
74
- {
75
- game_uid: "800000001",
76
- nickname: "旅行者",
77
- level: 58,
78
- region: "cn_gf01",
79
- region_name: "天空岛",
80
- },
81
- ],
82
- },
83
- },
84
- },
85
- {
86
- match: "game_record/app/genshin/api/index",
87
- body: { retcode: 0, message: "OK", data: { stats: { active_day_number: 421 } } },
88
- },
89
- ],
90
- calls,
91
- );
92
- const client = new GenshinApiClient({ fetch, now: FIXED_NOW, rand: FIXED_RAND });
93
- const profiles = await client.fetchProfiles(COOKIE);
94
- expect(profiles).toHaveLength(1);
95
- expect(profiles[0]).toMatchObject({
96
- uid: "800000001",
97
- nickname: "旅行者",
98
- level: 58,
99
- region: "cn_gf01",
100
- regionName: "天空岛",
101
- activeDayNumber: 421,
102
- });
103
- // DS header present + well-formed on every call; client_type=5, version set.
104
- for (const c of calls) {
105
- expect(c.headers.DS).toMatch(/^\d+,\d+,[0-9a-f]{32}$/);
106
- expect(c.headers["x-rpc-client_type"]).toBe("5");
107
- expect(c.headers["x-rpc-app_version"]).toBeTruthy();
108
- expect(c.headers.Cookie).toBe(COOKIE);
109
- }
110
- // index request carried role_id + server from the role.
111
- const idx = calls.find((c) => c.url.includes("/index"));
112
- expect(idx.url).toContain("role_id=800000001");
113
- expect(idx.url).toContain("server=cn_gf01");
114
- });
115
-
116
- it("fetchStats:false skips the game-record index call", async () => {
117
- const calls = [];
118
- const fetch = makeFetch(
119
- [
120
- {
121
- match: "getUserGameRolesByCookie",
122
- body: {
123
- retcode: 0,
124
- data: { list: [{ game_uid: "1", nickname: "a", level: 1, region: "cn_qd01" }] },
125
- },
126
- },
127
- ],
128
- calls,
129
- );
130
- const client = new GenshinApiClient({ fetch, now: FIXED_NOW, rand: FIXED_RAND });
131
- const profiles = await client.fetchProfiles(COOKIE, { fetchStats: false });
132
- expect(profiles[0].activeDayNumber).toBeNull();
133
- expect(calls.some((c) => c.url.includes("/index"))).toBe(false);
134
- });
135
-
136
- it("maps mihoyo retcode != 0 to null + lastError (e.g. 1034 risk-control)", async () => {
137
- const calls = [];
138
- const fetch = makeFetch(
139
- [{ match: "getUserGameRolesByCookie", body: { retcode: 1034, message: "请进行验证" } }],
140
- calls,
141
- );
142
- const client = new GenshinApiClient({ fetch, now: FIXED_NOW, rand: FIXED_RAND });
143
- const profiles = await client.fetchProfiles(COOKIE);
144
- expect(profiles).toBeNull();
145
- expect(client.lastError.code).toBe(1034);
146
- expect(client.lastError.message).toContain("验证");
147
- });
148
-
149
- it("empty cookie → null + lastError -1 (no network)", async () => {
150
- const calls = [];
151
- const client = new GenshinApiClient({
152
- fetch: makeFetch([], calls),
153
- now: FIXED_NOW,
154
- rand: FIXED_RAND,
155
- });
156
- expect(await client.fetchProfiles("")).toBeNull();
157
- expect(client.lastError.code).toBe(-1);
158
- expect(calls).toHaveLength(0);
159
- });
160
-
161
- it("HTTP non-2xx → null + lastError with status code", async () => {
162
- const calls = [];
163
- const fetch = makeFetch(
164
- [{ match: "getUserGameRolesByCookie", status: 503, body: "down" }],
165
- calls,
166
- );
167
- const client = new GenshinApiClient({ fetch, now: FIXED_NOW, rand: FIXED_RAND });
168
- expect(await client.fetchProfiles(COOKIE)).toBeNull();
169
- expect(client.lastError.code).toBe(503);
170
- });
171
- });
172
-
173
- describe("GenshinAdapter — cookie (live) sync mode", () => {
174
- it("authenticate accepts a valid cookie (mode: cookie)", async () => {
175
- const a = new GenshinAdapter();
176
- const r = await a.authenticate({ cookie: COOKIE });
177
- expect(r.ok).toBe(true);
178
- expect(r.mode).toBe("cookie");
179
- });
180
-
181
- it("authenticate rejects guest cookie (no uid)", async () => {
182
- const a = new GenshinAdapter();
183
- const r = await a.authenticate({ cookie: "foo=bar" });
184
- expect(r.ok).toBe(false);
185
- expect(r.reason).toBe("INVALID_COOKIE");
186
- });
187
-
188
- it("sync via cookie yields profile raws → valid normalized batch with region/activeDays", async () => {
189
- const calls = [];
190
- const fetch = makeFetch(
191
- [
192
- {
193
- match: "getUserGameRolesByCookie",
194
- body: {
195
- retcode: 0,
196
- data: {
197
- list: [
198
- { game_uid: "800000001", nickname: "旅行者", level: 58, region: "cn_gf01", region_name: "天空岛" },
199
- ],
200
- },
201
- },
202
- },
203
- {
204
- match: "/index",
205
- body: { retcode: 0, data: { stats: { active_day_number: 421 } } },
206
- },
207
- ],
208
- calls,
209
- );
210
- const a = new GenshinAdapter();
211
- const raws = [];
212
- for await (const r of a.sync({ cookie: COOKIE, fetch, now: FIXED_NOW, rand: FIXED_RAND })) {
213
- raws.push(r);
214
- }
215
- expect(raws).toHaveLength(1);
216
- expect(raws[0].kind).toBe("profile");
217
- expect(raws[0].originalId).toBe("genshin:profile:800000001");
218
-
219
- const batch = a.normalize(raws[0]);
220
- expect(validateBatch(batch).valid).toBe(true);
221
- const person = batch.persons[0];
222
- expect(person.identifiers["genshin-uid"]).toEqual(["800000001"]);
223
- expect(person.extra.region).toBe("cn_gf01");
224
- expect(person.extra.activeDayNumber).toBe(421);
225
- });
226
-
227
- it("sync via cookie throws (mapped lastError) on API risk-control", async () => {
228
- const calls = [];
229
- const fetch = makeFetch(
230
- [{ match: "getUserGameRolesByCookie", body: { retcode: 10001, message: "登录失效" } }],
231
- calls,
232
- );
233
- const a = new GenshinAdapter();
234
- await expect(async () => {
235
- for await (const _ of a.sync({ cookie: COOKIE, fetch, now: FIXED_NOW, rand: FIXED_RAND })) void _;
236
- }).rejects.toThrow(/登录失效|code 10001/);
237
- });
238
- });
@@ -1,108 +0,0 @@
1
- /**
2
- * FAMILY-23 v0.1 — game-genshin adapter scaffold tests.
3
- * 契约 + extractUid cookie scrape + snapshot sync + normalize → 合法 batch。
4
- */
5
- import { describe, it, expect } from "vitest";
6
- const fs = require("node:fs");
7
- const os = require("node:os");
8
- const path = require("node:path");
9
- const { GenshinAdapter } = require("../../lib");
10
- const { GenshinApiClient } = require("../../lib/adapters/game-genshin/api-client");
11
- const { assertAdapter } = require("../../lib/adapter-spec");
12
- const { validateBatch } = require("../../lib/batch");
13
-
14
- function writeSnapshot(obj) {
15
- const p = path.join(
16
- os.tmpdir(),
17
- `genshin-snap-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
18
- );
19
- fs.writeFileSync(p, JSON.stringify(obj), "utf-8");
20
- return p;
21
- }
22
-
23
- describe("GenshinAdapter — FAMILY-23 v0.1 cookie-scrape placeholder", () => {
24
- it("contract conformance + sensitivity medium", () => {
25
- const a = new GenshinAdapter();
26
- expect(assertAdapter(a).ok).toBe(true);
27
- expect(a.name).toBe("game-genshin");
28
- expect(a.version).toBe("0.2.0");
29
- expect(a.extractMode).toBe("web-api");
30
- expect(a.dataDisclosure.sensitivity).toBe("medium");
31
- expect(a.capabilities).toContain("sync:snapshot");
32
- expect(a.capabilities).toContain("sync:cookie");
33
- });
34
-
35
- it("extractUid parses HoYoLAB cookie keys (priority + null)", () => {
36
- const c = new GenshinApiClient();
37
- expect(c.extractUid("account_id_v2=12345; ltoken_v2=abc")).toBe("12345");
38
- expect(c.extractUid("ltuid_v2=67890; foo=bar")).toBe("67890");
39
- expect(c.extractUid("account_id=111; x=y")).toBe("111");
40
- expect(c.extractUid("ltuid=222")).toBe("222");
41
- // 优先级: account_id_v2 先于 ltuid
42
- expect(c.extractUid("ltuid=999; account_id_v2=12345")).toBe("12345");
43
- expect(c.extractUid("foo=bar")).toBeNull();
44
- expect(c.lastError.code).toBe(-7);
45
- expect(c.extractUid("")).toBeNull();
46
- expect(c.lastError.code).toBe(-1);
47
- });
48
-
49
- it("sync throws without inputPath or cookie", async () => {
50
- const a = new GenshinAdapter();
51
- await expect(async () => {
52
- for await (const _ of a.sync({})) void _;
53
- }).rejects.toThrow(/inputPath.*cookie|cookie.*inputPath/);
54
- });
55
-
56
- it("sync via snapshot yields profile + play raws", async () => {
57
- const snapPath = writeSnapshot({
58
- schemaVersion: 1,
59
- snapshottedAt: 1700000000000,
60
- account: { uid: "12345", displayName: "旅行者" },
61
- events: [
62
- { kind: "profile", id: "profile-12345", uid: "12345", nickname: "旅行者", level: 58 },
63
- { kind: "play", id: "play-s1", durationMs: 3600000, mode: "single", startAt: 1700000000000 },
64
- ],
65
- });
66
- try {
67
- const a = new GenshinAdapter();
68
- const raws = [];
69
- for await (const r of a.sync({ inputPath: snapPath })) raws.push(r);
70
- expect(raws).toHaveLength(2);
71
- expect(raws[0].kind).toBe("profile");
72
- expect(raws[0].originalId).toBe("genshin:profile:profile-12345");
73
- expect(raws[1].kind).toBe("play");
74
- } finally {
75
- fs.unlinkSync(snapPath);
76
- }
77
- });
78
-
79
- it("normalize profile → person-self with genshin-uid; play → MEDIA event (valid batch)", () => {
80
- const a = new GenshinAdapter();
81
- const profileBatch = a.normalize({
82
- adapter: "game-genshin",
83
- kind: "profile",
84
- originalId: "genshin:profile:profile-12345",
85
- capturedAt: 1700000000000,
86
- payload: { kind: "profile", uid: "12345", nickname: "旅行者", level: 58 },
87
- });
88
- expect(validateBatch(profileBatch).valid).toBe(true);
89
- expect(profileBatch.persons[0].subtype).toBe("self");
90
- expect(profileBatch.persons[0].identifiers["genshin-uid"]).toEqual(["12345"]);
91
-
92
- const playBatch = a.normalize({
93
- adapter: "game-genshin",
94
- kind: "play",
95
- originalId: "genshin:play:play-s1",
96
- capturedAt: 1700000000000,
97
- payload: { kind: "play", durationMs: 3600000, mode: "single", startAt: 1700000000000 },
98
- });
99
- expect(validateBatch(playBatch).valid).toBe(true);
100
- expect(playBatch.events[0].subtype).toBe("media");
101
- expect(playBatch.events[0].extra.durationMs).toBe(3600000);
102
- });
103
-
104
- it("normalize throws on missing payload", () => {
105
- const a = new GenshinAdapter();
106
- expect(() => a.normalize({})).toThrow(/payload missing/);
107
- });
108
- });
@@ -1,230 +0,0 @@
1
- /**
2
- * FAMILY-23 v0.2 — game-honor-of-kings LIVE 营地 (Camp) fetcher tests.
3
- * credential (accessToken+openid+role) → profile + battle-list → snapshot-shaped
4
- * events + adapter live sync + defensive field extraction + error mapping.
5
- * All network via injected fetch. Endpoint shapes are best-effort (see api-client
6
- * header) — these tests pin the REQUEST CONSTRUCTION + PARSING contract, not the
7
- * live server, which can't be verified here.
8
- */
9
- "use strict";
10
-
11
- import { describe, it, expect } from "vitest";
12
-
13
- const { HonorOfKingsAdapter } = require("../../lib");
14
- const {
15
- HonorOfKingsApiClient,
16
- pick,
17
- toDurationMs,
18
- toEpochMs,
19
- } = require("../../lib/adapters/game-honor-of-kings/api-client");
20
- const { validateBatch } = require("../../lib/batch");
21
-
22
- const CRED = {
23
- accessToken: "tok-abc",
24
- openid: "OPENID12345678",
25
- acctype: "qc",
26
- areaId: "1",
27
- roleId: "900000001",
28
- };
29
-
30
- function makeFetch(routes, calls) {
31
- return async (url, init) => {
32
- let parsedBody = null;
33
- try {
34
- parsedBody = init && init.body ? JSON.parse(init.body) : null;
35
- } catch (_e) {
36
- parsedBody = init && init.body;
37
- }
38
- calls.push({ url, body: parsedBody, headers: (init && init.headers) || {} });
39
- const route = routes.find((r) => url.includes(r.match));
40
- if (!route) return { ok: false, status: 404, text: async () => "not mapped" };
41
- return {
42
- ok: route.status ? route.status >= 200 && route.status < 300 : true,
43
- status: route.status || 200,
44
- text: async () =>
45
- typeof route.body === "string" ? route.body : JSON.stringify(route.body),
46
- };
47
- };
48
- }
49
-
50
- async function collect(iter) {
51
- const out = [];
52
- for await (const r of iter) out.push(r);
53
- return out;
54
- }
55
-
56
- describe("honor-of-kings field-extraction helpers", () => {
57
- it("pick returns first present non-empty value", () => {
58
- expect(pick({ a: "", b: "x" }, ["a", "b"])).toBe("x");
59
- expect(pick({ a: 0 }, ["a"])).toBe(0); // 0 is present
60
- expect(pick({}, ["a"], "fb")).toBe("fb");
61
- });
62
-
63
- it("toDurationMs treats <1e7 as seconds, else ms; coerces numeric strings", () => {
64
- expect(toDurationMs(1800)).toBe(1800000); // 30 min in seconds
65
- expect(toDurationMs(1800000)).toBe(1800000); // already ms
66
- expect(toDurationMs("900")).toBe(900000);
67
- expect(toDurationMs(0)).toBe(0);
68
- expect(toDurationMs("nope")).toBe(0);
69
- });
70
-
71
- it("toEpochMs coerces seconds/ms/date-string", () => {
72
- expect(toEpochMs(1700000000)).toBe(1700000000000);
73
- expect(toEpochMs(1700000000000)).toBe(1700000000000);
74
- expect(toEpochMs("2023-11-14T22:13:20Z")).toBe(1700000000000);
75
- expect(toEpochMs("bogus")).toBeNull();
76
- });
77
- });
78
-
79
- describe("HonorOfKingsApiClient.fetchSnapshot — live (mocked fetch)", () => {
80
- it("merges auth fields into each request + parses profile/battles defensively", async () => {
81
- const calls = [];
82
- const fetch = makeFetch(
83
- [
84
- {
85
- match: "/play/profildetail",
86
- body: {
87
- returnCode: 0,
88
- data: { role: { roleId: "900000001", roleName: "小明", level: 30, rankName: "钻石", logo: "u.png" } },
89
- },
90
- },
91
- {
92
- match: "/game/getbattlelist",
93
- body: {
94
- returnCode: 0,
95
- data: {
96
- list: [
97
- { gameSeq: "g1", startTime: 1700000000, gametime: 900, mapName: "排位赛" },
98
- { gameSeq: "g2", startTime: "1700003600", usedTime: 1200, modeName: "巅峰赛" },
99
- ],
100
- },
101
- },
102
- },
103
- ],
104
- calls,
105
- );
106
- const client = new HonorOfKingsApiClient({ fetch });
107
- const result = await client.fetchSnapshot(CRED);
108
- expect(result.account).toEqual({ uid: "900000001", displayName: "小明" });
109
- const profile = result.events.find((e) => e.kind === "profile");
110
- expect(profile).toMatchObject({ uid: "900000001", nickname: "小明", level: 30, rank: "钻石", avatarUrl: "u.png" });
111
- const plays = result.events.filter((e) => e.kind === "play");
112
- expect(plays).toHaveLength(2);
113
- expect(plays[0]).toMatchObject({ durationMs: 900000, mode: "排位赛", startAt: 1700000000000 });
114
- expect(plays[1]).toMatchObject({ durationMs: 1200000, mode: "巅峰赛", startAt: 1700003600000 });
115
- // every request carried the OAuth auth fields.
116
- for (const c of calls) {
117
- expect(c.body.accessToken).toBe("tok-abc");
118
- expect(c.body.openid).toBe("OPENID12345678");
119
- expect(c.body.acctype).toBe("qc");
120
- expect(c.headers["Content-Type"]).toContain("application/json");
121
- }
122
- });
123
-
124
- it("handles alternate envelope/field names (result + roleInfo + battleList)", async () => {
125
- const calls = [];
126
- const fetch = makeFetch(
127
- [
128
- {
129
- match: "/play/profildetail",
130
- body: { result: 0, data: { roleInfo: { uid: "777", nickName: "阿强", roleLevel: 15, gradeName: "黄金" } } },
131
- },
132
- {
133
- match: "/game/getbattlelist",
134
- body: { result: 0, data: { battleList: [{ gameId: "b9", battleTime: 1700000000, gameDuration: 600, gameName: "5v5" }] } },
135
- },
136
- ],
137
- calls,
138
- );
139
- const client = new HonorOfKingsApiClient({ fetch });
140
- const result = await client.fetchSnapshot(CRED);
141
- expect(result.account.displayName).toBe("阿强");
142
- const profile = result.events.find((e) => e.kind === "profile");
143
- expect(profile).toMatchObject({ uid: "777", level: 15, rank: "黄金" });
144
- const play = result.events.find((e) => e.kind === "play");
145
- expect(play).toMatchObject({ durationMs: 600000, mode: "5v5" });
146
- });
147
-
148
- it("include.play:false skips the battle-list call", async () => {
149
- const calls = [];
150
- const fetch = makeFetch(
151
- [{ match: "/play/profildetail", body: { returnCode: 0, data: { role: { roleId: "1", roleName: "x" } } } }],
152
- calls,
153
- );
154
- const client = new HonorOfKingsApiClient({ fetch });
155
- const result = await client.fetchSnapshot(CRED, { include: { play: false } });
156
- expect(calls.some((c) => c.url.includes("getbattlelist"))).toBe(false);
157
- expect(result.events.every((e) => e.kind !== "play")).toBe(true);
158
- });
159
-
160
- it("maps non-zero returnCode to null + lastError (e.g. token expired)", async () => {
161
- const calls = [];
162
- const fetch = makeFetch([{ match: "/play/profildetail", body: { returnCode: 1001, returnMsg: "登录态失效" } }], calls);
163
- const client = new HonorOfKingsApiClient({ fetch });
164
- expect(await client.fetchSnapshot(CRED)).toBeNull();
165
- expect(client.lastError.code).toBe(1001);
166
- expect(client.lastError.message).toContain("登录态失效");
167
- });
168
-
169
- it("missing accessToken/openid → null + lastError -1 (no network)", async () => {
170
- const calls = [];
171
- const client = new HonorOfKingsApiClient({ fetch: makeFetch([], calls) });
172
- expect(await client.fetchSnapshot({ openid: "x" })).toBeNull();
173
- expect(client.lastError.code).toBe(-1);
174
- expect(calls).toHaveLength(0);
175
- });
176
-
177
- it("HTTP non-2xx → null + lastError with status", async () => {
178
- const calls = [];
179
- const fetch = makeFetch([{ match: "/play/profildetail", status: 500, body: "err" }], calls);
180
- const client = new HonorOfKingsApiClient({ fetch });
181
- expect(await client.fetchSnapshot(CRED)).toBeNull();
182
- expect(client.lastError.code).toBe(500);
183
- });
184
- });
185
-
186
- describe("HonorOfKingsAdapter — credential (live) sync mode", () => {
187
- it("authenticate accepts a credential bundle; rejects incomplete", async () => {
188
- const a = new HonorOfKingsAdapter();
189
- expect((await a.authenticate({ credential: CRED })).mode).toBe("camp-token");
190
- expect((await a.authenticate({ credential: { openid: "x" } })).ok).toBe(false);
191
- });
192
-
193
- it("version/capabilities reflect v0.2 live mode", () => {
194
- const a = new HonorOfKingsAdapter();
195
- expect(a.version).toBe("0.2.0");
196
- expect(a.capabilities).toContain("sync:camp-token");
197
- });
198
-
199
- it("sync via credential yields profile+play raws → valid normalized batch", async () => {
200
- const calls = [];
201
- const fetch = makeFetch(
202
- [
203
- { match: "/play/profildetail", body: { returnCode: 0, data: { role: { roleId: "900000001", roleName: "小明", level: 30, rankName: "钻石" } } } },
204
- { match: "/game/getbattlelist", body: { returnCode: 0, data: { list: [{ gameSeq: "g1", startTime: 1700000000, gametime: 1800, mapName: "排位赛" }] } } },
205
- ],
206
- calls,
207
- );
208
- const a = new HonorOfKingsAdapter();
209
- const raws = await collect(a.sync({ credential: CRED, fetch }));
210
- expect(raws.map((r) => r.kind).sort()).toEqual(["play", "profile"]);
211
- const profileRaw = raws.find((r) => r.kind === "profile");
212
- expect(profileRaw.originalId).toBe("hok:profile:profile-900000001");
213
-
214
- for (const raw of raws) {
215
- expect(validateBatch(a.normalize(raw)).valid).toBe(true);
216
- }
217
- const profileBatch = a.normalize(profileRaw);
218
- expect(profileBatch.persons[0].identifiers["hok-uid"]).toEqual(["900000001"]);
219
- expect(profileBatch.persons[0].extra.rank).toBe("钻石");
220
- const playBatch = a.normalize(raws.find((r) => r.kind === "play"));
221
- expect(playBatch.events[0].extra.durationMs).toBe(1800000);
222
- });
223
-
224
- it("sync via credential throws (mapped lastError) on API error", async () => {
225
- const calls = [];
226
- const fetch = makeFetch([{ match: "/play/profildetail", body: { returnCode: 1001, returnMsg: "登录态失效" } }], calls);
227
- const a = new HonorOfKingsAdapter();
228
- await expect(collect(a.sync({ credential: CRED, fetch }))).rejects.toThrow(/登录态失效|code 1001/);
229
- });
230
- });