@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.
- package/lib/forensics/qq-nt-collect.js +190 -0
- package/lib/prompt-builder.js +15 -1
- package/package.json +8 -3
- package/__tests__/adapter-guide.test.js +0 -47
- package/__tests__/adapter-spec.test.js +0 -78
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
- package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
- package/__tests__/adapters/ai-chat-history.test.js +0 -396
- package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
- package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
- package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
- package/__tests__/adapters/apple-health.test.js +0 -95
- package/__tests__/adapters/bank-family.test.js +0 -125
- package/__tests__/adapters/biz-tianyancha.test.js +0 -159
- package/__tests__/adapters/browser-history-chrome.test.js +0 -377
- package/__tests__/adapters/browser-history-edge.test.js +0 -159
- package/__tests__/adapters/car-mercedesme.test.js +0 -74
- package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
- package/__tests__/adapters/doc-camscanner.test.js +0 -147
- package/__tests__/adapters/doc-platforms.test.js +0 -177
- package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
- package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
- package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
- package/__tests__/adapters/email-adapter.test.js +0 -742
- package/__tests__/adapters/email-classifier.test.js +0 -347
- package/__tests__/adapters/email-imap-session.test.js +0 -334
- package/__tests__/adapters/email-parser.test.js +0 -244
- package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
- package/__tests__/adapters/email-providers.test.js +0 -84
- package/__tests__/adapters/email-retry-progress.test.js +0 -294
- package/__tests__/adapters/email-templates.test.js +0 -822
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
- package/__tests__/adapters/finance-alipay-live.test.js +0 -258
- package/__tests__/adapters/finance-dcep.test.js +0 -74
- package/__tests__/adapters/fitness-joyrun.test.js +0 -82
- package/__tests__/adapters/game-genshin-live.test.js +0 -238
- package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
- package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
- package/__tests__/adapters/git-activity.test.js +0 -222
- package/__tests__/adapters/gov-12123.test.js +0 -103
- package/__tests__/adapters/gov-ixiamen.test.js +0 -150
- package/__tests__/adapters/gov-tax.test.js +0 -135
- package/__tests__/adapters/health-meiyou.test.js +0 -125
- package/__tests__/adapters/local-files.test.js +0 -264
- package/__tests__/adapters/local-im-pc.test.js +0 -154
- package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
- package/__tests__/adapters/music-kugou.test.js +0 -187
- package/__tests__/adapters/music-qq.test.js +0 -112
- package/__tests__/adapters/netease-music-live.test.js +0 -244
- package/__tests__/adapters/netease-music.test.js +0 -74
- package/__tests__/adapters/pc-local-discovery.test.js +0 -141
- package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
- package/__tests__/adapters/reading-family.test.js +0 -108
- package/__tests__/adapters/recruit-boss.test.js +0 -180
- package/__tests__/adapters/shell-history.test.js +0 -180
- package/__tests__/adapters/shopping-base.test.js +0 -179
- package/__tests__/adapters/shopping-dianping.test.js +0 -239
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
- package/__tests__/adapters/social-csdn.test.js +0 -175
- package/__tests__/adapters/social-dongchedi.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
- package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
- package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
- package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
- package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
- package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
- package/__tests__/adapters/social-zhihu.test.js +0 -246
- package/__tests__/adapters/system-data-adapter.test.js +0 -443
- package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
- package/__tests__/adapters/system-data-android.test.js +0 -519
- package/__tests__/adapters/system-data-disclosure.test.js +0 -153
- package/__tests__/adapters/travel-12306.test.js +0 -512
- package/__tests__/adapters/travel-amap.test.js +0 -219
- package/__tests__/adapters/travel-baidu-map.test.js +0 -305
- package/__tests__/adapters/travel-base.test.js +0 -205
- package/__tests__/adapters/travel-ctrip.test.js +0 -377
- package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
- package/__tests__/adapters/travel-didi.test.js +0 -204
- package/__tests__/adapters/travel-tencent-map.test.js +0 -207
- package/__tests__/adapters/travel-tongcheng.test.js +0 -289
- package/__tests__/adapters/video-platforms.test.js +0 -152
- package/__tests__/adapters/video-xigua.test.js +0 -106
- package/__tests__/adapters/vscode.test.js +0 -299
- package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
- package/__tests__/adapters/wechat-env-probe.test.js +0 -162
- package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
- package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
- package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
- package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
- package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
- package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
- package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
- package/__tests__/adapters/weread.test.js +0 -123
- package/__tests__/adapters/wework-pc.test.js +0 -124
- package/__tests__/adapters/win-recent.test.js +0 -192
- package/__tests__/analysis-skills.test.js +0 -754
- package/__tests__/analysis.test.js +0 -1845
- package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
- package/__tests__/batch.test.js +0 -133
- package/__tests__/bridges-cc-kg.test.js +0 -231
- package/__tests__/bridges-cc-llm.test.js +0 -191
- package/__tests__/bridges-cc-rag.test.js +0 -162
- package/__tests__/categories.test.js +0 -92
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
- package/__tests__/e2e/full-user-journey.test.js +0 -188
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
- package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
- package/__tests__/entity-resolver-stages.test.js +0 -411
- package/__tests__/entity-resolver-vault.test.js +0 -249
- package/__tests__/entity-resolver.test.js +0 -526
- package/__tests__/fitness-keep-snapshot.test.js +0 -224
- package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
- package/__tests__/ids.test.js +0 -45
- package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
- package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
- package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
- package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
- package/__tests__/key-providers.test.js +0 -126
- package/__tests__/kg-derive.test.js +0 -219
- package/__tests__/llm-client.test.js +0 -122
- package/__tests__/longtail-adapters.test.js +0 -281
- package/__tests__/messaging-qq-snapshot.test.js +0 -294
- package/__tests__/mobile-extractor-encrypted.test.js +0 -460
- package/__tests__/mobile-extractor.test.js +0 -288
- package/__tests__/mock-adapter.test.js +0 -93
- package/__tests__/prompt-builder.test.js +0 -249
- package/__tests__/query-parser.test.js +0 -365
- package/__tests__/rag-derive.test.js +0 -169
- package/__tests__/registry-readiness.test.js +0 -292
- package/__tests__/registry.test.js +0 -420
- package/__tests__/salvage-ingest.test.js +0 -97
- package/__tests__/schemas.test.js +0 -331
- package/__tests__/shopping-adapters.test.js +0 -392
- package/__tests__/shopping-eleme-snapshot.test.js +0 -454
- package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
- package/__tests__/shopping-snapshot.test.js +0 -438
- package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
- package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
- package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
- package/__tests__/sidecar-supervisor.test.js +0 -128
- package/__tests__/sign-providers.test.js +0 -62
- package/__tests__/social-adapters.test.js +0 -280
- package/__tests__/social-bilibili-snapshot.test.js +0 -278
- package/__tests__/social-douban-snapshot.test.js +0 -351
- package/__tests__/social-douyin-im-direct-read.test.js +0 -377
- package/__tests__/social-douyin-salvage-collector.test.js +0 -98
- package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
- package/__tests__/social-douyin-snapshot.test.js +0 -256
- package/__tests__/social-kuaishou-snapshot.test.js +0 -362
- package/__tests__/social-toutiao-snapshot.test.js +0 -366
- package/__tests__/social-weibo-snapshot.test.js +0 -234
- package/__tests__/social-weibo-sqlite-device.test.js +0 -174
- package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
- package/__tests__/sqlite-leaf-salvage.test.js +0 -97
- package/__tests__/travel-adapters.test.js +0 -483
- package/__tests__/travel-maps-snapshot.test.js +0 -426
- package/__tests__/vault-driver-error.test.js +0 -74
- package/__tests__/vault-search-helpers.test.js +0 -104
- package/__tests__/vault-search.test.js +0 -423
- package/__tests__/vault.test.js +0 -767
- package/__tests__/wechat-adapter.test.js +0 -594
- package/__tests__/whatsapp-adapter.test.js +0 -138
- package/scripts/_make-fixture-all.js +0 -126
- package/scripts/_make-fixture-contacts.js +0 -84
- package/scripts/evaluate-entity-resolver.js +0 -213
- package/scripts/run-native-tests-sandbox.sh +0 -55
- package/scripts/smoke-phase-5-5.js +0 -196
- package/scripts/smoke-phase-5-7.js +0 -181
- package/scripts/smoke-system-data-contacts.js +0 -309
- package/scripts/smoke-system-data.js +0 -312
- package/vitest.config.js +0 -88
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
AlipayBillAdapter,
|
|
7
|
-
mapAlipayTypeToSubtype,
|
|
8
|
-
parseAlipayDateTime,
|
|
9
|
-
} = require("../../lib/adapters/alipay-bill/alipay-bill-adapter");
|
|
10
|
-
const {
|
|
11
|
-
parseAlipayCsv,
|
|
12
|
-
parseAlipayCsvBuffer,
|
|
13
|
-
decodeBuffer,
|
|
14
|
-
splitCsvLine,
|
|
15
|
-
FIELD_ORDER,
|
|
16
|
-
} = require("../../lib/adapters/alipay-bill/csv-parser");
|
|
17
|
-
const {
|
|
18
|
-
classifyCounterparty,
|
|
19
|
-
counterpartyToPersonId,
|
|
20
|
-
normalizeCounterpartyName,
|
|
21
|
-
KNOWN_MERCHANTS,
|
|
22
|
-
} = require("../../lib/adapters/alipay-bill/counterparty");
|
|
23
|
-
const { assertAdapter } = require("../../lib/adapter-spec");
|
|
24
|
-
|
|
25
|
-
// ─── CSV parser ─────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
describe("csv-parser — splitCsvLine", () => {
|
|
28
|
-
it("simple comma-separated line", () => {
|
|
29
|
-
expect(splitCsvLine("a,b,c")).toEqual(["a", "b", "c"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("Alipay row with empty fields", () => {
|
|
33
|
-
expect(splitCsvLine("2024,,,d")).toEqual(["2024", "", "", "d"]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("quoted field with comma inside", () => {
|
|
37
|
-
expect(splitCsvLine('a,"b, c",d')).toEqual(["a", "b, c", "d"]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("doubled-quote escape", () => {
|
|
41
|
-
expect(splitCsvLine('a,"b""c",d')).toEqual(["a", 'b"c', "d"]);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("csv-parser — decodeBuffer", () => {
|
|
46
|
-
it("UTF-8 with BOM strips BOM and matches Alipay header", () => {
|
|
47
|
-
const text = "支付宝交易记录明细查询\n交易号,商家订单号";
|
|
48
|
-
const buf = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(text, "utf-8")]);
|
|
49
|
-
const r = decodeBuffer(buf);
|
|
50
|
-
expect(r.encoding).toBe("utf-8");
|
|
51
|
-
expect(r.text).toContain("支付宝交易记录");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("UTF-8 without BOM still detects Alipay magic header", () => {
|
|
55
|
-
const buf = Buffer.from("交易号,商家订单号", "utf-8");
|
|
56
|
-
const r = decodeBuffer(buf);
|
|
57
|
-
expect(r.encoding).toBe("utf-8");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("falls back to GBK when text is not valid UTF-8", () => {
|
|
61
|
-
// Use injected iconv stub to avoid pulling the dep
|
|
62
|
-
const fakeBuf = Buffer.from([0x80, 0x81, 0x82]); // not valid Alipay-ish UTF-8
|
|
63
|
-
const r = decodeBuffer(fakeBuf, {
|
|
64
|
-
iconvImpl: (buf, enc) => `<gbk-decoded ${enc} ${buf.length}b>`,
|
|
65
|
-
});
|
|
66
|
-
expect(r.encoding).toBe("gbk");
|
|
67
|
-
expect(r.text).toContain("gbk-decoded");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("throws on non-Buffer input", () => {
|
|
71
|
-
expect(() => decodeBuffer("not-a-buffer")).toThrow(/Buffer/);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const SAMPLE_CSV = [
|
|
76
|
-
"支付宝交易记录明细查询",
|
|
77
|
-
"账号:[user@example.com]",
|
|
78
|
-
"起始日期:[2024-04-01 00:00:00] 终止日期:[2024-05-01 00:00:00]",
|
|
79
|
-
"---------------------------------交易记录明细列表------------------------------",
|
|
80
|
-
"交易号,商家订单号,交易创建时间,付款时间,最近修改时间,交易来源地,类型,交易对方,商品名称,金额(元),收/支,交易状态,服务费(元),成功退款(元),备注,资金状态",
|
|
81
|
-
"2024040122001112345678,T20240401XXXX,2024-04-01 09:23:11,2024-04-01 09:23:13,2024-04-01 09:23:13,支付宝网站,即时到账交易,美团,美团外卖订单,38.50,支出,交易成功,0.00,0.00,,已支出",
|
|
82
|
-
"2024040522001112345679,,2024-04-05 14:00:00,2024-04-05 14:00:02,2024-04-05 14:00:02,客户端,转账,张三,生日礼物,500.00,支出,交易成功,0.00,0.00,生日快乐,已支出",
|
|
83
|
-
"2024041022001112345680,REFUND123,2024-04-10 10:00:00,2024-04-10 10:00:05,2024-04-10 10:00:05,支付宝网站,退款,淘宝,运动鞋退款,299.00,收入,退款成功,0.00,299.00,,已收入",
|
|
84
|
-
"---------------------------------交易记录明细列表结束------------------------------",
|
|
85
|
-
"导出时间:[2024-05-02 09:00:00] 用户姓名:[张三]",
|
|
86
|
-
].join("\n");
|
|
87
|
-
|
|
88
|
-
describe("csv-parser — parseAlipayCsv", () => {
|
|
89
|
-
it("parses header metadata + 3 rows from a valid CSV", () => {
|
|
90
|
-
const r = parseAlipayCsv(SAMPLE_CSV);
|
|
91
|
-
expect(r.header.account).toBe("user@example.com");
|
|
92
|
-
expect(r.header.startDate).toBe("2024-04-01 00:00:00");
|
|
93
|
-
expect(r.header.endDate).toBe("2024-05-01 00:00:00");
|
|
94
|
-
expect(r.rows).toHaveLength(3);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("first row has all 16 fields populated correctly", () => {
|
|
98
|
-
const r = parseAlipayCsv(SAMPLE_CSV);
|
|
99
|
-
const row = r.rows[0];
|
|
100
|
-
expect(row.txId).toBe("2024040122001112345678");
|
|
101
|
-
expect(row.merchantOrderNumber).toBe("T20240401XXXX");
|
|
102
|
-
expect(row.counterparty).toBe("美团");
|
|
103
|
-
expect(row.itemName).toBe("美团外卖订单");
|
|
104
|
-
expect(row.amount).toBe("38.50");
|
|
105
|
-
expect(row.direction).toBe("支出");
|
|
106
|
-
expect(row.status).toBe("交易成功");
|
|
107
|
-
expect(row.fundStatus).toBe("已支出");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("transfer row preserves note", () => {
|
|
111
|
-
const r = parseAlipayCsv(SAMPLE_CSV);
|
|
112
|
-
expect(r.rows[1].counterparty).toBe("张三");
|
|
113
|
-
expect(r.rows[1].note).toBe("生日快乐");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("refund row direction = 收入", () => {
|
|
117
|
-
const r = parseAlipayCsv(SAMPLE_CSV);
|
|
118
|
-
expect(r.rows[2].direction).toBe("收入");
|
|
119
|
-
expect(r.rows[2].refundedAmount).toBe("299.00");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("returns empty rows with warning when header missing", () => {
|
|
123
|
-
const r = parseAlipayCsv("no header here\njust some text");
|
|
124
|
-
expect(r.rows).toEqual([]);
|
|
125
|
-
expect(r.warning).toContain("header row");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("stops at the terminator marker", () => {
|
|
129
|
-
const r = parseAlipayCsv(SAMPLE_CSV);
|
|
130
|
-
// The "导出时间:[...]" trailer line is OUTSIDE the data list — must not be a row
|
|
131
|
-
for (const row of r.rows) {
|
|
132
|
-
expect(row.txId.startsWith("2024")).toBe(true);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("skips rows with too few commas", () => {
|
|
137
|
-
const csv = SAMPLE_CSV.replace(
|
|
138
|
-
"2024040522001112345679,,2024-04-05 14:00:00,2024-04-05 14:00:02,2024-04-05 14:00:02,客户端,转账,张三,生日礼物,500.00,支出,交易成功,0.00,0.00,生日快乐,已支出",
|
|
139
|
-
"garbage,line,with,few,commas",
|
|
140
|
-
);
|
|
141
|
-
const r = parseAlipayCsv(csv);
|
|
142
|
-
expect(r.rows).toHaveLength(2); // the original 3 minus the broken one
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("returns empty rows for empty input", () => {
|
|
146
|
-
expect(parseAlipayCsv("")).toEqual({ header: {}, rows: [] });
|
|
147
|
-
expect(parseAlipayCsv(null)).toEqual({ header: {}, rows: [] });
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("csv-parser — parseAlipayCsvBuffer", () => {
|
|
152
|
-
it("decodes UTF-8 + parses end-to-end", () => {
|
|
153
|
-
const buf = Buffer.from(SAMPLE_CSV, "utf-8");
|
|
154
|
-
const r = parseAlipayCsvBuffer(buf);
|
|
155
|
-
expect(r.encoding).toBe("utf-8");
|
|
156
|
-
expect(r.rows).toHaveLength(3);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ─── counterparty classifier ────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
describe("counterparty — classifyCounterparty", () => {
|
|
163
|
-
it("recognizes well-known merchants (substring)", () => {
|
|
164
|
-
expect(classifyCounterparty("美团")).toBe("merchant");
|
|
165
|
-
expect(classifyCounterparty("美团外卖")).toBe("merchant");
|
|
166
|
-
expect(classifyCounterparty("淘宝")).toBe("merchant");
|
|
167
|
-
expect(classifyCounterparty("天猫超市")).toBe("merchant");
|
|
168
|
-
expect(classifyCounterparty("12306")).toBe("merchant");
|
|
169
|
-
expect(classifyCounterparty("星巴克咖啡")).toBe("merchant");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("recognizes merchant suffix heuristic", () => {
|
|
173
|
-
expect(classifyCounterparty("北京三联书店")).toBe("merchant");
|
|
174
|
-
expect(classifyCounterparty("XX 科技有限公司")).toBe("merchant");
|
|
175
|
-
expect(classifyCounterparty("华润万家超市")).toBe("merchant");
|
|
176
|
-
expect(classifyCounterparty("普仁医院")).toBe("merchant");
|
|
177
|
-
expect(classifyCounterparty("中通快递")).toBe("merchant");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("classifies 2-4 char Chinese names as contact", () => {
|
|
181
|
-
expect(classifyCounterparty("张三")).toBe("contact");
|
|
182
|
-
expect(classifyCounterparty("李小明")).toBe("contact");
|
|
183
|
-
expect(classifyCounterparty("欧阳娜娜")).toBe("contact");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("strips contact-info brackets before classifying", () => {
|
|
187
|
-
expect(classifyCounterparty("张三(186****1234)")).toBe("contact");
|
|
188
|
-
expect(classifyCounterparty("王五(北京)")).toBe("contact");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("returns unknown for anything that doesn't fit", () => {
|
|
192
|
-
expect(classifyCounterparty("ABC123")).toBe("unknown");
|
|
193
|
-
expect(classifyCounterparty("")).toBe("unknown");
|
|
194
|
-
expect(classifyCounterparty(null)).toBe("unknown");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("KNOWN_MERCHANTS includes >= 80 entries (broad coverage)", () => {
|
|
198
|
-
expect(KNOWN_MERCHANTS.size).toBeGreaterThanOrEqual(80);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("counterparty — counterpartyToPersonId", () => {
|
|
203
|
-
it("returns same id for same name (idempotent)", () => {
|
|
204
|
-
const a = counterpartyToPersonId("美团");
|
|
205
|
-
const b = counterpartyToPersonId("美团");
|
|
206
|
-
expect(a).toBe(b);
|
|
207
|
-
expect(a.startsWith("person-alipay-")).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("different names → different ids", () => {
|
|
211
|
-
expect(counterpartyToPersonId("淘宝")).not.toBe(counterpartyToPersonId("京东"));
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("strips parens before slugifying", () => {
|
|
215
|
-
const a = counterpartyToPersonId("张三(186****1234)");
|
|
216
|
-
const b = counterpartyToPersonId("张三");
|
|
217
|
-
expect(a).toBe(b);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe("normalizeCounterpartyName", () => {
|
|
222
|
-
it("strips parens / asterisks", () => {
|
|
223
|
-
expect(normalizeCounterpartyName("张三(186****1234)")).toBe("张三");
|
|
224
|
-
expect(normalizeCounterpartyName("公司***北京")).toBe("公司北京");
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// ─── subtype mapping ───────────────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
describe("mapAlipayTypeToSubtype", () => {
|
|
231
|
-
it("transfer / refund / investment keywords", () => {
|
|
232
|
-
expect(mapAlipayTypeToSubtype("转账给好友", "支出")).toBe("transfer");
|
|
233
|
-
expect(mapAlipayTypeToSubtype("退款", "收入")).toBe("refund");
|
|
234
|
-
expect(mapAlipayTypeToSubtype("余额宝转入", "支出")).toBe("investment");
|
|
235
|
-
expect(mapAlipayTypeToSubtype("理财申购", "支出")).toBe("investment");
|
|
236
|
-
expect(mapAlipayTypeToSubtype("红包", "支出")).toBe("redenvelope");
|
|
237
|
-
expect(mapAlipayTypeToSubtype("缴费", "支出")).toBe("utility");
|
|
238
|
-
expect(mapAlipayTypeToSubtype("交易关闭", "支出")).toBe("cancelled");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("default by direction", () => {
|
|
242
|
-
expect(mapAlipayTypeToSubtype("即时到账交易", "支出")).toBe("payment");
|
|
243
|
-
expect(mapAlipayTypeToSubtype("收款", "收入")).toBe("income");
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ─── parseAlipayDateTime ────────────────────────────────────────────────
|
|
248
|
-
|
|
249
|
-
describe("parseAlipayDateTime", () => {
|
|
250
|
-
it("parses 'YYYY-MM-DD HH:MM:SS' to ms epoch", () => {
|
|
251
|
-
const ms = parseAlipayDateTime("2024-04-01 09:23:13");
|
|
252
|
-
const d = new Date(ms);
|
|
253
|
-
expect(d.getFullYear()).toBe(2024);
|
|
254
|
-
expect(d.getMonth()).toBe(3); // April
|
|
255
|
-
expect(d.getDate()).toBe(1);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("returns null on bad input", () => {
|
|
259
|
-
expect(parseAlipayDateTime("")).toBeNull();
|
|
260
|
-
expect(parseAlipayDateTime(null)).toBeNull();
|
|
261
|
-
expect(parseAlipayDateTime("garbage")).toBeNull();
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// ─── AlipayBillAdapter — contract + sync + normalize ───────────────────
|
|
266
|
-
|
|
267
|
-
describe("AlipayBillAdapter contract", () => {
|
|
268
|
-
const a = new AlipayBillAdapter({
|
|
269
|
-
account: { email: "u@example.com" },
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("conforms to PersonalDataAdapter spec", () => {
|
|
273
|
-
const r = assertAdapter(a);
|
|
274
|
-
expect(r.ok).toBe(true);
|
|
275
|
-
if (!r.ok) console.log(r.errors);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("name + version + capabilities + sensitivity", () => {
|
|
279
|
-
expect(a.name).toBe("alipay-bill");
|
|
280
|
-
expect(a.version).toBe("0.1.0");
|
|
281
|
-
expect(a.capabilities).toContain("import:csv-zip");
|
|
282
|
-
expect(a.dataDisclosure.sensitivity).toBe("high");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("authenticate returns ok:true (no server auth)", async () => {
|
|
286
|
-
const r = await a.authenticate();
|
|
287
|
-
expect(r.ok).toBe(true);
|
|
288
|
-
expect(r.account).toBe("u@example.com");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("healthCheck returns ok:true", async () => {
|
|
292
|
-
const r = await a.healthCheck();
|
|
293
|
-
expect(r.ok).toBe(true);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("rejects missing account", () => {
|
|
297
|
-
expect(() => new AlipayBillAdapter()).toThrow();
|
|
298
|
-
expect(() => new AlipayBillAdapter({})).toThrow(/account/);
|
|
299
|
-
expect(() => new AlipayBillAdapter({ account: {} })).toThrow(/email/);
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
describe("AlipayBillAdapter.sync", () => {
|
|
304
|
-
it("returns 0 events when no zipPath/csvPath given (idle)", async () => {
|
|
305
|
-
const a = new AlipayBillAdapter({ account: { email: "u@example.com" } });
|
|
306
|
-
const raws = [];
|
|
307
|
-
for await (const r of a.sync()) raws.push(r);
|
|
308
|
-
expect(raws).toHaveLength(0);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("yields one raw per row when csvPath provided (mocked parser)", async () => {
|
|
312
|
-
// Write a temp CSV file
|
|
313
|
-
const fs = require("node:fs");
|
|
314
|
-
const os = require("node:os");
|
|
315
|
-
const path = require("node:path");
|
|
316
|
-
const tmp = path.join(os.tmpdir(), `alipay-test-${Date.now()}.csv`);
|
|
317
|
-
fs.writeFileSync(tmp, SAMPLE_CSV, "utf-8");
|
|
318
|
-
|
|
319
|
-
const events = [];
|
|
320
|
-
const a = new AlipayBillAdapter({
|
|
321
|
-
account: { email: "u@example.com" },
|
|
322
|
-
// Use real parser; CSV is a valid Alipay export shape
|
|
323
|
-
});
|
|
324
|
-
const raws = [];
|
|
325
|
-
for await (const r of a.sync({
|
|
326
|
-
csvPath: tmp,
|
|
327
|
-
onProgress: (e) => events.push(e.phase),
|
|
328
|
-
})) raws.push(r);
|
|
329
|
-
|
|
330
|
-
expect(raws).toHaveLength(3);
|
|
331
|
-
expect(raws[0].adapter).toBe("alipay-bill");
|
|
332
|
-
expect(raws[0].originalId).toBe("2024040122001112345678");
|
|
333
|
-
expect(events).toContain("opening");
|
|
334
|
-
expect(events).toContain("parsing");
|
|
335
|
-
expect(events).toContain("parsed");
|
|
336
|
-
expect(events).toContain("done");
|
|
337
|
-
expect(events.filter((p) => p === "row")).toHaveLength(3);
|
|
338
|
-
|
|
339
|
-
fs.unlinkSync(tmp);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("uses injected zipExtractor when zipPath provided", async () => {
|
|
343
|
-
const events = [];
|
|
344
|
-
const a = new AlipayBillAdapter({
|
|
345
|
-
account: { email: "u@example.com" },
|
|
346
|
-
zipPassword: "OPENME-mock",
|
|
347
|
-
zipExtractor: async (zipPath, opts) => {
|
|
348
|
-
events.push({ kind: "zip", zipPath, password: opts.password });
|
|
349
|
-
return { buffer: Buffer.from(SAMPLE_CSV, "utf-8"), filename: "test.csv" };
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
const raws = [];
|
|
353
|
-
for await (const r of a.sync({ zipPath: "/fake/path.zip" })) raws.push(r);
|
|
354
|
-
expect(raws).toHaveLength(3);
|
|
355
|
-
expect(events[0].password).toBe("OPENME-mock");
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
describe("AlipayBillAdapter.normalize", () => {
|
|
360
|
-
const a = new AlipayBillAdapter({ account: { email: "u@example.com" } });
|
|
361
|
-
|
|
362
|
-
it("payment event has correct subtype + amount.direction=out", () => {
|
|
363
|
-
const raw = {
|
|
364
|
-
adapter: "alipay-bill",
|
|
365
|
-
originalId: "TX1",
|
|
366
|
-
capturedAt: Date.now(),
|
|
367
|
-
payload: {
|
|
368
|
-
row: {
|
|
369
|
-
txId: "TX1",
|
|
370
|
-
merchantOrderNumber: "MO1",
|
|
371
|
-
createdAt: "2024-04-01 10:00:00",
|
|
372
|
-
paidAt: "2024-04-01 10:00:02",
|
|
373
|
-
lastModifiedAt: "2024-04-01 10:00:02",
|
|
374
|
-
sourceChannel: "支付宝网站",
|
|
375
|
-
alipayType: "即时到账交易",
|
|
376
|
-
counterparty: "美团",
|
|
377
|
-
itemName: "美团外卖订单",
|
|
378
|
-
amount: "38.50",
|
|
379
|
-
direction: "支出",
|
|
380
|
-
status: "交易成功",
|
|
381
|
-
serviceFee: "0.00",
|
|
382
|
-
refundedAmount: "0.00",
|
|
383
|
-
note: "",
|
|
384
|
-
fundStatus: "已支出",
|
|
385
|
-
},
|
|
386
|
-
accountEmail: "u@example.com",
|
|
387
|
-
},
|
|
388
|
-
};
|
|
389
|
-
const batch = a.normalize(raw);
|
|
390
|
-
expect(batch.events).toHaveLength(1);
|
|
391
|
-
const ev = batch.events[0];
|
|
392
|
-
expect(ev.subtype).toBe("payment");
|
|
393
|
-
expect(ev.content.amount.value).toBe(38.5);
|
|
394
|
-
expect(ev.content.amount.direction).toBe("out");
|
|
395
|
-
expect(ev.actor).toBe("person-self");
|
|
396
|
-
expect(ev.source.adapter).toBe("alipay-bill");
|
|
397
|
-
expect(ev.source.originalId).toBe("TX1");
|
|
398
|
-
expect(ev.extra.merchantOrderNumber).toBe("MO1");
|
|
399
|
-
expect(ev.extra.alipayType).toBe("即时到账交易");
|
|
400
|
-
expect(ev.extra.counterpartyKind).toBe("merchant");
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("transfer creates contact person", () => {
|
|
404
|
-
const raw = {
|
|
405
|
-
adapter: "alipay-bill",
|
|
406
|
-
originalId: "TX2",
|
|
407
|
-
capturedAt: Date.now(),
|
|
408
|
-
payload: {
|
|
409
|
-
row: {
|
|
410
|
-
txId: "TX2",
|
|
411
|
-
merchantOrderNumber: "",
|
|
412
|
-
createdAt: "2024-04-05 14:00:00",
|
|
413
|
-
paidAt: "2024-04-05 14:00:02",
|
|
414
|
-
lastModifiedAt: "2024-04-05 14:00:02",
|
|
415
|
-
sourceChannel: "客户端",
|
|
416
|
-
alipayType: "转账",
|
|
417
|
-
counterparty: "张三",
|
|
418
|
-
itemName: "生日礼物",
|
|
419
|
-
amount: "500.00",
|
|
420
|
-
direction: "支出",
|
|
421
|
-
status: "交易成功",
|
|
422
|
-
serviceFee: "0.00",
|
|
423
|
-
refundedAmount: "0.00",
|
|
424
|
-
note: "生日快乐",
|
|
425
|
-
fundStatus: "已支出",
|
|
426
|
-
},
|
|
427
|
-
accountEmail: "u@example.com",
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
const batch = a.normalize(raw);
|
|
431
|
-
expect(batch.events[0].subtype).toBe("transfer");
|
|
432
|
-
expect(batch.events[0].content.text).toBe("生日快乐");
|
|
433
|
-
expect(batch.persons).toHaveLength(1);
|
|
434
|
-
expect(batch.persons[0].subtype).toBe("contact");
|
|
435
|
-
expect(batch.persons[0].names).toContain("张三");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it("refund flips direction to in", () => {
|
|
439
|
-
const raw = {
|
|
440
|
-
adapter: "alipay-bill",
|
|
441
|
-
originalId: "TX3",
|
|
442
|
-
capturedAt: Date.now(),
|
|
443
|
-
payload: {
|
|
444
|
-
row: {
|
|
445
|
-
txId: "TX3", merchantOrderNumber: "REFUND123",
|
|
446
|
-
createdAt: "2024-04-10 10:00:00", paidAt: "2024-04-10 10:00:05",
|
|
447
|
-
lastModifiedAt: "2024-04-10 10:00:05",
|
|
448
|
-
sourceChannel: "支付宝网站", alipayType: "退款",
|
|
449
|
-
counterparty: "淘宝", itemName: "运动鞋退款", amount: "299.00",
|
|
450
|
-
direction: "收入", status: "退款成功",
|
|
451
|
-
serviceFee: "0.00", refundedAmount: "299.00",
|
|
452
|
-
note: "", fundStatus: "已收入",
|
|
453
|
-
},
|
|
454
|
-
accountEmail: "u@example.com",
|
|
455
|
-
},
|
|
456
|
-
};
|
|
457
|
-
const batch = a.normalize(raw);
|
|
458
|
-
expect(batch.events[0].subtype).toBe("refund");
|
|
459
|
-
expect(batch.events[0].content.amount.direction).toBe("in");
|
|
460
|
-
expect(batch.events[0].extra.refundedAmount).toBe(299);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it("cancelled transactions get subtype=cancelled", () => {
|
|
464
|
-
const raw = {
|
|
465
|
-
adapter: "alipay-bill", originalId: "TX4",
|
|
466
|
-
capturedAt: Date.now(),
|
|
467
|
-
payload: {
|
|
468
|
-
row: {
|
|
469
|
-
txId: "TX4", merchantOrderNumber: "",
|
|
470
|
-
createdAt: "2024-04-15 12:00:00", paidAt: "2024-04-15 12:00:00",
|
|
471
|
-
lastModifiedAt: "2024-04-15 12:00:00",
|
|
472
|
-
sourceChannel: "支付宝网站", alipayType: "即时到账交易",
|
|
473
|
-
counterparty: "测试商家", itemName: "test", amount: "100.00",
|
|
474
|
-
direction: "支出", status: "交易关闭",
|
|
475
|
-
serviceFee: "0.00", refundedAmount: "0.00",
|
|
476
|
-
note: "", fundStatus: "冻结",
|
|
477
|
-
},
|
|
478
|
-
accountEmail: "u@example.com",
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
const batch = a.normalize(raw);
|
|
482
|
-
expect(batch.events[0].subtype).toBe("cancelled");
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("unknown counterparty stamps needsResolve=true", () => {
|
|
486
|
-
const raw = {
|
|
487
|
-
adapter: "alipay-bill", originalId: "TX5",
|
|
488
|
-
capturedAt: Date.now(),
|
|
489
|
-
payload: {
|
|
490
|
-
row: {
|
|
491
|
-
txId: "TX5", merchantOrderNumber: "",
|
|
492
|
-
createdAt: "2024-04-20 09:00:00", paidAt: "2024-04-20 09:00:00",
|
|
493
|
-
lastModifiedAt: "2024-04-20 09:00:00",
|
|
494
|
-
sourceChannel: "支付宝网站", alipayType: "即时到账交易",
|
|
495
|
-
counterparty: "ABC123XYZ", itemName: "unclassifiable", amount: "10.00",
|
|
496
|
-
direction: "支出", status: "交易成功",
|
|
497
|
-
serviceFee: "0.00", refundedAmount: "0.00",
|
|
498
|
-
note: "", fundStatus: "已支出",
|
|
499
|
-
},
|
|
500
|
-
accountEmail: "u@example.com",
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
const batch = a.normalize(raw);
|
|
504
|
-
expect(batch.events[0].extra.counterpartyKind).toBe("unknown");
|
|
505
|
-
expect(batch.events[0].extra.needsResolve).toBe(true);
|
|
506
|
-
expect(batch.persons[0].extra.needsResolve).toBe(true);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("creates an Item when itemName is distinct from alipayType", () => {
|
|
510
|
-
const raw = {
|
|
511
|
-
adapter: "alipay-bill", originalId: "TX6",
|
|
512
|
-
capturedAt: Date.now(),
|
|
513
|
-
payload: {
|
|
514
|
-
row: {
|
|
515
|
-
txId: "TX6", merchantOrderNumber: "MO6",
|
|
516
|
-
createdAt: "2024-04-25 09:00:00", paidAt: "2024-04-25 09:00:00",
|
|
517
|
-
lastModifiedAt: "2024-04-25 09:00:00",
|
|
518
|
-
sourceChannel: "支付宝网站", alipayType: "即时到账交易",
|
|
519
|
-
counterparty: "京东", itemName: "iPhone 17 Pro 256GB", amount: "9999.00",
|
|
520
|
-
direction: "支出", status: "交易成功",
|
|
521
|
-
serviceFee: "0.00", refundedAmount: "0.00",
|
|
522
|
-
note: "", fundStatus: "已支出",
|
|
523
|
-
},
|
|
524
|
-
accountEmail: "u@example.com",
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
const batch = a.normalize(raw);
|
|
528
|
-
expect(batch.items).toHaveLength(1);
|
|
529
|
-
expect(batch.items[0].name).toBe("iPhone 17 Pro 256GB");
|
|
530
|
-
expect(batch.items[0].price.value).toBe(9999);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it("rejects missing raw.payload", () => {
|
|
534
|
-
expect(() => a.normalize(null)).toThrow();
|
|
535
|
-
expect(() => a.normalize({})).toThrow();
|
|
536
|
-
expect(() => a.normalize({ payload: {} })).toThrow(/row/);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { AppleHealthAdapter } = require("../../lib/adapters/apple-health");
|
|
6
|
-
const { partitionBatch } = require("../../lib/batch");
|
|
7
|
-
|
|
8
|
-
const XML = [
|
|
9
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
10
|
-
'<HealthData locale="zh_CN">',
|
|
11
|
-
' <Record type="HKQuantityTypeIdentifierStepCount" sourceName="iPhone" unit="count" creationDate="2024-01-15 08:36:00 +0800" startDate="2024-01-15 08:30:00 +0800" endDate="2024-01-15 08:35:00 +0800" value="123"/>',
|
|
12
|
-
' <Record type="HKCategoryTypeIdentifierSleepAnalysis" sourceName="Watch" startDate="2024-01-15 23:00:00 +0800" endDate="2024-01-16 07:00:00 +0800" value="HKCategoryValueSleepAnalysisAsleep"/>',
|
|
13
|
-
' <Workout workoutActivityType="HKWorkoutActivityTypeRunning" duration="30" durationUnit="min" totalDistance="5" totalDistanceUnit="km" startDate="2024-01-15 18:00:00 +0800" endDate="2024-01-15 18:30:00 +0800"/>',
|
|
14
|
-
' <SomethingElse foo="bar"/>',
|
|
15
|
-
"</HealthData>",
|
|
16
|
-
].join("\n");
|
|
17
|
-
|
|
18
|
-
function adapter(xml = XML, { exists = true } = {}) {
|
|
19
|
-
const a = new AppleHealthAdapter();
|
|
20
|
-
a._deps.fs = {
|
|
21
|
-
existsSync: () => exists,
|
|
22
|
-
readFileSync: () => xml,
|
|
23
|
-
accessSync: () => {},
|
|
24
|
-
constants: { R_OK: 4 },
|
|
25
|
-
};
|
|
26
|
-
return a;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function collect(iter) {
|
|
30
|
-
const out = [];
|
|
31
|
-
for await (const r of iter) out.push(r);
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe("AppleHealthAdapter", () => {
|
|
36
|
-
it("readinessOnly → NO_FILE (file-import, not 手机采集)", async () => {
|
|
37
|
-
const a = new AppleHealthAdapter();
|
|
38
|
-
const r = await a.authenticate({ readinessOnly: true });
|
|
39
|
-
expect(r.reason).toBe("NO_FILE");
|
|
40
|
-
expect(a.extractMode).toBe("file-import");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("parses Record + Workout lines, ignores other elements", async () => {
|
|
44
|
-
const raws = await collect(adapter().sync({ inputPath: "/fake/export.xml" }));
|
|
45
|
-
expect(raws.map((r) => r.kind)).toEqual(["record", "record", "workout"]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("normalizes to valid events (metrics → other, workout → trip)", async () => {
|
|
49
|
-
const a = adapter();
|
|
50
|
-
const raws = await collect(a.sync({ inputPath: "/fake/export.xml" }));
|
|
51
|
-
const merged = { events: [], persons: [], places: [], items: [], topics: [] };
|
|
52
|
-
for (const r of raws) {
|
|
53
|
-
const n = a.normalize(r);
|
|
54
|
-
for (const k of Object.keys(merged)) merged[k].push(...n[k]);
|
|
55
|
-
}
|
|
56
|
-
const { valid, invalidReasons } = partitionBatch(merged);
|
|
57
|
-
expect(invalidReasons).toHaveLength(0);
|
|
58
|
-
expect(valid.events).toHaveLength(3);
|
|
59
|
-
const subtypes = valid.events.map((e) => e.subtype).sort();
|
|
60
|
-
expect(subtypes).toEqual(["other", "other", "trip"]);
|
|
61
|
-
const steps = valid.events.find((e) => e.extra.metric === "HKQuantityTypeIdentifierStepCount");
|
|
62
|
-
expect(steps.content.title).toContain("步数");
|
|
63
|
-
expect(steps.content.title).toContain("123");
|
|
64
|
-
const workout = valid.events.find((e) => e.subtype === "trip");
|
|
65
|
-
expect(workout.extra.activityType).toBe("Running");
|
|
66
|
-
expect(workout.content.title).toContain("5km");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("parses the +0800 timezone offset correctly", async () => {
|
|
70
|
-
const a = adapter();
|
|
71
|
-
const raws = await collect(a.sync({ inputPath: "/fake/export.xml" }));
|
|
72
|
-
// 2024-01-15 08:30:00 +0800 == 2024-01-15T00:30:00Z
|
|
73
|
-
expect(raws[0].capturedAt).toBe(Date.parse("2024-01-15T00:30:00Z"));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("respects limit + include", async () => {
|
|
77
|
-
const a = adapter();
|
|
78
|
-
const capped = await collect(a.sync({ inputPath: "/x", limit: 1 }));
|
|
79
|
-
expect(capped).toHaveLength(1);
|
|
80
|
-
const noWorkout = await collect(a.sync({ inputPath: "/x", include: { workout: false } }));
|
|
81
|
-
expect(noWorkout.every((r) => r.kind === "record")).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("emits truncated progress when maxRecords exceeded", async () => {
|
|
85
|
-
const a = adapter();
|
|
86
|
-
const events = [];
|
|
87
|
-
await collect(a.sync({ inputPath: "/x", maxRecords: 1, onProgress: (e) => events.push(e) }));
|
|
88
|
-
expect(events.find((e) => e.phase === "truncated")).toBeTruthy();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("missing file yields nothing", async () => {
|
|
92
|
-
const raws = await collect(adapter(XML, { exists: false }).sync({ inputPath: "/x" }));
|
|
93
|
-
expect(raws).toHaveLength(0);
|
|
94
|
-
});
|
|
95
|
-
});
|