@chainlesschain/personal-data-hub 0.4.29 → 0.4.30
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/prompt-builder.js +15 -1
- package/package.json +4 -1
- 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,288 +0,0 @@
|
|
|
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
|
-
const { AndroidExtractor, iOSBackupReader } = require("../lib/mobile-extractor");
|
|
9
|
-
|
|
10
|
-
// ─── AndroidExtractor — mocked execFn ────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
function mockExec(scriptMap) {
|
|
13
|
-
// scriptMap: { "args joined by space": { stdout: "...", stderr: "..." }
|
|
14
|
-
// | () => Promise<{stdout, stderr}> }
|
|
15
|
-
return async (cmd, args) => {
|
|
16
|
-
const key = args.join(" ");
|
|
17
|
-
if (key in scriptMap) {
|
|
18
|
-
const v = scriptMap[key];
|
|
19
|
-
return typeof v === "function" ? await v() : v;
|
|
20
|
-
}
|
|
21
|
-
// Allow prefix matches for convenience
|
|
22
|
-
for (const k of Object.keys(scriptMap)) {
|
|
23
|
-
if (key.startsWith(k)) {
|
|
24
|
-
const v = scriptMap[k];
|
|
25
|
-
return typeof v === "function" ? await v() : v;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
throw new Error(`mockExec: no script for: ${cmd} ${key}`);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("AndroidExtractor", () => {
|
|
33
|
-
it("listDevices parses adb output", async () => {
|
|
34
|
-
const ext = new AndroidExtractor({
|
|
35
|
-
execFn: mockExec({
|
|
36
|
-
"devices -l": {
|
|
37
|
-
stdout:
|
|
38
|
-
"List of devices attached\n" +
|
|
39
|
-
"ABCDEF123 device product:redmi model:Redmi_24115RA8EC device:redmi\n" +
|
|
40
|
-
"OFFLINE99 offline\n",
|
|
41
|
-
stderr: "",
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
});
|
|
45
|
-
const devices = await ext.listDevices();
|
|
46
|
-
expect(devices).toHaveLength(2);
|
|
47
|
-
expect(devices[0].serial).toBe("ABCDEF123");
|
|
48
|
-
expect(devices[0].state).toBe("device");
|
|
49
|
-
expect(devices[0].model).toBe("Redmi_24115RA8EC");
|
|
50
|
-
expect(devices[1].state).toBe("offline");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("isDeviceReady checks state", async () => {
|
|
54
|
-
const ext = new AndroidExtractor({
|
|
55
|
-
execFn: mockExec({
|
|
56
|
-
"devices -l": {
|
|
57
|
-
stdout: "List of devices attached\nABCDEF123 device product:p\n",
|
|
58
|
-
stderr: "",
|
|
59
|
-
},
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
62
|
-
expect(await ext.isDeviceReady("ABCDEF123")).toBe(true);
|
|
63
|
-
expect(await ext.isDeviceReady("BAD-SERIAL")).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("probeRoot detects Magisk + selinux", async () => {
|
|
67
|
-
const ext = new AndroidExtractor({
|
|
68
|
-
execFn: mockExec({
|
|
69
|
-
"-s ABC shell which su": { stdout: "/system/bin/magisk\n" },
|
|
70
|
-
"-s ABC shell which magisk": { stdout: "/system/bin/magisk\n" },
|
|
71
|
-
"-s ABC shell getenforce": { stdout: "Enforcing\n" },
|
|
72
|
-
}),
|
|
73
|
-
});
|
|
74
|
-
const probe = await ext.probeRoot("ABC");
|
|
75
|
-
expect(probe.rooted).toBe(true);
|
|
76
|
-
expect(probe.su).toBe("magisk-su");
|
|
77
|
-
expect(probe.magiskInstalled).toBe(true);
|
|
78
|
-
expect(probe.selinux).toBe("enforcing");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("probeRoot non-rooted device", async () => {
|
|
82
|
-
const ext = new AndroidExtractor({
|
|
83
|
-
execFn: mockExec({
|
|
84
|
-
"-s ABC shell which su": { stdout: "" },
|
|
85
|
-
"-s ABC shell which magisk": { stdout: "" },
|
|
86
|
-
"-s ABC shell getenforce": { stdout: "Enforcing\n" },
|
|
87
|
-
}),
|
|
88
|
-
});
|
|
89
|
-
const probe = await ext.probeRoot("ABC");
|
|
90
|
-
expect(probe.rooted).toBe(false);
|
|
91
|
-
expect(probe.su).toBeNull();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("listPackages filters user-installed by default", async () => {
|
|
95
|
-
const ext = new AndroidExtractor({
|
|
96
|
-
execFn: mockExec({
|
|
97
|
-
"-s ABC shell pm list packages -3": {
|
|
98
|
-
stdout: "package:com.tencent.mm\npackage:com.taobao.taobao\n",
|
|
99
|
-
},
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
const pkgs = await ext.listPackages("ABC");
|
|
103
|
-
expect(pkgs).toEqual(["com.tencent.mm", "com.taobao.taobao"]);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("pull creates dest dir + invokes adb pull", async () => {
|
|
107
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ax-"));
|
|
108
|
-
let pulled = false;
|
|
109
|
-
const ext = new AndroidExtractor({
|
|
110
|
-
execFn: async (cmd, args) => {
|
|
111
|
-
if (args[2] === "pull") {
|
|
112
|
-
pulled = true;
|
|
113
|
-
// Simulate adb pull writing to dest
|
|
114
|
-
fs.writeFileSync(args[4], "fake-pulled-content");
|
|
115
|
-
return { stdout: "1 file pulled", stderr: "" };
|
|
116
|
-
}
|
|
117
|
-
throw new Error("unexpected adb call");
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
const dest = path.join(dir, "sub/file.bin");
|
|
121
|
-
await ext.pull("ABC", "/sdcard/x.bin", dest);
|
|
122
|
-
expect(pulled).toBe(true);
|
|
123
|
-
expect(fs.existsSync(dest)).toBe(true);
|
|
124
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("pullFromAppPrivate uses su cat + temp + cleanup", async () => {
|
|
128
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ax-"));
|
|
129
|
-
const calls = [];
|
|
130
|
-
const ext = new AndroidExtractor({
|
|
131
|
-
execFn: async (cmd, args) => {
|
|
132
|
-
calls.push(args.join(" "));
|
|
133
|
-
if (args[2] === "shell" && args[3].startsWith("su -c 'cat")) {
|
|
134
|
-
return { stdout: "", stderr: "" };
|
|
135
|
-
}
|
|
136
|
-
if (args[2] === "pull") {
|
|
137
|
-
fs.writeFileSync(args[4], "decrypted-db-bytes");
|
|
138
|
-
return { stdout: "", stderr: "" };
|
|
139
|
-
}
|
|
140
|
-
if (args[2] === "shell" && args[3].startsWith("rm -f")) {
|
|
141
|
-
return { stdout: "", stderr: "" };
|
|
142
|
-
}
|
|
143
|
-
throw new Error("unexpected adb call: " + args.join(" "));
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
const dest = path.join(dir, "EnMicroMsg.db");
|
|
147
|
-
await ext.pullFromAppPrivate("ABC", "com.tencent.mm", "/data/data/com.tencent.mm/MicroMsg/x/EnMicroMsg.db", dest);
|
|
148
|
-
expect(fs.existsSync(dest)).toBe(true);
|
|
149
|
-
// Verify su cat happened + cleanup rm happened
|
|
150
|
-
expect(calls.some((c) => c.includes("su -c 'cat"))).toBe(true);
|
|
151
|
-
expect(calls.some((c) => c.includes("rm -f"))).toBe(true);
|
|
152
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("lsAppPrivate parses ls -1 output", async () => {
|
|
156
|
-
const ext = new AndroidExtractor({
|
|
157
|
-
execFn: mockExec({
|
|
158
|
-
"-s ABC shell su -c 'ls -1 \"/data/data/com.tencent.mm\"'": {
|
|
159
|
-
stdout: "MicroMsg\nshared_prefs\nfiles\ncache\n",
|
|
160
|
-
},
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
const ls = await ext.lsAppPrivate("ABC", "/data/data/com.tencent.mm");
|
|
164
|
-
expect(ls).toContain("MicroMsg");
|
|
165
|
-
expect(ls).toContain("shared_prefs");
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ─── iOSBackupReader — fixture-driven ────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
describe("iOSBackupReader", () => {
|
|
172
|
-
let dir;
|
|
173
|
-
|
|
174
|
-
function makeBackup({ encrypted = false } = {}) {
|
|
175
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "ios-bk-"));
|
|
176
|
-
fs.writeFileSync(
|
|
177
|
-
path.join(dir, "Manifest.plist"),
|
|
178
|
-
`<?xml version="1.0"?><plist version="1.0"><dict>
|
|
179
|
-
<key>IsEncrypted</key>${encrypted ? "<true/>" : "<false/>"}
|
|
180
|
-
</dict></plist>`,
|
|
181
|
-
);
|
|
182
|
-
fs.writeFileSync(
|
|
183
|
-
path.join(dir, "Info.plist"),
|
|
184
|
-
`<?xml version="1.0"?><plist version="1.0"><dict>
|
|
185
|
-
<key>Device Name</key><string>Test iPhone</string>
|
|
186
|
-
<key>Product Type</key><string>iPhone15,2</string>
|
|
187
|
-
<key>Product Version</key><string>18.0</string>
|
|
188
|
-
</dict></plist>`,
|
|
189
|
-
);
|
|
190
|
-
// Empty SQLite Manifest.db — mock driver below skips it
|
|
191
|
-
fs.writeFileSync(path.join(dir, "Manifest.db"), Buffer.from("SQLite format 3\0"));
|
|
192
|
-
return dir;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
afterEach(() => {
|
|
196
|
-
if (dir) {
|
|
197
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
198
|
-
}
|
|
199
|
-
dir = null;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("constructor rejects missing dir", () => {
|
|
203
|
-
expect(() => new iOSBackupReader({ backupDir: "/nonexistent/path/xx" })).toThrow();
|
|
204
|
-
expect(() => new iOSBackupReader({})).toThrow(/backupDir/);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("encrypted backup throws (Phase 7.5b TODO)", async () => {
|
|
208
|
-
const backupDir = makeBackup({ encrypted: true });
|
|
209
|
-
const reader = new iOSBackupReader({
|
|
210
|
-
backupDir,
|
|
211
|
-
dbDriverFn: () => {
|
|
212
|
-
throw new Error("driver should not be called when encrypted");
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
await expect(reader.open()).rejects.toThrow(/encrypted/);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("open() reads Info.plist + opens Manifest.db", async () => {
|
|
219
|
-
const backupDir = makeBackup({ encrypted: false });
|
|
220
|
-
// Mock SQLite driver
|
|
221
|
-
const mockDb = {
|
|
222
|
-
prepare: () => ({ all: () => [], get: () => undefined }),
|
|
223
|
-
close: () => {},
|
|
224
|
-
};
|
|
225
|
-
const mockDriver = () => mockDb;
|
|
226
|
-
const reader = new iOSBackupReader({ backupDir, dbDriverFn: mockDriver });
|
|
227
|
-
const r = await reader.open();
|
|
228
|
-
expect(r.encrypted).toBe(false);
|
|
229
|
-
expect(r.info["Device Name"]).toBe("Test iPhone");
|
|
230
|
-
expect(r.info["Product Version"]).toBe("18.0");
|
|
231
|
-
reader.close();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("listFiles passes WHERE clauses based on opts", async () => {
|
|
235
|
-
const backupDir = makeBackup({ encrypted: false });
|
|
236
|
-
let lastSql, lastParams;
|
|
237
|
-
const mockDriver = () => ({
|
|
238
|
-
prepare: (sql) => ({
|
|
239
|
-
all: (...params) => {
|
|
240
|
-
lastSql = sql;
|
|
241
|
-
lastParams = params;
|
|
242
|
-
return [{ fileID: "abc", domain: "HomeDomain", relativePath: "Library/Notes/notes.sqlite", flags: 1 }];
|
|
243
|
-
},
|
|
244
|
-
}),
|
|
245
|
-
close: () => {},
|
|
246
|
-
});
|
|
247
|
-
const reader = new iOSBackupReader({ backupDir, dbDriverFn: mockDriver });
|
|
248
|
-
await reader.open();
|
|
249
|
-
const files = reader.listFiles({ domain: "HomeDomain", limit: 10 });
|
|
250
|
-
expect(files).toHaveLength(1);
|
|
251
|
-
expect(lastSql).toContain("WHERE domain = ?");
|
|
252
|
-
expect(lastParams).toContain("HomeDomain");
|
|
253
|
-
reader.close();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("resolveFileOnDisk shards by first 2 chars", async () => {
|
|
257
|
-
const backupDir = makeBackup({ encrypted: false });
|
|
258
|
-
const mockDriver = () => ({
|
|
259
|
-
prepare: () => ({ all: () => [], get: () => undefined }),
|
|
260
|
-
close: () => {},
|
|
261
|
-
});
|
|
262
|
-
const reader = new iOSBackupReader({ backupDir, dbDriverFn: mockDriver });
|
|
263
|
-
await reader.open();
|
|
264
|
-
const p = reader.resolveFileOnDisk("abcd1234567890");
|
|
265
|
-
expect(p).toBe(path.join(backupDir, "ab", "abcd1234567890"));
|
|
266
|
-
reader.close();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("copyOut copies the sharded file to localPath", async () => {
|
|
270
|
-
const backupDir = makeBackup({ encrypted: false });
|
|
271
|
-
// Create a fake sharded file
|
|
272
|
-
const fileID = "abc1234";
|
|
273
|
-
const shardedDir = path.join(backupDir, "ab");
|
|
274
|
-
fs.mkdirSync(shardedDir, { recursive: true });
|
|
275
|
-
fs.writeFileSync(path.join(shardedDir, fileID), "test-content");
|
|
276
|
-
|
|
277
|
-
const mockDriver = () => ({
|
|
278
|
-
prepare: () => ({ all: () => [], get: () => undefined }),
|
|
279
|
-
close: () => {},
|
|
280
|
-
});
|
|
281
|
-
const reader = new iOSBackupReader({ backupDir, dbDriverFn: mockDriver });
|
|
282
|
-
await reader.open();
|
|
283
|
-
const local = path.join(backupDir, "out", "extracted.bin");
|
|
284
|
-
reader.copyOut(fileID, local);
|
|
285
|
-
expect(fs.readFileSync(local, "utf-8")).toBe("test-content");
|
|
286
|
-
reader.close();
|
|
287
|
-
});
|
|
288
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { MockAdapter } = require("../lib/mock-adapter");
|
|
6
|
-
const { validate } = require("../lib/schemas");
|
|
7
|
-
const { assertAdapter } = require("../lib/adapter-spec");
|
|
8
|
-
|
|
9
|
-
describe("MockAdapter", () => {
|
|
10
|
-
it("satisfies the PersonalDataAdapter contract", () => {
|
|
11
|
-
expect(assertAdapter(new MockAdapter()).ok).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("yields exactly `count` raw events", async () => {
|
|
15
|
-
const a = new MockAdapter({ count: 7 });
|
|
16
|
-
const raws = [];
|
|
17
|
-
for await (const r of a.sync()) raws.push(r);
|
|
18
|
-
expect(raws.length).toBe(7);
|
|
19
|
-
expect(raws[0].adapter).toBe("mock");
|
|
20
|
-
expect(raws[0].originalId).toMatch(/^mock-raw-/);
|
|
21
|
-
expect(raws[0].capturedAt).toBeGreaterThan(1_000_000_000_000);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("respects sinceWatermark by skipping seen items", async () => {
|
|
25
|
-
const a = new MockAdapter({ count: 10 });
|
|
26
|
-
const skipped = [];
|
|
27
|
-
for await (const r of a.sync({ sinceWatermark: 4 })) skipped.push(r);
|
|
28
|
-
expect(skipped.length).toBe(6); // 10 - 4
|
|
29
|
-
expect(skipped[0].payload.index).toBe(4);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("respects maxEvents", async () => {
|
|
33
|
-
const a = new MockAdapter({ count: 50 });
|
|
34
|
-
const got = [];
|
|
35
|
-
for await (const r of a.sync({ maxEvents: 5 })) got.push(r);
|
|
36
|
-
expect(got.length).toBe(5);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("normalize produces valid UnifiedSchema entities (v0, v1, v2 variants)", async () => {
|
|
40
|
-
const a = new MockAdapter({ count: 6, seed: 42 });
|
|
41
|
-
for await (const r of a.sync()) {
|
|
42
|
-
const batch = a.normalize(r);
|
|
43
|
-
expect(batch.events.length).toBeGreaterThanOrEqual(1);
|
|
44
|
-
for (const e of batch.events) expect(validate(e).valid).toBe(true);
|
|
45
|
-
for (const p of batch.persons) expect(validate(p).valid).toBe(true);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("healthCheck honors shouldFailHealth flag", async () => {
|
|
50
|
-
const a = new MockAdapter();
|
|
51
|
-
expect((await a.healthCheck()).ok).toBe(true);
|
|
52
|
-
a.shouldFailHealth = true;
|
|
53
|
-
const r = await a.healthCheck();
|
|
54
|
-
expect(r.ok).toBe(false);
|
|
55
|
-
expect(r.reason).toContain("unhealthy");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("failAfter throws mid-sync", async () => {
|
|
59
|
-
const a = new MockAdapter({ count: 10 });
|
|
60
|
-
a.failAfter = 3;
|
|
61
|
-
const got = [];
|
|
62
|
-
await expect(async () => {
|
|
63
|
-
for await (const r of a.sync()) got.push(r);
|
|
64
|
-
}).rejects.toThrow(/induced sync failure/);
|
|
65
|
-
expect(got.length).toBe(3); // 3 yielded before throw
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("normalizeShouldThrowAt fires on the right call", async () => {
|
|
69
|
-
const a = new MockAdapter({ count: 5 });
|
|
70
|
-
a.normalizeShouldThrowAt(2);
|
|
71
|
-
let callCount = 0;
|
|
72
|
-
for await (const r of a.sync()) {
|
|
73
|
-
callCount += 1;
|
|
74
|
-
if (callCount < 3) {
|
|
75
|
-
expect(() => a.normalize(r)).not.toThrow();
|
|
76
|
-
} else if (callCount === 3) {
|
|
77
|
-
expect(() => a.normalize(r)).toThrow(/induced normalize/);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("same seed produces same payloads (deterministic)", async () => {
|
|
83
|
-
const collect = async () => {
|
|
84
|
-
const a = new MockAdapter({ count: 5, seed: 99 });
|
|
85
|
-
const out = [];
|
|
86
|
-
for await (const r of a.sync()) out.push(r.payload);
|
|
87
|
-
return out;
|
|
88
|
-
};
|
|
89
|
-
const a = await collect();
|
|
90
|
-
const b = await collect();
|
|
91
|
-
expect(a).toEqual(b);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
DEFAULT_SYSTEM_PROMPT,
|
|
7
|
-
buildPrompt,
|
|
8
|
-
summarizeFact,
|
|
9
|
-
summarizeEvent,
|
|
10
|
-
summarizePerson,
|
|
11
|
-
parseCitations,
|
|
12
|
-
validateCitations,
|
|
13
|
-
} = require("../lib/prompt-builder");
|
|
14
|
-
|
|
15
|
-
// ─── summarize ────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
describe("summarizeFact", () => {
|
|
18
|
-
it("strips raw extra from events; keeps content + source.adapter", () => {
|
|
19
|
-
const e = {
|
|
20
|
-
id: "evt-1",
|
|
21
|
-
type: "event",
|
|
22
|
-
subtype: "order",
|
|
23
|
-
occurredAt: 1700000000000,
|
|
24
|
-
ingestedAt: 1700000000001,
|
|
25
|
-
content: {
|
|
26
|
-
title: "spam",
|
|
27
|
-
text: "delivery",
|
|
28
|
-
amount: { value: 100, currency: "CNY", direction: "out" },
|
|
29
|
-
},
|
|
30
|
-
source: { adapter: "taobao", adapterVersion: "0.1.0", capturedAt: 1, capturedBy: "api" },
|
|
31
|
-
extra: { secret: "hidden", tracking: "abc" },
|
|
32
|
-
};
|
|
33
|
-
const s = summarizeFact(e);
|
|
34
|
-
expect(s.id).toBe("evt-1");
|
|
35
|
-
expect(s.title).toBe("spam");
|
|
36
|
-
expect(s.amount).toEqual({ value: 100, currency: "CNY", dir: "out" });
|
|
37
|
-
expect(s.source).toBe("taobao");
|
|
38
|
-
expect(s).not.toHaveProperty("extra");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("packs person names + relation + identifiers + notes; omits source", () => {
|
|
42
|
-
// 2026-05-27 — identifiers (phone/wechatId/email) MUST reach the LLM,
|
|
43
|
-
// otherwise "妈手机号是多少" can never be answered even when vault has
|
|
44
|
-
// the phone. notes too — they're user-written context. source/ingestedAt
|
|
45
|
-
// are framing metadata, not user data, so still stripped.
|
|
46
|
-
const p = summarizePerson({
|
|
47
|
-
id: "p1",
|
|
48
|
-
type: "person",
|
|
49
|
-
subtype: "contact",
|
|
50
|
-
names: ["妈妈", "陈某某"],
|
|
51
|
-
relation: "母亲",
|
|
52
|
-
identifiers: { phone: ["13800001111"], wechatId: "wxid_abc" },
|
|
53
|
-
notes: "best mom ever",
|
|
54
|
-
ingestedAt: 1,
|
|
55
|
-
source: { adapter: "x", adapterVersion: "0.1.0", capturedAt: 1, capturedBy: "api" },
|
|
56
|
-
});
|
|
57
|
-
expect(p.names).toEqual(["妈妈", "陈某某"]);
|
|
58
|
-
expect(p.relation).toBe("母亲");
|
|
59
|
-
expect(p.identifiers).toEqual({ phone: ["13800001111"], wechatId: "wxid_abc" });
|
|
60
|
-
expect(p.notes).toBe("best mom ever");
|
|
61
|
-
expect(p).not.toHaveProperty("source");
|
|
62
|
-
expect(p).not.toHaveProperty("ingestedAt");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("omits identifiers / notes fields when absent on the person row", () => {
|
|
66
|
-
const p = summarizePerson({
|
|
67
|
-
id: "p2",
|
|
68
|
-
type: "person",
|
|
69
|
-
subtype: "contact",
|
|
70
|
-
names: ["路人甲"],
|
|
71
|
-
});
|
|
72
|
-
expect(p).not.toHaveProperty("identifiers");
|
|
73
|
-
expect(p).not.toHaveProperty("notes");
|
|
74
|
-
expect(p).not.toHaveProperty("relation");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("returns null for non-object / unknown types just yields minimal shape", () => {
|
|
78
|
-
expect(summarizeFact(null)).toBeNull();
|
|
79
|
-
expect(summarizeFact({ id: "x", type: "unknown" })).toEqual({ id: "x", type: "unknown" });
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ─── buildPrompt ──────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe("buildPrompt", () => {
|
|
86
|
-
const facts = [
|
|
87
|
-
{
|
|
88
|
-
id: "evt-1",
|
|
89
|
-
type: "event",
|
|
90
|
-
subtype: "order",
|
|
91
|
-
occurredAt: 1700000000000,
|
|
92
|
-
ingestedAt: 1,
|
|
93
|
-
content: { title: "蛋白粉", amount: { value: 288.5, currency: "CNY", direction: "out" } },
|
|
94
|
-
source: { adapter: "taobao", adapterVersion: "0.1.0", capturedAt: 1, capturedBy: "api" },
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: "evt-2",
|
|
98
|
-
type: "event",
|
|
99
|
-
subtype: "order",
|
|
100
|
-
occurredAt: 1700000060000,
|
|
101
|
-
ingestedAt: 1,
|
|
102
|
-
content: { title: "按摩仪", amount: { value: 459, currency: "CNY", direction: "out" } },
|
|
103
|
-
source: { adapter: "taobao", adapterVersion: "0.1.0", capturedAt: 1, capturedBy: "api" },
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
it("returns system + user messages and factIds set", () => {
|
|
108
|
-
const { messages, factIds, factCount, truncated } = buildPrompt({
|
|
109
|
-
question: "上个月在淘宝总共花了多少?",
|
|
110
|
-
facts,
|
|
111
|
-
});
|
|
112
|
-
expect(messages.length).toBe(2);
|
|
113
|
-
expect(messages[0].role).toBe("system");
|
|
114
|
-
expect(messages[1].role).toBe("user");
|
|
115
|
-
expect(messages[1].content).toContain("上个月在淘宝总共花了多少");
|
|
116
|
-
expect(messages[1].content).toContain("evt-1");
|
|
117
|
-
expect(messages[1].content).toContain("evt-2");
|
|
118
|
-
expect(factIds.has("evt-1")).toBe(true);
|
|
119
|
-
expect(factIds.has("evt-2")).toBe(true);
|
|
120
|
-
expect(factCount).toBe(2);
|
|
121
|
-
expect(truncated).toBe(0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("packs question into user role (not system)", () => {
|
|
125
|
-
const { messages } = buildPrompt({ question: "test", facts });
|
|
126
|
-
expect(messages[0].content).not.toContain("test"); // system stays untrusted-free
|
|
127
|
-
expect(messages[1].content).toContain("test");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("uses default system prompt unless overridden", () => {
|
|
131
|
-
const { messages } = buildPrompt({ question: "x", facts: [] });
|
|
132
|
-
expect(messages[0].content).toBe(DEFAULT_SYSTEM_PROMPT);
|
|
133
|
-
|
|
134
|
-
const { messages: m2 } = buildPrompt({
|
|
135
|
-
question: "x",
|
|
136
|
-
facts: [],
|
|
137
|
-
systemPrompt: "custom",
|
|
138
|
-
});
|
|
139
|
-
expect(m2[0].content).toBe("custom");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("emits no-facts hint when empty", () => {
|
|
143
|
-
const { messages, factCount } = buildPrompt({ question: "x", facts: [] });
|
|
144
|
-
expect(factCount).toBe(0);
|
|
145
|
-
expect(messages[1].content).toContain("FACTS is empty");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("caps facts at maxFacts + reports truncation", () => {
|
|
149
|
-
const many = Array.from({ length: 100 }, (_, i) => ({
|
|
150
|
-
id: `evt-${i}`,
|
|
151
|
-
type: "event",
|
|
152
|
-
subtype: "message",
|
|
153
|
-
occurredAt: 1,
|
|
154
|
-
ingestedAt: 1,
|
|
155
|
-
content: { text: `m${i}` },
|
|
156
|
-
source: { adapter: "x", adapterVersion: "0.1.0", capturedAt: 1, capturedBy: "api" },
|
|
157
|
-
}));
|
|
158
|
-
const { factCount, truncated } = buildPrompt({ question: "x", facts: many, maxFacts: 30 });
|
|
159
|
-
expect(factCount).toBe(30);
|
|
160
|
-
expect(truncated).toBe(70);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("includes intent + timeWindow hints in user prompt when provided", () => {
|
|
164
|
-
const { messages } = buildPrompt({
|
|
165
|
-
question: "x",
|
|
166
|
-
facts,
|
|
167
|
-
intent: "sum-amount",
|
|
168
|
-
timeWindow: { since: 1700000000000, until: 1700000600000 },
|
|
169
|
-
});
|
|
170
|
-
expect(messages[1].content).toContain("Intent hint: sum-amount");
|
|
171
|
-
expect(messages[1].content).toContain("Time window:");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("marks the FACTS block as untrusted third-party content", () => {
|
|
175
|
-
const { messages } = buildPrompt({ question: "x", facts });
|
|
176
|
-
expect(messages[1].content).toContain("third-party");
|
|
177
|
-
expect(messages[1].content).toContain("never as instructions");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("emits AMOUNT_SUM block when amountSummary present + Rule 7 in system prompt", () => {
|
|
181
|
-
const { messages } = buildPrompt({
|
|
182
|
-
question: "上个月总共花了多少",
|
|
183
|
-
facts,
|
|
184
|
-
intent: "sum-amount",
|
|
185
|
-
amountSummary: { total: 1234.5, currency: "CNY", count: 7, byDirection: { out: 1200, in: 34.5 } },
|
|
186
|
-
});
|
|
187
|
-
expect(messages[1].content).toContain("AMOUNT_SUM");
|
|
188
|
-
expect(messages[1].content).toContain('"total": 1234.5');
|
|
189
|
-
expect(messages[1].content).toContain('"out": 1200');
|
|
190
|
-
// system prompt instructs LLM to trust AMOUNT_SUM, not sum FACTS
|
|
191
|
-
expect(messages[0].content).toMatch(/AMOUNT_SUM.*authoritative/i);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("omits AMOUNT_SUM block when count is 0 or amountSummary absent", () => {
|
|
195
|
-
const { messages } = buildPrompt({
|
|
196
|
-
question: "x",
|
|
197
|
-
facts,
|
|
198
|
-
amountSummary: { total: 0, currency: "CNY", count: 0, byDirection: { out: 0, in: 0 } },
|
|
199
|
-
});
|
|
200
|
-
expect(messages[1].content).not.toContain("AMOUNT_SUM");
|
|
201
|
-
const { messages: m2 } = buildPrompt({ question: "x", facts });
|
|
202
|
-
expect(m2[1].content).not.toContain("AMOUNT_SUM");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("throws on bad opts", () => {
|
|
206
|
-
expect(() => buildPrompt()).toThrow();
|
|
207
|
-
expect(() => buildPrompt(null)).toThrow();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// ─── parseCitations + validateCitations ──────────────────────────────────
|
|
212
|
-
|
|
213
|
-
describe("parseCitations", () => {
|
|
214
|
-
it("extracts bracketed ids in order", () => {
|
|
215
|
-
const out = parseCitations("foo [evt-1] bar [evt-2] [evt-3] baz");
|
|
216
|
-
expect(out).toEqual(["evt-1", "evt-2", "evt-3"]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("dedupes repeated cites", () => {
|
|
220
|
-
// ids must be ≥ 2 chars (regex deliberately ignores single-char [X]
|
|
221
|
-
// markdown to avoid false positives on footnote-style brackets)
|
|
222
|
-
expect(parseCitations("[evt-1] [evt-2] [evt-1]")).toEqual(["evt-1", "evt-2"]);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("ignores single-char and non-id-like brackets", () => {
|
|
226
|
-
// Single chars like [a] are deliberately ignored
|
|
227
|
-
expect(parseCitations("[a] [!] []")).toEqual([]);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("non-string returns []", () => {
|
|
231
|
-
expect(parseCitations(null)).toEqual([]);
|
|
232
|
-
expect(parseCitations(undefined)).toEqual([]);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe("validateCitations", () => {
|
|
237
|
-
it("partitions cited into known vs unknown", () => {
|
|
238
|
-
const factIds = new Set(["evt-1", "evt-2"]);
|
|
239
|
-
const r = validateCitations(["evt-1", "evt-fake", "evt-2"], factIds);
|
|
240
|
-
expect(r.known).toEqual(["evt-1", "evt-2"]);
|
|
241
|
-
expect(r.unknown).toEqual(["evt-fake"]);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("accepts array factIds too", () => {
|
|
245
|
-
const r = validateCitations(["a"], ["a", "b"]);
|
|
246
|
-
expect(r.known).toEqual(["a"]);
|
|
247
|
-
expect(r.unknown).toEqual([]);
|
|
248
|
-
});
|
|
249
|
-
});
|