@chainlesschain/personal-data-hub 0.1.0 → 0.2.0
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/__tests__/adapters/ai-chat-history.test.js +395 -0
- package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
- package/__tests__/adapters/ai-chat-vendors.test.js +733 -0
- package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
- package/__tests__/adapters/email-adapter.test.js +138 -1
- package/__tests__/adapters/email-classifier.test.js +347 -0
- package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
- package/__tests__/adapters/email-retry-progress.test.js +294 -0
- package/__tests__/adapters/email-templates.test.js +699 -0
- package/__tests__/adapters/system-data-adapter.test.js +440 -0
- package/__tests__/adapters/system-data-disclosure.test.js +153 -0
- package/__tests__/analysis-skills.test.js +409 -0
- package/__tests__/entity-resolver-ingest-hook.test.js +177 -0
- package/__tests__/entity-resolver-stages.test.js +411 -0
- package/__tests__/entity-resolver-vault.test.js +246 -0
- package/__tests__/entity-resolver.test.js +526 -0
- package/__tests__/fixtures/entity-resolver-200-mock.json +96 -0
- package/__tests__/longtail-adapters.test.js +217 -0
- package/__tests__/mobile-extractor.test.js +288 -0
- package/__tests__/shopping-adapters.test.js +296 -0
- package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
- package/__tests__/sidecar-supervisor.test.js +120 -0
- package/__tests__/social-adapters.test.js +206 -0
- package/__tests__/travel-adapters.test.js +325 -0
- package/__tests__/vault.test.js +3 -3
- package/__tests__/wechat-adapter.test.js +476 -0
- package/__tests__/whatsapp-adapter.test.js +135 -0
- package/lib/adapter-spec.js +12 -0
- package/lib/adapters/_python-sidecar-base.js +207 -0
- package/lib/adapters/ai-chat-history/ai-chat-adapter.js +335 -0
- package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
- package/lib/adapters/ai-chat-history/http-client.js +211 -0
- package/lib/adapters/ai-chat-history/index.js +28 -0
- package/lib/adapters/ai-chat-history/schema-map.js +221 -0
- package/lib/adapters/ai-chat-history/vendor-spec.js +85 -0
- package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
- package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
- package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
- package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
- package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
- package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
- package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
- package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
- package/lib/adapters/alipay-bill/alipay-bill-adapter.js +307 -0
- package/lib/adapters/alipay-bill/counterparty.js +129 -0
- package/lib/adapters/alipay-bill/csv-parser.js +217 -0
- package/lib/adapters/alipay-bill/index.js +41 -0
- package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
- package/lib/adapters/email-imap/classifier.js +495 -0
- package/lib/adapters/email-imap/email-adapter.js +419 -8
- package/lib/adapters/email-imap/index.js +42 -0
- package/lib/adapters/email-imap/pdf-extractor.js +192 -0
- package/lib/adapters/email-imap/templates/bill.js +232 -0
- package/lib/adapters/email-imap/templates/government.js +120 -0
- package/lib/adapters/email-imap/templates/index.js +78 -0
- package/lib/adapters/email-imap/templates/order.js +186 -0
- package/lib/adapters/email-imap/templates/other.js +114 -0
- package/lib/adapters/email-imap/templates/register.js +113 -0
- package/lib/adapters/email-imap/templates/travel.js +157 -0
- package/lib/adapters/email-imap/templates/utils.js +275 -0
- package/lib/adapters/email-imap/transactions.js +234 -0
- package/lib/adapters/messaging-qq/index.js +158 -0
- package/lib/adapters/messaging-telegram/index.js +142 -0
- package/lib/adapters/messaging-whatsapp/index.js +189 -0
- package/lib/adapters/shopping-base/index.js +208 -0
- package/lib/adapters/shopping-jd/index.js +150 -0
- package/lib/adapters/shopping-meituan/index.js +154 -0
- package/lib/adapters/shopping-taobao/index.js +176 -0
- package/lib/adapters/social-bilibili/index.js +171 -0
- package/lib/adapters/social-douyin/index.js +116 -0
- package/lib/adapters/social-weibo/index.js +164 -0
- package/lib/adapters/social-xiaohongshu/index.js +96 -0
- package/lib/adapters/system-data/disclosure.js +166 -0
- package/lib/adapters/system-data/index.js +34 -0
- package/lib/adapters/system-data/system-data-adapter.js +344 -0
- package/lib/adapters/travel-12306/index.js +151 -0
- package/lib/adapters/travel-amap/index.js +164 -0
- package/lib/adapters/travel-baidu-map/index.js +162 -0
- package/lib/adapters/travel-base/index.js +240 -0
- package/lib/adapters/travel-ctrip/index.js +151 -0
- package/lib/adapters/wechat/content-parser.js +326 -0
- package/lib/adapters/wechat/db-reader.js +209 -0
- package/lib/adapters/wechat/index.js +28 -0
- package/lib/adapters/wechat/key-extractor.js +158 -0
- package/lib/adapters/wechat/normalize.js +220 -0
- package/lib/adapters/wechat/wechat-adapter.js +205 -0
- package/lib/analysis-skills/base.js +113 -0
- package/lib/analysis-skills/footprint.js +167 -0
- package/lib/analysis-skills/index.js +58 -0
- package/lib/analysis-skills/interests.js +161 -0
- package/lib/analysis-skills/relations.js +226 -0
- package/lib/analysis-skills/spending.js +216 -0
- package/lib/analysis-skills/timeline.js +167 -0
- package/lib/entity-resolver/embedding-stage.js +198 -0
- package/lib/entity-resolver/entity-resolver.js +384 -0
- package/lib/entity-resolver/index.js +42 -0
- package/lib/entity-resolver/llm-stage.js +191 -0
- package/lib/entity-resolver/rule-stage.js +208 -0
- package/lib/entity-resolver/worker.js +149 -0
- package/lib/index.js +115 -0
- package/lib/migrations.js +73 -0
- package/lib/mobile-extractor/android.js +193 -0
- package/lib/mobile-extractor/index.js +9 -0
- package/lib/mobile-extractor/ios.js +223 -0
- package/lib/registry.js +42 -0
- package/lib/sidecar/index.js +15 -0
- package/lib/sidecar/supervisor.js +359 -0
- package/lib/vault.js +266 -0
- package/package.json +29 -3
- package/scripts/_make-fixture-all.js +126 -0
- package/scripts/_make-fixture-contacts.js +84 -0
- package/scripts/evaluate-entity-resolver.js +213 -0
- package/scripts/smoke-phase-5-5.js +196 -0
- package/scripts/smoke-phase-5-7.js +181 -0
- package/scripts/smoke-system-data-contacts.js +309 -0
- package/scripts/smoke-system-data.js +312 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
|
|
9
|
+
const { BilibiliAdapter, WeiboAdapter } = require("../lib");
|
|
10
|
+
const { assertAdapter } = require("../lib/adapter-spec");
|
|
11
|
+
const { validateBatch } = require("../lib/batch");
|
|
12
|
+
|
|
13
|
+
function makeMockDriver(scriptedRows) {
|
|
14
|
+
return function () {
|
|
15
|
+
return {
|
|
16
|
+
prepare(sql) {
|
|
17
|
+
return {
|
|
18
|
+
all() {
|
|
19
|
+
for (const [matchSubstr, rows] of scriptedRows) {
|
|
20
|
+
if (sql.includes(matchSubstr)) return rows;
|
|
21
|
+
}
|
|
22
|
+
throw new Error("no such table");
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
close() {},
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── BilibiliAdapter ────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("BilibiliAdapter", () => {
|
|
34
|
+
it("contract conformance", () => {
|
|
35
|
+
const a = new BilibiliAdapter({ account: { uid: "1234" } });
|
|
36
|
+
expect(assertAdapter(a).ok).toBe(true);
|
|
37
|
+
expect(a.extractMode).toBe("device-pull");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects missing account.uid", () => {
|
|
41
|
+
expect(() => new BilibiliAdapter({})).toThrow();
|
|
42
|
+
expect(() => new BilibiliAdapter({ account: {} })).toThrow(/uid/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("sync yields history + favourite records via mocked driver", async () => {
|
|
46
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bili-"));
|
|
47
|
+
const dbPath = path.join(dir, "bili.db");
|
|
48
|
+
fs.writeFileSync(dbPath, "fake");
|
|
49
|
+
try {
|
|
50
|
+
const mockDriver = makeMockDriver([
|
|
51
|
+
["FROM history", [
|
|
52
|
+
{ id: 1, bvid: "BV1abc", title: "趣味视频", view_at: 1700000000, uploader: "UpA" },
|
|
53
|
+
{ id: 2, bvid: "BV1xyz", title: "教程", view_at: 1700000010, uploader: "UpB", duration: 300 },
|
|
54
|
+
]],
|
|
55
|
+
["FROM bili_favourite", [
|
|
56
|
+
{ id: 1, bvid: "BV1fav", title: "收藏A", save_time: 1700001000, folder_name: "学习" },
|
|
57
|
+
]],
|
|
58
|
+
]);
|
|
59
|
+
const a = new BilibiliAdapter({
|
|
60
|
+
account: { uid: "1234" },
|
|
61
|
+
dbPath,
|
|
62
|
+
dbDriverFactory: () => mockDriver,
|
|
63
|
+
});
|
|
64
|
+
const raws = [];
|
|
65
|
+
for await (const r of a.sync()) raws.push(r);
|
|
66
|
+
expect(raws.length).toBe(3); // 2 history + 1 favourite
|
|
67
|
+
const histories = raws.filter((r) => r.payload.kind === "history");
|
|
68
|
+
const favs = raws.filter((r) => r.payload.kind === "favourite");
|
|
69
|
+
expect(histories).toHaveLength(2);
|
|
70
|
+
expect(favs).toHaveLength(1);
|
|
71
|
+
|
|
72
|
+
// Normalize each
|
|
73
|
+
for (const raw of raws) {
|
|
74
|
+
const batch = a.normalize(raw);
|
|
75
|
+
const v = validateBatch(batch);
|
|
76
|
+
expect(v.valid).toBe(true);
|
|
77
|
+
const subtype = batch.events[0].subtype;
|
|
78
|
+
if (raw.payload.kind === "history") expect(subtype).toBe("browse");
|
|
79
|
+
if (raw.payload.kind === "favourite") expect(subtype).toBe("like");
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("idle when DB path missing", async () => {
|
|
87
|
+
const a = new BilibiliAdapter({ account: { uid: "1234" } });
|
|
88
|
+
const raws = [];
|
|
89
|
+
for await (const r of a.sync()) raws.push(r);
|
|
90
|
+
expect(raws).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("normalize captures bvid/avid/uploader into extra", async () => {
|
|
94
|
+
const a = new BilibiliAdapter({ account: { uid: "1234" } });
|
|
95
|
+
const raw = {
|
|
96
|
+
adapter: "social-bilibili",
|
|
97
|
+
originalId: "history-1",
|
|
98
|
+
capturedAt: 1700000000000,
|
|
99
|
+
payload: {
|
|
100
|
+
kind: "history",
|
|
101
|
+
row: {
|
|
102
|
+
id: 1, bvid: "BV1abc", avid: "1234",
|
|
103
|
+
title: "Test", view_at: 1700000000,
|
|
104
|
+
uploader: "UpA", duration: 300,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const batch = a.normalize(raw);
|
|
109
|
+
expect(batch.events[0].extra.bvid).toBe("BV1abc");
|
|
110
|
+
expect(batch.events[0].extra.avid).toBe("1234");
|
|
111
|
+
expect(batch.events[0].extra.uploader).toBe("UpA");
|
|
112
|
+
expect(batch.events[0].extra.duration).toBe(300);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── WeiboAdapter ───────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
describe("WeiboAdapter", () => {
|
|
119
|
+
it("contract conformance", () => {
|
|
120
|
+
const a = new WeiboAdapter({ account: { uid: "1234" } });
|
|
121
|
+
expect(assertAdapter(a).ok).toBe(true);
|
|
122
|
+
expect(a.extractMode).toBe("device-pull");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects missing account.uid", () => {
|
|
126
|
+
expect(() => new WeiboAdapter({})).toThrow();
|
|
127
|
+
expect(() => new WeiboAdapter({ account: {} })).toThrow(/uid/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("sync yields posts + search records via mocked driver", async () => {
|
|
131
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "weibo-"));
|
|
132
|
+
const dbPath = path.join(dir, "weibo.db");
|
|
133
|
+
fs.writeFileSync(dbPath, "fake");
|
|
134
|
+
try {
|
|
135
|
+
const mockDriver = makeMockDriver([
|
|
136
|
+
["FROM post", [
|
|
137
|
+
{ id: 1, mid: "M1", text: "今天天气真好", created_at: 1700000000, reposts_count: 5, comments_count: 3 },
|
|
138
|
+
]],
|
|
139
|
+
["FROM status", []],
|
|
140
|
+
["FROM search_history", [
|
|
141
|
+
{ id: 1, keyword: "iPhone", time: 1700001000 },
|
|
142
|
+
{ id: 2, keyword: "音乐", time: 1700001100 },
|
|
143
|
+
]],
|
|
144
|
+
]);
|
|
145
|
+
const a = new WeiboAdapter({
|
|
146
|
+
account: { uid: "1234" },
|
|
147
|
+
dbPath,
|
|
148
|
+
dbDriverFactory: () => mockDriver,
|
|
149
|
+
});
|
|
150
|
+
const raws = [];
|
|
151
|
+
for await (const r of a.sync()) raws.push(r);
|
|
152
|
+
expect(raws.length).toBe(3); // 1 post + 2 searches
|
|
153
|
+
|
|
154
|
+
for (const raw of raws) {
|
|
155
|
+
const batch = a.normalize(raw);
|
|
156
|
+
const v = validateBatch(batch);
|
|
157
|
+
expect(v.valid).toBe(true);
|
|
158
|
+
const subtype = batch.events[0].subtype;
|
|
159
|
+
if (raw.payload.kind === "post") expect(subtype).toBe("post");
|
|
160
|
+
if (raw.payload.kind === "search") expect(subtype).toBe("interaction");
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("normalize captures post metrics", async () => {
|
|
168
|
+
const a = new WeiboAdapter({ account: { uid: "1234" } });
|
|
169
|
+
const raw = {
|
|
170
|
+
adapter: "social-weibo",
|
|
171
|
+
originalId: "post-M1",
|
|
172
|
+
capturedAt: 1700000000000,
|
|
173
|
+
payload: {
|
|
174
|
+
kind: "post",
|
|
175
|
+
row: {
|
|
176
|
+
id: 1, mid: "M1", text: "测试",
|
|
177
|
+
created_at: 1700000000,
|
|
178
|
+
reposts_count: 5, comments_count: 3, attitudes_count: 10,
|
|
179
|
+
location: "上海",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
const batch = a.normalize(raw);
|
|
184
|
+
expect(batch.events[0].extra.weiboMid).toBe("M1");
|
|
185
|
+
expect(batch.events[0].extra.repostsCount).toBe(5);
|
|
186
|
+
expect(batch.events[0].extra.commentsCount).toBe(3);
|
|
187
|
+
expect(batch.events[0].extra.likesCount).toBe(10);
|
|
188
|
+
expect(batch.events[0].extra.location).toBe("上海");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("normalize falls back when text is empty", async () => {
|
|
192
|
+
const a = new WeiboAdapter({ account: { uid: "1234" } });
|
|
193
|
+
const raw = {
|
|
194
|
+
adapter: "social-weibo",
|
|
195
|
+
originalId: "post-x",
|
|
196
|
+
capturedAt: Date.now(),
|
|
197
|
+
payload: {
|
|
198
|
+
kind: "post",
|
|
199
|
+
row: { id: 1, mid: "X", text: "", created_at: Math.floor(Date.now() / 1000) },
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const batch = a.normalize(raw);
|
|
203
|
+
expect(batch.events[0].content.title).toBe("(空)");
|
|
204
|
+
expect(validateBatch(batch).valid).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
normalizeTravelRecord,
|
|
11
|
+
parseChineseDateTime,
|
|
12
|
+
Train12306Adapter,
|
|
13
|
+
CtripAdapter,
|
|
14
|
+
AmapAdapter,
|
|
15
|
+
BaiduMapAdapter,
|
|
16
|
+
} = require("../lib");
|
|
17
|
+
const { parseRecords: parse12306 } = require("../lib/adapters/travel-12306");
|
|
18
|
+
const { parseRecords: parseCtripRecords, TYPE_MAP: CTRIP_TYPE_MAP } = require("../lib/adapters/travel-ctrip");
|
|
19
|
+
const { assertAdapter } = require("../lib/adapter-spec");
|
|
20
|
+
const { validateBatch } = require("../lib/batch");
|
|
21
|
+
|
|
22
|
+
// ─── travel-base ────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("parseChineseDateTime", () => {
|
|
25
|
+
it("YYYY-MM-DD HH:MM:SS", () => {
|
|
26
|
+
const ms = parseChineseDateTime("2026-04-15 14:30:00");
|
|
27
|
+
expect(new Date(ms).getFullYear()).toBe(2026);
|
|
28
|
+
expect(new Date(ms).getMonth()).toBe(3);
|
|
29
|
+
expect(new Date(ms).getDate()).toBe(15);
|
|
30
|
+
expect(new Date(ms).getHours()).toBe(14);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("YYYY/MM/DD HH:MM", () => {
|
|
34
|
+
const ms = parseChineseDateTime("2026/04/15 09:00");
|
|
35
|
+
expect(new Date(ms).getMonth()).toBe(3);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("YYYY年M月D日 HH:MM", () => {
|
|
39
|
+
const ms = parseChineseDateTime("2026年4月15日 14:30");
|
|
40
|
+
expect(new Date(ms).getMonth()).toBe(3);
|
|
41
|
+
expect(new Date(ms).getDate()).toBe(15);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("YYYY年M月D日 (no time)", () => {
|
|
45
|
+
const ms = parseChineseDateTime("2026年4月15日");
|
|
46
|
+
expect(new Date(ms).getMonth()).toBe(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("invalid input → null", () => {
|
|
50
|
+
expect(parseChineseDateTime("")).toBeNull();
|
|
51
|
+
expect(parseChineseDateTime(null)).toBeNull();
|
|
52
|
+
expect(parseChineseDateTime("notadate")).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("normalizeTravelRecord", () => {
|
|
57
|
+
it("produces Event + Places + carrier Person", () => {
|
|
58
|
+
const rec = {
|
|
59
|
+
vendorId: "12306",
|
|
60
|
+
recordId: "ORDER-1",
|
|
61
|
+
vehicleType: "train",
|
|
62
|
+
from: { station: "上海虹桥", city: "上海" },
|
|
63
|
+
to: { station: "北京南", city: "北京" },
|
|
64
|
+
departureMs: new Date(2026, 3, 15, 9).getTime(),
|
|
65
|
+
arrivalMs: new Date(2026, 3, 15, 14).getTime(),
|
|
66
|
+
carrier: "12306",
|
|
67
|
+
vehicleNumber: "G2",
|
|
68
|
+
totalCost: { value: 553.5, currency: "CNY" },
|
|
69
|
+
traveler: "张三",
|
|
70
|
+
confirmationCode: "E123456789",
|
|
71
|
+
};
|
|
72
|
+
const b = normalizeTravelRecord(rec, { adapterName: "travel-12306", adapterVersion: "0.5.0" });
|
|
73
|
+
expect(b.events).toHaveLength(1);
|
|
74
|
+
expect(b.events[0].subtype).toBe("trip");
|
|
75
|
+
expect(b.events[0].content.title).toContain("train:");
|
|
76
|
+
expect(b.events[0].content.amount.value).toBe(553.5);
|
|
77
|
+
expect(b.events[0].extra.vehicleNumber).toBe("G2");
|
|
78
|
+
expect(b.events[0].extra.arrivalMs).toBe(rec.arrivalMs);
|
|
79
|
+
expect(b.places).toHaveLength(2);
|
|
80
|
+
expect(b.persons.some((p) => p.subtype === "merchant")).toBe(true);
|
|
81
|
+
const v = validateBatch(b);
|
|
82
|
+
expect(v.valid).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("missing record id throws", () => {
|
|
86
|
+
expect(() => normalizeTravelRecord({})).toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("traveler that matches selfName not added as Person", () => {
|
|
90
|
+
const b = normalizeTravelRecord(
|
|
91
|
+
{ vendorId: "12306", recordId: "X", vehicleType: "train", traveler: "自己" },
|
|
92
|
+
{ adapterName: "travel-12306", selfName: "自己" },
|
|
93
|
+
);
|
|
94
|
+
expect(b.persons.find((p) => p.extra && p.extra.role === "traveler")).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── 12306 adapter ──────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("Train12306Adapter", () => {
|
|
101
|
+
it("adapter contract conformance", () => {
|
|
102
|
+
const a = new Train12306Adapter({ account: { username: "test" } });
|
|
103
|
+
const r = assertAdapter(a);
|
|
104
|
+
expect(r.ok).toBe(true);
|
|
105
|
+
if (!r.ok) console.log(r.errors);
|
|
106
|
+
expect(a.extractMode).toBe("file-import");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("parseRecords parses JSON array", () => {
|
|
110
|
+
const json = JSON.stringify([
|
|
111
|
+
{
|
|
112
|
+
orderId: "ORD-1", trainNumber: "G2",
|
|
113
|
+
fromStation: "上海虹桥", toStation: "北京南",
|
|
114
|
+
departureTime: "2026-04-15 09:00:00",
|
|
115
|
+
arrivalTime: "2026-04-15 14:00:00",
|
|
116
|
+
passengerName: "张三", price: "553.5",
|
|
117
|
+
ticketNumber: "T-1",
|
|
118
|
+
seatNumber: "01车05A号", seat: "二等座",
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
const recs = parse12306(json);
|
|
122
|
+
expect(recs).toHaveLength(1);
|
|
123
|
+
expect(recs[0].vehicleType).toBe("train");
|
|
124
|
+
expect(recs[0].vehicleNumber).toBe("G2");
|
|
125
|
+
expect(recs[0].departureMs).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("parseRecords handles JSONL format", () => {
|
|
129
|
+
const jsonl = '{"orderId":"a","trainNumber":"G1","fromStation":"X","toStation":"Y","passengerName":"p","ticketNumber":"t1"}\n{"orderId":"b","trainNumber":"G2","fromStation":"X","toStation":"Y","passengerName":"p","ticketNumber":"t2"}';
|
|
130
|
+
const recs = parse12306(jsonl);
|
|
131
|
+
expect(recs).toHaveLength(2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("sync yields raw events from a file", async () => {
|
|
135
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "12306-"));
|
|
136
|
+
const dataPath = path.join(dir, "12306.json");
|
|
137
|
+
fs.writeFileSync(dataPath, JSON.stringify([
|
|
138
|
+
{ orderId: "x", trainNumber: "G3", fromStation: "上海虹桥", toStation: "北京南", passengerName: "张三", ticketNumber: "tx" },
|
|
139
|
+
]));
|
|
140
|
+
try {
|
|
141
|
+
const a = new Train12306Adapter({ account: { username: "test" }, dataPath });
|
|
142
|
+
const raws = [];
|
|
143
|
+
for await (const r of a.sync()) raws.push(r);
|
|
144
|
+
expect(raws).toHaveLength(1);
|
|
145
|
+
expect(raws[0].adapter).toBe("travel-12306");
|
|
146
|
+
const batch = a.normalize(raws[0]);
|
|
147
|
+
const v = validateBatch(batch);
|
|
148
|
+
expect(v.valid).toBe(true);
|
|
149
|
+
} finally {
|
|
150
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("rejects missing account.username", () => {
|
|
155
|
+
expect(() => new Train12306Adapter({})).toThrow();
|
|
156
|
+
expect(() => new Train12306Adapter({ account: {} })).toThrow(/username/);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─── Ctrip adapter ───────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("CtripAdapter", () => {
|
|
163
|
+
it("adapter contract", () => {
|
|
164
|
+
const a = new CtripAdapter({ account: { email: "test@example.com" } });
|
|
165
|
+
expect(assertAdapter(a).ok).toBe(true);
|
|
166
|
+
expect(a.extractMode).toBe("file-import");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("parseRecords maps Ctrip flight type", () => {
|
|
170
|
+
const json = JSON.stringify([
|
|
171
|
+
{
|
|
172
|
+
orderId: "C-1", type: "flight",
|
|
173
|
+
depCity: "上海", arrCity: "北京",
|
|
174
|
+
flightNumber: "CA1234",
|
|
175
|
+
airline: "中国国际航空",
|
|
176
|
+
departureTime: "2026-04-15 09:00:00",
|
|
177
|
+
arrivalTime: "2026-04-15 11:30:00",
|
|
178
|
+
passengerName: "张三",
|
|
179
|
+
price: 1234.5,
|
|
180
|
+
pnr: "ABC123",
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
const recs = parseCtripRecords(json);
|
|
184
|
+
expect(recs[0].vehicleType).toBe("flight");
|
|
185
|
+
expect(recs[0].vehicleNumber).toBe("CA1234");
|
|
186
|
+
expect(recs[0].carrier).toBe("中国国际航空");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("parseRecords maps Ctrip hotel type", () => {
|
|
190
|
+
const json = JSON.stringify([
|
|
191
|
+
{
|
|
192
|
+
orderId: "H-1", type: "hotel",
|
|
193
|
+
hotelCity: "上海", hotelName: "外滩英迪格酒店",
|
|
194
|
+
checkIn: "2026-04-15", checkOut: "2026-04-17",
|
|
195
|
+
guestName: "张三", price: 1980, nights: 2,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
const recs = parseCtripRecords(json);
|
|
199
|
+
expect(recs[0].vehicleType).toBe("hotel");
|
|
200
|
+
expect(recs[0].carrier).toBe("外滩英迪格酒店");
|
|
201
|
+
expect(recs[0].extras.nights).toBe(2);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("TYPE_MAP covers expected types", () => {
|
|
205
|
+
expect(CTRIP_TYPE_MAP.flight).toBe("flight");
|
|
206
|
+
expect(CTRIP_TYPE_MAP.hotel).toBe("hotel");
|
|
207
|
+
expect(CTRIP_TYPE_MAP.train).toBe("train");
|
|
208
|
+
expect(CTRIP_TYPE_MAP.cruise).toBe("cruise");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ─── Amap adapter (mocked SQLite) ───────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
describe("AmapAdapter", () => {
|
|
215
|
+
it("adapter contract", () => {
|
|
216
|
+
const a = new AmapAdapter({ account: { deviceId: "dev-1" } });
|
|
217
|
+
expect(assertAdapter(a).ok).toBe(true);
|
|
218
|
+
expect(a.extractMode).toBe("device-pull");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("sync yields route + search records via mocked driver", async () => {
|
|
222
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "amap-"));
|
|
223
|
+
const dbPath = path.join(dir, "amap.db");
|
|
224
|
+
fs.writeFileSync(dbPath, "fake");
|
|
225
|
+
|
|
226
|
+
const mockDriver = function (path, opts) {
|
|
227
|
+
return {
|
|
228
|
+
prepare(sql) {
|
|
229
|
+
return {
|
|
230
|
+
all() {
|
|
231
|
+
if (sql.includes("history_route")) {
|
|
232
|
+
return [
|
|
233
|
+
{ id: "r1", from_name: "上海", to_name: "北京", time: 1700000000000, mode: "drive" },
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
if (sql.includes("history_search")) {
|
|
237
|
+
return [
|
|
238
|
+
{ id: "s1", keyword: "外滩", time: 1700000001000, lat: 31.23, lng: 121.49 },
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
throw new Error("no such table");
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
close() {},
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const a = new AmapAdapter({
|
|
251
|
+
account: { deviceId: "dev-1" },
|
|
252
|
+
dbPath,
|
|
253
|
+
dbDriverFactory: () => mockDriver,
|
|
254
|
+
});
|
|
255
|
+
const raws = [];
|
|
256
|
+
for await (const r of a.sync()) raws.push(r);
|
|
257
|
+
expect(raws.length).toBeGreaterThan(0);
|
|
258
|
+
const route = raws.find((r) => r.payload.kind === "route");
|
|
259
|
+
const search = raws.find((r) => r.payload.kind === "search");
|
|
260
|
+
expect(route).toBeDefined();
|
|
261
|
+
expect(search).toBeDefined();
|
|
262
|
+
// Normalize succeeds + validates
|
|
263
|
+
const batch = a.normalize(route);
|
|
264
|
+
const v = validateBatch(batch);
|
|
265
|
+
expect(v.valid).toBe(true);
|
|
266
|
+
} finally {
|
|
267
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ─── BaiduMap adapter (mocked SQLite) ────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
describe("BaiduMapAdapter", () => {
|
|
275
|
+
it("adapter contract", () => {
|
|
276
|
+
const a = new BaiduMapAdapter({ account: { deviceId: "dev-1" } });
|
|
277
|
+
expect(assertAdapter(a).ok).toBe(true);
|
|
278
|
+
expect(a.extractMode).toBe("device-pull");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("sync yields route from mocked driver", async () => {
|
|
282
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bd-"));
|
|
283
|
+
const dbPath = path.join(dir, "baidu.db");
|
|
284
|
+
fs.writeFileSync(dbPath, "fake");
|
|
285
|
+
|
|
286
|
+
const mockDriver = function () {
|
|
287
|
+
return {
|
|
288
|
+
prepare(sql) {
|
|
289
|
+
return {
|
|
290
|
+
all() {
|
|
291
|
+
if (sql.includes("route_history")) {
|
|
292
|
+
return [
|
|
293
|
+
{ _id: 1, start_name: "上海", end_name: "杭州", time: 1700000000, type: "drive" },
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
if (sql.includes("search_history")) {
|
|
297
|
+
return [
|
|
298
|
+
{ _id: 2, key: "西湖", time: 1700000001 },
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
throw new Error("no such table");
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
close() {},
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const a = new BaiduMapAdapter({
|
|
311
|
+
account: { deviceId: "dev-1" },
|
|
312
|
+
dbPath,
|
|
313
|
+
dbDriverFactory: () => mockDriver,
|
|
314
|
+
});
|
|
315
|
+
const raws = [];
|
|
316
|
+
for await (const r of a.sync()) raws.push(r);
|
|
317
|
+
expect(raws.length).toBeGreaterThan(0);
|
|
318
|
+
const batch = a.normalize(raws[0]);
|
|
319
|
+
const v = validateBatch(batch);
|
|
320
|
+
expect(v.valid).toBe(true);
|
|
321
|
+
} finally {
|
|
322
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
package/__tests__/vault.test.js
CHANGED
|
@@ -104,7 +104,7 @@ afterEach(() => {
|
|
|
104
104
|
describe("LocalVault open + migrations", () => {
|
|
105
105
|
it("opens a fresh vault and runs initial migrations", () => {
|
|
106
106
|
freshVault();
|
|
107
|
-
expect(vault.schemaVersion()).toBe(
|
|
107
|
+
expect(vault.schemaVersion()).toBe(2);
|
|
108
108
|
expect(fs.existsSync(vaultPath)).toBe(true);
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -116,7 +116,7 @@ describe("LocalVault open + migrations", () => {
|
|
|
116
116
|
|
|
117
117
|
const reopen = new LocalVault({ path: vaultPath, key, skipAudit: true });
|
|
118
118
|
reopen.open();
|
|
119
|
-
expect(reopen.schemaVersion()).toBe(
|
|
119
|
+
expect(reopen.schemaVersion()).toBe(2);
|
|
120
120
|
expect(reopen.stats().persons).toBe(1);
|
|
121
121
|
reopen.close();
|
|
122
122
|
});
|
|
@@ -494,7 +494,7 @@ describe("LocalVault.stats", () => {
|
|
|
494
494
|
vault.audit("hello");
|
|
495
495
|
|
|
496
496
|
const s = vault.stats();
|
|
497
|
-
expect(s.schemaVersion).toBe(
|
|
497
|
+
expect(s.schemaVersion).toBe(2);
|
|
498
498
|
expect(s.events).toBe(3);
|
|
499
499
|
expect(s.persons).toBe(1);
|
|
500
500
|
expect(s.places).toBe(2);
|