@chainlesschain/personal-data-hub 0.4.3 → 0.4.5
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/edu-huawei-learning-live.test.js +198 -0
- package/__tests__/adapters/edu-zuoyebang-live.test.js +226 -0
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +5 -1
- package/__tests__/adapters/finance-alipay-live.test.js +258 -0
- package/__tests__/adapters/game-genshin-live.test.js +238 -0
- package/__tests__/adapters/game-genshin-scaffold.test.js +4 -3
- package/__tests__/adapters/game-honor-of-kings-live.test.js +230 -0
- package/__tests__/adapters/messaging-whatsapp.test.js +289 -0
- package/__tests__/adapters/netease-music-live.test.js +244 -0
- package/__tests__/adapters/shopping-base.test.js +179 -0
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +165 -0
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +192 -0
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +64 -0
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +11 -0
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +135 -0
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +89 -0
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +95 -2
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +30 -0
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +431 -0
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +200 -0
- package/__tests__/adapters/travel-12306.test.js +279 -0
- package/__tests__/adapters/travel-amap.test.js +219 -0
- package/__tests__/adapters/travel-baidu-map.test.js +305 -0
- package/__tests__/adapters/travel-base.test.js +205 -0
- package/__tests__/adapters/travel-ctrip.test.js +203 -0
- package/__tests__/adapters/travel-tencent-map.test.js +207 -0
- package/lib/adapters/_live-json-helpers.js +50 -0
- package/lib/adapters/edu-huawei-learning/api-client.js +178 -5
- package/lib/adapters/edu-huawei-learning/index.js +83 -9
- package/lib/adapters/edu-zuoyebang/api-client.js +181 -6
- package/lib/adapters/edu-zuoyebang/index.js +83 -9
- package/lib/adapters/finance-alipay/api-client.js +268 -6
- package/lib/adapters/finance-alipay/index.js +85 -9
- package/lib/adapters/game-genshin/api-client.js +207 -6
- package/lib/adapters/game-genshin/index.js +90 -9
- package/lib/adapters/game-honor-of-kings/api-client.js +235 -12
- package/lib/adapters/game-honor-of-kings/index.js +80 -9
- package/lib/adapters/netease-music/api-client.js +284 -0
- package/lib/adapters/netease-music/index.js +85 -9
- package/lib/adapters/social-douyin/index.js +2 -0
- package/lib/adapters/social-douyin-adb/aweme-detail-client.js +119 -0
- package/lib/adapters/social-douyin-adb/collector.js +114 -0
- package/lib/adapters/social-douyin-adb/index.js +18 -1
- package/lib/adapters/social-douyin-adb/watch-history-reader.js +188 -0
- package/lib/adapters/social-kuaishou/index.js +7 -2
- package/lib/adapters/social-kuaishou-adb/api-client.js +38 -18
- package/lib/adapters/social-kuaishou-adb/cookies-extension.js +16 -15
- package/lib/adapters/social-toutiao/index.js +8 -4
- package/lib/adapters/social-toutiao-adb/account-reader.js +179 -0
- package/lib/adapters/social-toutiao-adb/api-client.js +41 -17
- package/lib/adapters/social-toutiao-adb/collector.js +55 -19
- package/lib/adapters/social-toutiao-adb/cookies-extension.js +21 -1
- package/lib/adapters/social-toutiao-adb/index.js +6 -0
- package/lib/adapters/social-xiaohongshu-adb/cookies-extension.js +19 -1
- package/lib/adapters/travel-base/index.js +9 -2
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
Train12306Adapter,
|
|
11
|
+
parseRecords,
|
|
12
|
+
NAME,
|
|
13
|
+
VERSION,
|
|
14
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
15
|
+
VALID_SNAPSHOT_KINDS,
|
|
16
|
+
} = require("../../lib/adapters/travel-12306");
|
|
17
|
+
|
|
18
|
+
function writeTmp(content) {
|
|
19
|
+
const p = path.join(os.tmpdir(), `cc-12306-test-${crypto.randomUUID()}.json`);
|
|
20
|
+
fs.writeFileSync(p, content, "utf-8");
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function collect(gen) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for await (const x of gen) out.push(x);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TICKET_EVENT = {
|
|
31
|
+
kind: "ticket",
|
|
32
|
+
id: "ticket-SEQ1:0",
|
|
33
|
+
capturedAt: 1716383021000,
|
|
34
|
+
orderSequenceNo: "SEQ1",
|
|
35
|
+
ticketNumber: "E123456789",
|
|
36
|
+
passengerName: "张三",
|
|
37
|
+
passengerIdLast6: "123456",
|
|
38
|
+
trainNumber: "G35",
|
|
39
|
+
fromStation: "上海虹桥",
|
|
40
|
+
toStation: "北京南",
|
|
41
|
+
departureMs: 1716383021000,
|
|
42
|
+
arrivalMs: 1716401021000,
|
|
43
|
+
seatTypeName: "二等座",
|
|
44
|
+
coachNo: "05",
|
|
45
|
+
seatNo: "12A",
|
|
46
|
+
ticketPrice: 553.5,
|
|
47
|
+
orderDateMs: 1716000000000,
|
|
48
|
+
orderTotalPrice: 553.5,
|
|
49
|
+
isCompleted: true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function makeSnapshot(events, extra = {}) {
|
|
53
|
+
return JSON.stringify({
|
|
54
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
55
|
+
snapshottedAt: 1716383021000,
|
|
56
|
+
vendor: "12306",
|
|
57
|
+
events,
|
|
58
|
+
...extra,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("constants", () => {
|
|
63
|
+
it("exposes name/version/schema", () => {
|
|
64
|
+
expect(NAME).toBe("travel-12306");
|
|
65
|
+
expect(VERSION).toBe("0.6.0");
|
|
66
|
+
expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
|
|
67
|
+
expect([...VALID_SNAPSHOT_KINDS]).toEqual(["ticket"]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("authenticate", () => {
|
|
72
|
+
it("snapshot mode ok when inputPath readable", async () => {
|
|
73
|
+
const p = writeTmp(makeSnapshot([]));
|
|
74
|
+
try {
|
|
75
|
+
const a = new Train12306Adapter();
|
|
76
|
+
expect(await a.authenticate({ inputPath: p })).toEqual({
|
|
77
|
+
ok: true,
|
|
78
|
+
mode: "snapshot-file",
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
fs.unlinkSync(p);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("snapshot mode fails INPUT_PATH_UNREADABLE on missing file", async () => {
|
|
86
|
+
const a = new Train12306Adapter();
|
|
87
|
+
const r = await a.authenticate({
|
|
88
|
+
inputPath: path.join(os.tmpdir(), "nonexistent-12306.json"),
|
|
89
|
+
});
|
|
90
|
+
expect(r.ok).toBe(false);
|
|
91
|
+
expect(r.reason).toBe("INPUT_PATH_UNREADABLE");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("file-import mode requires account.username", async () => {
|
|
95
|
+
const noAccount = new Train12306Adapter({ dataPath: "x.json" });
|
|
96
|
+
expect((await noAccount.authenticate({})).reason).toBe(
|
|
97
|
+
"NO_ACCOUNT_USERNAME",
|
|
98
|
+
);
|
|
99
|
+
const withAccount = new Train12306Adapter({
|
|
100
|
+
dataPath: "x.json",
|
|
101
|
+
account: { username: "alice" },
|
|
102
|
+
});
|
|
103
|
+
expect(await withAccount.authenticate({})).toEqual({
|
|
104
|
+
ok: true,
|
|
105
|
+
account: "alice",
|
|
106
|
+
mode: "file-import",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("fails NO_INPUT when neither path given", async () => {
|
|
111
|
+
const a = new Train12306Adapter();
|
|
112
|
+
expect((await a.authenticate({})).reason).toBe("NO_INPUT");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("sync — snapshot mode", () => {
|
|
117
|
+
it("yields ticket events with stable originalId", async () => {
|
|
118
|
+
const p = writeTmp(makeSnapshot([TICKET_EVENT]));
|
|
119
|
+
try {
|
|
120
|
+
const a = new Train12306Adapter();
|
|
121
|
+
const items = await collect(a.sync({ inputPath: p }));
|
|
122
|
+
expect(items).toHaveLength(1);
|
|
123
|
+
expect(items[0]).toMatchObject({
|
|
124
|
+
adapter: NAME,
|
|
125
|
+
kind: "ticket",
|
|
126
|
+
originalId: "12306:ticket:ticket-SEQ1:0",
|
|
127
|
+
capturedAt: 1716383021000,
|
|
128
|
+
});
|
|
129
|
+
expect(items[0].payload.snapshot).toBe(true);
|
|
130
|
+
expect(items[0].payload.trainNumber).toBe("G35");
|
|
131
|
+
} finally {
|
|
132
|
+
fs.unlinkSync(p);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("throws on schemaVersion mismatch", async () => {
|
|
137
|
+
const p = writeTmp(JSON.stringify({ schemaVersion: 99, events: [] }));
|
|
138
|
+
try {
|
|
139
|
+
const a = new Train12306Adapter();
|
|
140
|
+
await expect(collect(a.sync({ inputPath: p }))).rejects.toThrow(
|
|
141
|
+
/schemaVersion mismatch/,
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
fs.unlinkSync(p);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("skips unknown kinds + honors include gate + limit", async () => {
|
|
149
|
+
const p = writeTmp(
|
|
150
|
+
makeSnapshot([
|
|
151
|
+
{ kind: "alien", id: "x" },
|
|
152
|
+
TICKET_EVENT,
|
|
153
|
+
{ ...TICKET_EVENT, id: "ticket-SEQ1:1" },
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
try {
|
|
157
|
+
const a = new Train12306Adapter();
|
|
158
|
+
const all = await collect(a.sync({ inputPath: p }));
|
|
159
|
+
expect(all).toHaveLength(2);
|
|
160
|
+
const limited = await collect(a.sync({ inputPath: p, limit: 1 }));
|
|
161
|
+
expect(limited).toHaveLength(1);
|
|
162
|
+
const gated = await collect(
|
|
163
|
+
a.sync({ inputPath: p, include: { ticket: false } }),
|
|
164
|
+
);
|
|
165
|
+
expect(gated).toHaveLength(0);
|
|
166
|
+
} finally {
|
|
167
|
+
fs.unlinkSync(p);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("falls back capturedAt to departureMs then snapshottedAt", async () => {
|
|
172
|
+
const noCaptured = { ...TICKET_EVENT, capturedAt: undefined };
|
|
173
|
+
const noTimes = {
|
|
174
|
+
...TICKET_EVENT,
|
|
175
|
+
capturedAt: undefined,
|
|
176
|
+
departureMs: undefined,
|
|
177
|
+
};
|
|
178
|
+
const p = writeTmp(makeSnapshot([noCaptured, noTimes]));
|
|
179
|
+
try {
|
|
180
|
+
const a = new Train12306Adapter();
|
|
181
|
+
const items = await collect(a.sync({ inputPath: p }));
|
|
182
|
+
expect(items[0].capturedAt).toBe(TICKET_EVENT.departureMs);
|
|
183
|
+
expect(items[1].capturedAt).toBe(1716383021000); // snapshottedAt
|
|
184
|
+
} finally {
|
|
185
|
+
fs.unlinkSync(p);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("normalize — snapshot payload", () => {
|
|
191
|
+
it("maps ticket event to trip with traveler/cost/seat extras", () => {
|
|
192
|
+
const a = new Train12306Adapter();
|
|
193
|
+
const batch = a.normalize({
|
|
194
|
+
payload: { ...TICKET_EVENT, snapshot: true },
|
|
195
|
+
});
|
|
196
|
+
const ev = batch.events[0];
|
|
197
|
+
expect(ev.subtype).toBe("trip");
|
|
198
|
+
expect(ev.content.title).toBe("train: 上海虹桥 → 北京南");
|
|
199
|
+
expect(ev.content.amount).toEqual({
|
|
200
|
+
value: 553.5,
|
|
201
|
+
currency: "CNY",
|
|
202
|
+
direction: "out",
|
|
203
|
+
});
|
|
204
|
+
expect(ev.extra.vehicleNumber).toBe("G35");
|
|
205
|
+
expect(ev.extra.confirmationCode).toBe("E123456789");
|
|
206
|
+
expect(ev.extra.vendorExtras).toMatchObject({
|
|
207
|
+
seat: "二等座",
|
|
208
|
+
coachNo: "05",
|
|
209
|
+
seatNumber: "12A",
|
|
210
|
+
isCompleted: true,
|
|
211
|
+
idLast6: "123456",
|
|
212
|
+
});
|
|
213
|
+
const traveler = batch.persons.find((p) => p.subtype === "contact");
|
|
214
|
+
expect(traveler.names).toEqual(["张三"]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("throws on missing payload / record", () => {
|
|
218
|
+
const a = new Train12306Adapter();
|
|
219
|
+
expect(() => a.normalize(null)).toThrow(/payload missing/);
|
|
220
|
+
expect(() => a.normalize({ payload: {} })).toThrow(
|
|
221
|
+
/payload\.record missing/,
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("sync + parseRecords — file-import mode", () => {
|
|
227
|
+
const ORDER = {
|
|
228
|
+
orderId: "O1",
|
|
229
|
+
fromStation: "上海虹桥",
|
|
230
|
+
toStation: "北京南",
|
|
231
|
+
departureTime: 1716383021000,
|
|
232
|
+
trainNumber: "G35",
|
|
233
|
+
price: "553.5",
|
|
234
|
+
passengerName: "张三",
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
it("requires account.username at sync time", async () => {
|
|
238
|
+
const a = new Train12306Adapter({ dataPath: "whatever.json" });
|
|
239
|
+
await expect(collect(a.sync({}))).rejects.toThrow(
|
|
240
|
+
/account\.username required/,
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("parses JSON array / {orders} / JSONL shapes", () => {
|
|
245
|
+
expect(parseRecords(JSON.stringify([ORDER]))).toHaveLength(1);
|
|
246
|
+
expect(parseRecords(JSON.stringify({ orders: [ORDER] }))).toHaveLength(1);
|
|
247
|
+
const jsonl = `${JSON.stringify(ORDER)}\n# comment\n${JSON.stringify({ ...ORDER, orderId: "O2" })}`;
|
|
248
|
+
const recs = parseRecords(jsonl);
|
|
249
|
+
expect(recs.map((r) => r.recordId)).toEqual(["O1", "O2"]);
|
|
250
|
+
expect(recs[0].vehicleType).toBe("train");
|
|
251
|
+
expect(recs[0].totalCost).toEqual({ value: 553.5, currency: "CNY" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("drops orders without any id", () => {
|
|
255
|
+
expect(parseRecords(JSON.stringify([{ price: "1" }]))).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("yields from a real dump file end-to-end", async () => {
|
|
259
|
+
const p = writeTmp(JSON.stringify([ORDER]));
|
|
260
|
+
try {
|
|
261
|
+
const a = new Train12306Adapter({
|
|
262
|
+
dataPath: p,
|
|
263
|
+
account: { username: "alice" },
|
|
264
|
+
});
|
|
265
|
+
const items = await collect(a.sync({}));
|
|
266
|
+
expect(items).toHaveLength(1);
|
|
267
|
+
expect(items[0].originalId).toBe("O1");
|
|
268
|
+
const batch = a.normalize(items[0]);
|
|
269
|
+
expect(batch.events[0].content.title).toBe("train: 上海虹桥 → 北京南");
|
|
270
|
+
} finally {
|
|
271
|
+
fs.unlinkSync(p);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("sync throws when neither inputPath nor dataPath", async () => {
|
|
276
|
+
const a = new Train12306Adapter();
|
|
277
|
+
await expect(collect(a.sync({}))).rejects.toThrow(/needs opts\.inputPath/);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
|
|
9
|
+
const { AmapAdapter, NAME, VERSION } = require("../../lib/adapters/travel-amap");
|
|
10
|
+
|
|
11
|
+
function writeTmpDb() {
|
|
12
|
+
// sync() only checks fs.existsSync before handing the path to the
|
|
13
|
+
// injected driver — content is irrelevant for the fake.
|
|
14
|
+
const p = path.join(os.tmpdir(), `cc-amap-test-${crypto.randomUUID()}.db`);
|
|
15
|
+
fs.writeFileSync(p, "fake");
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function collect(gen) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for await (const x of gen) out.push(x);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fake better-sqlite3 driver. `tables` maps a SQL substring (table name)
|
|
27
|
+
* to rows; unmatched prepare() throws like sqlite does on missing tables
|
|
28
|
+
* so trySelect's fallback chain is exercised.
|
|
29
|
+
*/
|
|
30
|
+
function makeFakeDriverFactory(tables, log = {}) {
|
|
31
|
+
return () =>
|
|
32
|
+
class FakeDb {
|
|
33
|
+
constructor(dbPath, opts) {
|
|
34
|
+
log.opened = { dbPath, opts };
|
|
35
|
+
}
|
|
36
|
+
prepare(sql) {
|
|
37
|
+
for (const [needle, rows] of Object.entries(tables)) {
|
|
38
|
+
if (sql.includes(needle)) return { all: () => rows };
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`no such table in: ${sql}`);
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
log.closed = true;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ROUTE_ROW = {
|
|
49
|
+
id: 7,
|
|
50
|
+
mode: "drive",
|
|
51
|
+
from_name: "家",
|
|
52
|
+
to_name: "公司",
|
|
53
|
+
from_lat: 31.23,
|
|
54
|
+
from_lng: 121.47,
|
|
55
|
+
to_lat: 31.2,
|
|
56
|
+
to_lng: 121.44,
|
|
57
|
+
time: 1716383021, // seconds — must be upgraded to ms
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SEARCH_ROW = {
|
|
61
|
+
id: 9,
|
|
62
|
+
keyword: "咖啡店",
|
|
63
|
+
city: "上海",
|
|
64
|
+
lat: 31.22,
|
|
65
|
+
lng: 121.45,
|
|
66
|
+
time: 1716383021000, // already ms
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe("constants", () => {
|
|
70
|
+
it("exposes name/version", () => {
|
|
71
|
+
expect(NAME).toBe("travel-amap");
|
|
72
|
+
expect(VERSION).toBe("0.6.0");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("authenticate", () => {
|
|
77
|
+
it("ready mode when no db path", async () => {
|
|
78
|
+
const a = new AmapAdapter();
|
|
79
|
+
expect(await a.authenticate({})).toEqual({
|
|
80
|
+
ok: true,
|
|
81
|
+
account: null,
|
|
82
|
+
mode: "ready",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("snapshot-file mode when dbPath exists", async () => {
|
|
87
|
+
const p = writeTmpDb();
|
|
88
|
+
try {
|
|
89
|
+
const a = new AmapAdapter({ account: { deviceId: "DEV1" } });
|
|
90
|
+
expect(await a.authenticate({ dbPath: p })).toEqual({
|
|
91
|
+
ok: true,
|
|
92
|
+
account: "DEV1",
|
|
93
|
+
mode: "snapshot-file",
|
|
94
|
+
});
|
|
95
|
+
} finally {
|
|
96
|
+
fs.unlinkSync(p);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("sync — fake sqlite driver", () => {
|
|
102
|
+
it("yields route + search records, opens readonly, closes db", async () => {
|
|
103
|
+
const p = writeTmpDb();
|
|
104
|
+
const log = {};
|
|
105
|
+
try {
|
|
106
|
+
const a = new AmapAdapter({
|
|
107
|
+
dbPath: p,
|
|
108
|
+
dbDriverFactory: makeFakeDriverFactory(
|
|
109
|
+
{ history_route: [ROUTE_ROW], history_search: [SEARCH_ROW] },
|
|
110
|
+
log,
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
const items = await collect(a.sync({}));
|
|
114
|
+
expect(items).toHaveLength(2);
|
|
115
|
+
|
|
116
|
+
const route = items.find((i) => i.payload.kind === "route");
|
|
117
|
+
expect(route.originalId).toBe("route-7");
|
|
118
|
+
expect(route.payload.record).toMatchObject({
|
|
119
|
+
vendorId: "amap",
|
|
120
|
+
vehicleType: "car", // mode=drive
|
|
121
|
+
carrier: "高德地图",
|
|
122
|
+
departureMs: 1716383021 * 1000, // seconds upgraded to ms
|
|
123
|
+
});
|
|
124
|
+
expect(route.payload.record.from.name).toBe("家");
|
|
125
|
+
expect(route.payload.record.to.name).toBe("公司");
|
|
126
|
+
|
|
127
|
+
const search = items.find((i) => i.payload.kind === "search");
|
|
128
|
+
expect(search.originalId).toBe("search-9");
|
|
129
|
+
expect(search.payload.record).toMatchObject({
|
|
130
|
+
vehicleType: "visit",
|
|
131
|
+
departureMs: 1716383021000, // ms passthrough
|
|
132
|
+
});
|
|
133
|
+
expect(search.payload.record.to).toMatchObject({
|
|
134
|
+
name: "咖啡店",
|
|
135
|
+
city: "上海",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(log.opened.opts).toEqual({ readonly: true });
|
|
139
|
+
expect(log.closed).toBe(true);
|
|
140
|
+
} finally {
|
|
141
|
+
fs.unlinkSync(p);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("falls back to legacy ROUTE_HISTORY table name", async () => {
|
|
146
|
+
const p = writeTmpDb();
|
|
147
|
+
try {
|
|
148
|
+
const a = new AmapAdapter({
|
|
149
|
+
dbPath: p,
|
|
150
|
+
dbDriverFactory: makeFakeDriverFactory({
|
|
151
|
+
ROUTE_HISTORY: [ROUTE_ROW],
|
|
152
|
+
history_search: [],
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
const items = await collect(a.sync({}));
|
|
156
|
+
expect(items).toHaveLength(1);
|
|
157
|
+
expect(items[0].payload.kind).toBe("route");
|
|
158
|
+
} finally {
|
|
159
|
+
fs.unlinkSync(p);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("skips rows without any id, tolerates missing tables entirely", async () => {
|
|
164
|
+
const p = writeTmpDb();
|
|
165
|
+
try {
|
|
166
|
+
const a = new AmapAdapter({
|
|
167
|
+
dbPath: p,
|
|
168
|
+
dbDriverFactory: makeFakeDriverFactory({
|
|
169
|
+
history_route: [{ mode: "walk" }], // no id → dropped
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
expect(await collect(a.sync({}))).toEqual([]);
|
|
173
|
+
} finally {
|
|
174
|
+
fs.unlinkSync(p);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("returns silently when db file missing", async () => {
|
|
179
|
+
const a = new AmapAdapter({
|
|
180
|
+
dbPath: path.join(os.tmpdir(), "nonexistent-amap.db"),
|
|
181
|
+
dbDriverFactory: makeFakeDriverFactory({}),
|
|
182
|
+
});
|
|
183
|
+
expect(await collect(a.sync({}))).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("normalize", () => {
|
|
188
|
+
it("route record → trip event with place coordinates", async () => {
|
|
189
|
+
const p = writeTmpDb();
|
|
190
|
+
try {
|
|
191
|
+
const a = new AmapAdapter({
|
|
192
|
+
dbPath: p,
|
|
193
|
+
dbDriverFactory: makeFakeDriverFactory({
|
|
194
|
+
history_route: [ROUTE_ROW],
|
|
195
|
+
history_search: [],
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
const [item] = await collect(a.sync({}));
|
|
199
|
+
const batch = a.normalize(item);
|
|
200
|
+
const ev = batch.events[0];
|
|
201
|
+
expect(ev.subtype).toBe("trip");
|
|
202
|
+
expect(ev.content.title).toBe("car: 家 → 公司");
|
|
203
|
+
expect(ev.source.adapter).toBe(NAME);
|
|
204
|
+
expect(batch.places).toHaveLength(2);
|
|
205
|
+
expect(batch.places[0].coordinates).toEqual({ lat: 31.23, lng: 121.47 });
|
|
206
|
+
const merchant = batch.persons.find((x) => x.subtype === "merchant");
|
|
207
|
+
expect(merchant.names).toEqual(["高德地图"]);
|
|
208
|
+
} finally {
|
|
209
|
+
fs.unlinkSync(p);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("throws on missing record", () => {
|
|
214
|
+
const a = new AmapAdapter();
|
|
215
|
+
expect(() => a.normalize({ payload: {} })).toThrow(
|
|
216
|
+
/payload\.record missing/,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
});
|