@chainlesschain/personal-data-hub 0.2.1 → 0.2.2
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/wechat-frida-agent.test.js +132 -1
- package/__tests__/integration/social-bilibili-pipeline.test.js +261 -0
- package/__tests__/social-adapters.test.js +63 -14
- package/__tests__/social-bilibili-snapshot.test.js +278 -0
- package/__tests__/wechat-adapter.test.js +118 -0
- package/lib/adapters/social-bilibili/adapter.js +500 -0
- package/lib/adapters/social-bilibili/index.js +21 -169
- package/lib/adapters/wechat/content-parser.js +11 -2
- package/lib/adapters/wechat/db-reader.js +88 -10
- package/lib/adapters/wechat/frida-agent/loader.js +7 -0
- package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +140 -18
- package/lib/adapters/wechat/key-providers/frida-key-provider.js +8 -0
- package/lib/adapters/wechat/normalize.js +12 -3
- package/package.json +1 -1
|
@@ -171,6 +171,134 @@ describe("frida-agent — fallback symbol resolution", () => {
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
describe("frida-agent — sjqz-audit fixes (sig + format + module case)", () => {
|
|
175
|
+
// Helper extending fakePtr with readCString for ascii-hex tests.
|
|
176
|
+
function fakeAsciiHexPtr(asciiHex) {
|
|
177
|
+
return {
|
|
178
|
+
_v: asciiHex,
|
|
179
|
+
toInt32() { return 0; },
|
|
180
|
+
readByteArray(_len) { return new Uint8Array(0).buffer; },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function memoryReadCString(ptr, _maxLen) {
|
|
184
|
+
return ptr && typeof ptr._v === "string" ? ptr._v : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
it("attaches when only uppercase libWCDB.so resolves (sjqz canonical name)", () => {
|
|
188
|
+
const send = vi.fn();
|
|
189
|
+
const Interceptor = { attach: vi.fn() };
|
|
190
|
+
const Module = {
|
|
191
|
+
findExportByName(mod, sym) {
|
|
192
|
+
return mod === "libWCDB.so" && sym === "sqlite3_key"
|
|
193
|
+
? { symbol: sym }
|
|
194
|
+
: null;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const Process = {
|
|
198
|
+
findModuleByName(mod) {
|
|
199
|
+
return mod === "libWCDB.so" ? { name: mod } : null;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
runAgentUnderMock({ Module, Process, Interceptor, send });
|
|
204
|
+
|
|
205
|
+
const hooked = send.mock.calls.find((c) => c[0].kind === "hooked");
|
|
206
|
+
expect(hooked).toBeDefined();
|
|
207
|
+
expect(hooked[0].module).toBe("libWCDB.so");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("v2 hook reads key from args[2] and length from args[3] (not args[1]/[2])", () => {
|
|
211
|
+
const send = vi.fn();
|
|
212
|
+
const attached = {};
|
|
213
|
+
const Interceptor = {
|
|
214
|
+
attach(addr, handlers) { attached[addr.symbol] = handlers; },
|
|
215
|
+
};
|
|
216
|
+
const Module = {
|
|
217
|
+
findExportByName(mod, sym) {
|
|
218
|
+
return sym === "sqlite3_key_v2" ? { symbol: sym } : null;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const Process = { findModuleByName() { return { name: "libwcdb.so" }; } };
|
|
222
|
+
|
|
223
|
+
runAgentUnderMock({ Module, Process, Interceptor, send });
|
|
224
|
+
|
|
225
|
+
// sqlite3_key_v2(sqlite3 *db, const char *zDbName, const void *pKey, int nKey)
|
|
226
|
+
// args[0]=db, args[1]=name, args[2]=keyBytes, args[3]=len
|
|
227
|
+
const dbNamePtr = fakePtr("ffeeffeeffeeffeeffeeffeeffeeffee"); // would be wrong if read as key
|
|
228
|
+
const keyHex = "12345678" + "00".repeat(28); // 32 bytes
|
|
229
|
+
const args = [
|
|
230
|
+
fakePtr(0), // db
|
|
231
|
+
dbNamePtr, // name (NOT the key)
|
|
232
|
+
fakePtr(keyHex), // pKey — correct args[2]
|
|
233
|
+
fakePtr(32), // nKey — correct args[3]
|
|
234
|
+
];
|
|
235
|
+
attached.sqlite3_key_v2.onEnter(args);
|
|
236
|
+
|
|
237
|
+
const keyEvt = send.mock.calls.find((c) => c[0].kind === "key");
|
|
238
|
+
expect(keyEvt).toBeDefined();
|
|
239
|
+
expect(keyEvt[0].hex).toBe(keyHex); // proves args[2] was read, not args[1]
|
|
240
|
+
expect(keyEvt[0].sig).toBe("v2");
|
|
241
|
+
expect(keyEvt[0].format).toBe("raw-bytes");
|
|
242
|
+
expect(keyEvt[0].length).toBe(32);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("reads ascii-hex key via readCString when length === 64", () => {
|
|
246
|
+
const send = vi.fn();
|
|
247
|
+
const attached = {};
|
|
248
|
+
const Interceptor = {
|
|
249
|
+
attach(addr, handlers) { attached[addr.symbol] = handlers; },
|
|
250
|
+
};
|
|
251
|
+
const Module = {
|
|
252
|
+
findExportByName(mod, sym) {
|
|
253
|
+
return sym === "sqlite3_key" ? { symbol: sym } : null;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const Process = { findModuleByName() { return { name: "libwcdb.so" }; } };
|
|
257
|
+
const Memory = { readCString: memoryReadCString };
|
|
258
|
+
|
|
259
|
+
runAgentUnderMock({ Module, Process, Interceptor, send, Memory });
|
|
260
|
+
|
|
261
|
+
// 64-char ASCII hex string + len=64 → readCString path (sjqz scenario)
|
|
262
|
+
const asciiHex = "ABCDEF0123456789".repeat(4).toLowerCase();
|
|
263
|
+
const args = [fakePtr(0), fakeAsciiHexPtr(asciiHex), fakePtr(64)];
|
|
264
|
+
attached.sqlite3_key.onEnter(args);
|
|
265
|
+
|
|
266
|
+
const keyEvt = send.mock.calls.find((c) => c[0].kind === "key");
|
|
267
|
+
expect(keyEvt).toBeDefined();
|
|
268
|
+
expect(keyEvt[0].hex).toBe(asciiHex);
|
|
269
|
+
expect(keyEvt[0].format).toBe("ascii-hex");
|
|
270
|
+
expect(keyEvt[0].length).toBe(64);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("emits unsupported-signature error for mangled C++ symbol (no host attempt)", () => {
|
|
274
|
+
const send = vi.fn();
|
|
275
|
+
const attached = {};
|
|
276
|
+
const Interceptor = {
|
|
277
|
+
attach(addr, handlers) { attached[addr.symbol] = handlers; },
|
|
278
|
+
};
|
|
279
|
+
const mangledSymbol =
|
|
280
|
+
"_ZN4WCDB8Database13setCipherKeyERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE";
|
|
281
|
+
const Module = {
|
|
282
|
+
findExportByName(mod, sym) {
|
|
283
|
+
return sym === mangledSymbol ? { symbol: sym } : null;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const Process = { findModuleByName() { return { name: "libwcdb.so" }; } };
|
|
287
|
+
|
|
288
|
+
runAgentUnderMock({ Module, Process, Interceptor, send });
|
|
289
|
+
|
|
290
|
+
attached[mangledSymbol].onEnter([fakePtr(0), fakePtr("aabb"), fakePtr(32)]);
|
|
291
|
+
|
|
292
|
+
const errEvt = send.mock.calls.find(
|
|
293
|
+
(c) => c[0].kind === "error" && /unsupported symbol signature/.test(c[0].message),
|
|
294
|
+
);
|
|
295
|
+
expect(errEvt).toBeDefined();
|
|
296
|
+
// And NO key event emitted (host must fall back to MD5 path).
|
|
297
|
+
const keyEvt = send.mock.calls.find((c) => c[0].kind === "key");
|
|
298
|
+
expect(keyEvt).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
174
302
|
describe("frida-agent — module not yet loaded path", () => {
|
|
175
303
|
it("emits module-waiting and schedules retry", () => {
|
|
176
304
|
const send = vi.fn();
|
|
@@ -183,7 +311,10 @@ describe("frida-agent — module not yet loaded path", () => {
|
|
|
183
311
|
|
|
184
312
|
const waiting = send.mock.calls.find((c) => c[0].kind === "module-waiting");
|
|
185
313
|
expect(waiting).toBeDefined();
|
|
186
|
-
|
|
314
|
+
// Post-sjqz audit: agent now tries both libWCDB.so (uppercase, sjqz-verified)
|
|
315
|
+
// and libwcdb.so. The module-waiting event surfaces the join so the
|
|
316
|
+
// host telemetry shows both attempted names.
|
|
317
|
+
expect(waiting[0].module).toBe("libWCDB.so|libwcdb.so");
|
|
187
318
|
expect(setTimeoutMock).toHaveBeenCalled();
|
|
188
319
|
// First retry delay 500ms
|
|
189
320
|
expect(setTimeoutMock.mock.calls[0][1]).toBe(500);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration test — A8 v0.1 Bilibili snapshot → vault pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Exercises the full chain WITHOUT any real WebView / OkHttp / Android JNI:
|
|
7
|
+
*
|
|
8
|
+
* Synthetic snapshot JSON (4 kinds)
|
|
9
|
+
* ↓
|
|
10
|
+
* AdapterRegistry (real) + LocalVault (real, SQLCipher)
|
|
11
|
+
* ↓
|
|
12
|
+
* adapter.sync({ inputPath }) → _syncViaSnapshot yields raw events
|
|
13
|
+
* ↓
|
|
14
|
+
* registry.putRawEvent → vault.raw_events
|
|
15
|
+
* ↓
|
|
16
|
+
* adapter.normalize(raw) → batch
|
|
17
|
+
* ↓
|
|
18
|
+
* vault.putBatch → events / persons / places / items / topics tables
|
|
19
|
+
*
|
|
20
|
+
* Two scenarios:
|
|
21
|
+
* A. happy path — 4-kind snapshot ingests; vault yields exact counts;
|
|
22
|
+
* KG triples derive; originalId stable across re-sync (idempotency)
|
|
23
|
+
* B. partial snapshot — only history + follow; vault gets correct subset
|
|
24
|
+
*
|
|
25
|
+
* Win note: bs3mc has a known NODE_MODULE_VERSION mismatch on this dev box
|
|
26
|
+
* (Node 22.22.2 ABI v127 vs prebuild ABI v140); test passes on CI Linux
|
|
27
|
+
* which uses the matched prebuild. See memory pdh-plan-a-android-standalone-
|
|
28
|
+
* design §"bs3mc NODE_MODULE_VERSION mismatch".
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
32
|
+
|
|
33
|
+
const fs = require("node:fs");
|
|
34
|
+
const path = require("node:path");
|
|
35
|
+
const os = require("node:os");
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
LocalVault,
|
|
39
|
+
generateKeyHex,
|
|
40
|
+
AdapterRegistry,
|
|
41
|
+
} = require("../../lib");
|
|
42
|
+
const {
|
|
43
|
+
BilibiliAdapter,
|
|
44
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
45
|
+
} = require("../../lib/adapters/social-bilibili");
|
|
46
|
+
|
|
47
|
+
function makeRig() {
|
|
48
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bili-int-"));
|
|
49
|
+
const vault = new LocalVault({ path: path.join(dir, "v.db"), key: generateKeyHex() });
|
|
50
|
+
vault.open();
|
|
51
|
+
const registry = new AdapterRegistry({ vault });
|
|
52
|
+
return { vault, registry, dir };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cleanup(rig) {
|
|
56
|
+
if (!rig) return;
|
|
57
|
+
try { rig.vault.close(); } catch (_e) { /* noop */ }
|
|
58
|
+
try { fs.rmSync(rig.dir, { recursive: true, force: true }); } catch (_e) { /* noop */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeSnapshot(dir, snapshot) {
|
|
62
|
+
const p = path.join(dir, "social-bilibili.json");
|
|
63
|
+
fs.writeFileSync(p, JSON.stringify(snapshot), "utf-8");
|
|
64
|
+
return p;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sampleSnapshot(opts = {}) {
|
|
68
|
+
const include = {
|
|
69
|
+
history: true,
|
|
70
|
+
favourite: true,
|
|
71
|
+
dynamic: true,
|
|
72
|
+
follow: true,
|
|
73
|
+
...opts.include,
|
|
74
|
+
};
|
|
75
|
+
const events = [];
|
|
76
|
+
if (include.history) {
|
|
77
|
+
events.push({
|
|
78
|
+
kind: "history",
|
|
79
|
+
id: "BV1abc",
|
|
80
|
+
capturedAt: 1715000000000,
|
|
81
|
+
title: "Rust 异步学习",
|
|
82
|
+
bvid: "BV1abc",
|
|
83
|
+
avid: 42,
|
|
84
|
+
duration: 600,
|
|
85
|
+
uploader: "技术UP主",
|
|
86
|
+
uploaderMid: 100,
|
|
87
|
+
part: "01 介绍",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (include.favourite) {
|
|
91
|
+
events.push({
|
|
92
|
+
kind: "favourite",
|
|
93
|
+
id: "fav-BV2def",
|
|
94
|
+
capturedAt: 1714000000000,
|
|
95
|
+
title: "前端架构",
|
|
96
|
+
bvid: "BV2def",
|
|
97
|
+
folderName: "学习",
|
|
98
|
+
uploader: "码农UP",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (include.dynamic) {
|
|
102
|
+
events.push({
|
|
103
|
+
kind: "dynamic",
|
|
104
|
+
id: "dyn-99",
|
|
105
|
+
capturedAt: 1713000000000,
|
|
106
|
+
summary: "今天发了一个新视频",
|
|
107
|
+
dynamicType: "av",
|
|
108
|
+
rid: "99",
|
|
109
|
+
authorMid: 200,
|
|
110
|
+
authorName: "我关注的UP",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (include.follow) {
|
|
114
|
+
events.push({
|
|
115
|
+
kind: "follow",
|
|
116
|
+
id: "follow-300",
|
|
117
|
+
capturedAt: 1712000000000,
|
|
118
|
+
mid: 300,
|
|
119
|
+
uname: "美食UP",
|
|
120
|
+
face: "https://i0.hdslb.com/300.jpg",
|
|
121
|
+
sign: "好吃的视频",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
126
|
+
snapshottedAt: 1716000000000,
|
|
127
|
+
account: { uid: "12345", displayName: "alice" },
|
|
128
|
+
events,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe("Integration — A8 Bilibili snapshot → vault end-to-end", () => {
|
|
133
|
+
let rig;
|
|
134
|
+
afterEach(() => { cleanup(rig); rig = null; });
|
|
135
|
+
|
|
136
|
+
it("4 kinds ingest into vault with exact entity counts", async () => {
|
|
137
|
+
rig = makeRig();
|
|
138
|
+
const adapter = new BilibiliAdapter();
|
|
139
|
+
rig.registry.register(adapter);
|
|
140
|
+
|
|
141
|
+
const snapshotPath = writeSnapshot(rig.dir, sampleSnapshot());
|
|
142
|
+
const report = await rig.registry.syncAdapter("social-bilibili", {
|
|
143
|
+
inputPath: snapshotPath,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 3 events (history + favourite + dynamic) + 1 person (follow) +
|
|
147
|
+
// 2 items (history-video + favourite-video)
|
|
148
|
+
expect(report.status).toBe("ok");
|
|
149
|
+
expect(report.entityCounts.events).toBe(3);
|
|
150
|
+
expect(report.entityCounts.persons).toBe(1);
|
|
151
|
+
expect(report.entityCounts.items).toBe(2);
|
|
152
|
+
|
|
153
|
+
// Vault round-trip
|
|
154
|
+
const events = rig.vault.queryEvents({ limit: 100 });
|
|
155
|
+
expect(events).toHaveLength(3);
|
|
156
|
+
const subtypes = events.map((e) => e.subtype).sort();
|
|
157
|
+
expect(subtypes).toEqual(["browse", "browse", "like"]); // history+dynamic+favourite
|
|
158
|
+
|
|
159
|
+
const persons = rig.vault.queryPersons({ limit: 100 });
|
|
160
|
+
expect(persons).toHaveLength(1);
|
|
161
|
+
expect(persons[0].names[0]).toBe("美食UP");
|
|
162
|
+
|
|
163
|
+
const items = rig.vault.queryItems({ limit: 100 });
|
|
164
|
+
expect(items).toHaveLength(2);
|
|
165
|
+
expect(items.map((i) => i.name).sort()).toEqual(["Rust 异步学习", "前端架构"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("re-sync is idempotent — same snapshot twice does NOT double entities", async () => {
|
|
169
|
+
rig = makeRig();
|
|
170
|
+
const adapter = new BilibiliAdapter();
|
|
171
|
+
rig.registry.register(adapter);
|
|
172
|
+
const snapshotPath = writeSnapshot(rig.dir, sampleSnapshot());
|
|
173
|
+
|
|
174
|
+
// First sync
|
|
175
|
+
const report1 = await rig.registry.syncAdapter("social-bilibili", {
|
|
176
|
+
inputPath: snapshotPath,
|
|
177
|
+
});
|
|
178
|
+
expect(report1.status).toBe("ok");
|
|
179
|
+
|
|
180
|
+
// Second sync — same snapshot
|
|
181
|
+
const report2 = await rig.registry.syncAdapter("social-bilibili", {
|
|
182
|
+
inputPath: snapshotPath,
|
|
183
|
+
});
|
|
184
|
+
expect(report2.status).toBe("ok");
|
|
185
|
+
|
|
186
|
+
// Stable originalId means re-sync de-dups at raw_events layer.
|
|
187
|
+
// The person/item entities should remain at 1 / 2 respectively
|
|
188
|
+
// because their IDs derive from bvid / mid (stable). Events can
|
|
189
|
+
// legitimately double-write because each "browse" is a separate
|
|
190
|
+
// occurrence — registry doesn't dedup events.
|
|
191
|
+
const persons = rig.vault.queryPersons({ limit: 100 });
|
|
192
|
+
expect(persons).toHaveLength(1);
|
|
193
|
+
|
|
194
|
+
const items = rig.vault.queryItems({ limit: 100 });
|
|
195
|
+
// Items with same bvid produce same ID, so item table stays at 2
|
|
196
|
+
// (UPSERT semantics via primary-key id).
|
|
197
|
+
expect(items).toHaveLength(2);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("partial snapshot (history + follow only) yields exact subset", async () => {
|
|
201
|
+
rig = makeRig();
|
|
202
|
+
const adapter = new BilibiliAdapter();
|
|
203
|
+
rig.registry.register(adapter);
|
|
204
|
+
|
|
205
|
+
const snapshotPath = writeSnapshot(
|
|
206
|
+
rig.dir,
|
|
207
|
+
sampleSnapshot({ include: { favourite: false, dynamic: false } })
|
|
208
|
+
);
|
|
209
|
+
const report = await rig.registry.syncAdapter("social-bilibili", {
|
|
210
|
+
inputPath: snapshotPath,
|
|
211
|
+
});
|
|
212
|
+
expect(report.status).toBe("ok");
|
|
213
|
+
expect(report.entityCounts.events).toBe(1); // history only
|
|
214
|
+
expect(report.entityCounts.persons).toBe(1); // follow
|
|
215
|
+
expect(report.entityCounts.items).toBe(1); // history video
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("empty events array → ok status with 0 entity counts", async () => {
|
|
219
|
+
rig = makeRig();
|
|
220
|
+
const adapter = new BilibiliAdapter();
|
|
221
|
+
rig.registry.register(adapter);
|
|
222
|
+
|
|
223
|
+
const snapshotPath = writeSnapshot(rig.dir, {
|
|
224
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
225
|
+
snapshottedAt: Date.now(),
|
|
226
|
+
events: [],
|
|
227
|
+
});
|
|
228
|
+
const report = await rig.registry.syncAdapter("social-bilibili", {
|
|
229
|
+
inputPath: snapshotPath,
|
|
230
|
+
});
|
|
231
|
+
expect(report.status).toBe("ok");
|
|
232
|
+
expect(report.entityCounts.events).toBe(0);
|
|
233
|
+
expect(report.entityCounts.persons).toBe(0);
|
|
234
|
+
expect(report.entityCounts.items).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("schemaVersion mismatch surfaces in SyncReport.error (not silent)", async () => {
|
|
238
|
+
rig = makeRig();
|
|
239
|
+
const adapter = new BilibiliAdapter();
|
|
240
|
+
rig.registry.register(adapter);
|
|
241
|
+
|
|
242
|
+
const snapshotPath = writeSnapshot(rig.dir, {
|
|
243
|
+
schemaVersion: 99, // wrong
|
|
244
|
+
snapshottedAt: Date.now(),
|
|
245
|
+
events: [],
|
|
246
|
+
});
|
|
247
|
+
const report = await rig.registry.syncAdapter("social-bilibili", {
|
|
248
|
+
inputPath: snapshotPath,
|
|
249
|
+
});
|
|
250
|
+
expect(report.status).toBe("error");
|
|
251
|
+
expect(String(report.error)).toMatch(/schemaVersion mismatch/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("registry queryable by adapter.name after register()", () => {
|
|
255
|
+
rig = makeRig();
|
|
256
|
+
const adapter = new BilibiliAdapter();
|
|
257
|
+
rig.registry.register(adapter);
|
|
258
|
+
expect(rig.registry.has("social-bilibili")).toBe(true);
|
|
259
|
+
expect(rig.registry.list().some((m) => m.name === "social-bilibili")).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -37,9 +37,47 @@ describe("BilibiliAdapter", () => {
|
|
|
37
37
|
expect(a.extractMode).toBe("device-pull");
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
it("
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
it("accepts stateless construction (snapshot mode added in A8)", () => {
|
|
41
|
+
// Before A8: constructor required opts.account.uid. After A8 the adapter
|
|
42
|
+
// is stateless when running snapshot mode (in-APK Android cc reads a JSON
|
|
43
|
+
// produced by the phone). Sqlite mode still needs account.uid but the
|
|
44
|
+
// check moved into _syncViaSqlite where it actually matters.
|
|
45
|
+
expect(() => new BilibiliAdapter({})).not.toThrow();
|
|
46
|
+
expect(() => new BilibiliAdapter({ account: {} })).not.toThrow();
|
|
47
|
+
expect(() => new BilibiliAdapter()).not.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("sqlite mode rejects missing account.uid at sync time", async () => {
|
|
51
|
+
const a = new BilibiliAdapter({ dbPath: "/tmp/bili.db" });
|
|
52
|
+
// Path-existence check happens before account.uid validation, so we
|
|
53
|
+
// exercise the guard via dbPath=null + account=null which falls to
|
|
54
|
+
// "sync needs inputPath OR dbPath" first. Use a real-looking dbPath
|
|
55
|
+
// with no account to surface the account.uid throw deterministically.
|
|
56
|
+
const fs = require("node:fs");
|
|
57
|
+
const path = require("node:path");
|
|
58
|
+
const os = require("node:os");
|
|
59
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bili-no-acct-"));
|
|
60
|
+
const dbPath = path.join(dir, "bili.db");
|
|
61
|
+
fs.writeFileSync(dbPath, "fake");
|
|
62
|
+
try {
|
|
63
|
+
const b = new BilibiliAdapter({
|
|
64
|
+
dbPath,
|
|
65
|
+
dbDriverFactory: () => () => ({
|
|
66
|
+
prepare: () => ({ all: () => [] }),
|
|
67
|
+
close() {},
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
let threw = null;
|
|
71
|
+
try {
|
|
72
|
+
for await (const _r of b.sync()) { /* drain */ }
|
|
73
|
+
} catch (err) {
|
|
74
|
+
threw = err;
|
|
75
|
+
}
|
|
76
|
+
expect(threw).toBeTruthy();
|
|
77
|
+
expect(String(threw.message)).toMatch(/account\.uid/);
|
|
78
|
+
} finally {
|
|
79
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
43
81
|
});
|
|
44
82
|
|
|
45
83
|
it("sync yields history + favourite records via mocked driver", async () => {
|
|
@@ -83,26 +121,34 @@ describe("BilibiliAdapter", () => {
|
|
|
83
121
|
}
|
|
84
122
|
});
|
|
85
123
|
|
|
86
|
-
it("
|
|
124
|
+
it("throws when neither inputPath nor dbPath provided (A8: surface config errors)", async () => {
|
|
125
|
+
// Before A8: sync silently yielded 0 if dbPath missing — masked typos and
|
|
126
|
+
// misconfigured callers. After A8 we throw so callers see the problem.
|
|
87
127
|
const a = new BilibiliAdapter({ account: { uid: "1234" } });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
let threw = null;
|
|
129
|
+
try {
|
|
130
|
+
for await (const _r of a.sync()) { /* drain */ }
|
|
131
|
+
} catch (err) {
|
|
132
|
+
threw = err;
|
|
133
|
+
}
|
|
134
|
+
expect(threw).toBeTruthy();
|
|
135
|
+
expect(String(threw.message)).toMatch(/inputPath|dbPath/);
|
|
91
136
|
});
|
|
92
137
|
|
|
93
|
-
it("normalize captures bvid/avid/uploader into extra", async () => {
|
|
138
|
+
it("normalize captures bvid/avid/uploader into extra (flat payload, A8 shape)", async () => {
|
|
94
139
|
const a = new BilibiliAdapter({ account: { uid: "1234" } });
|
|
95
140
|
const raw = {
|
|
96
141
|
adapter: "social-bilibili",
|
|
97
|
-
|
|
142
|
+
kind: "history",
|
|
143
|
+
originalId: "bilibili:history:BV1abc",
|
|
98
144
|
capturedAt: 1700000000000,
|
|
99
145
|
payload: {
|
|
100
146
|
kind: "history",
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
147
|
+
title: "Test",
|
|
148
|
+
bvid: "BV1abc",
|
|
149
|
+
avid: "1234",
|
|
150
|
+
uploader: "UpA",
|
|
151
|
+
duration: 300,
|
|
106
152
|
},
|
|
107
153
|
};
|
|
108
154
|
const batch = a.normalize(raw);
|
|
@@ -110,6 +156,9 @@ describe("BilibiliAdapter", () => {
|
|
|
110
156
|
expect(batch.events[0].extra.avid).toBe("1234");
|
|
111
157
|
expect(batch.events[0].extra.uploader).toBe("UpA");
|
|
112
158
|
expect(batch.events[0].extra.duration).toBe(300);
|
|
159
|
+
// A8: history also yields an item entity (video) for KG linkage
|
|
160
|
+
expect(batch.items).toHaveLength(1);
|
|
161
|
+
expect(batch.items[0].extra.bvid).toBe("BV1abc");
|
|
113
162
|
});
|
|
114
163
|
});
|
|
115
164
|
|