@chainlesschain/personal-data-hub 0.4.29 → 0.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/forensics/qq-nt-collect.js +190 -0
- package/lib/prompt-builder.js +15 -1
- package/package.json +8 -3
- package/__tests__/adapter-guide.test.js +0 -47
- package/__tests__/adapter-spec.test.js +0 -78
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +0 -211
- package/__tests__/adapters/ai-chat-health-checker.test.js +0 -262
- package/__tests__/adapters/ai-chat-history.test.js +0 -396
- package/__tests__/adapters/ai-chat-http-client.test.js +0 -242
- package/__tests__/adapters/ai-chat-vendors.test.js +0 -874
- package/__tests__/adapters/alipay-bill-adapter.test.js +0 -538
- package/__tests__/adapters/apple-health.test.js +0 -95
- package/__tests__/adapters/bank-family.test.js +0 -125
- package/__tests__/adapters/biz-tianyancha.test.js +0 -159
- package/__tests__/adapters/browser-history-chrome.test.js +0 -377
- package/__tests__/adapters/browser-history-edge.test.js +0 -159
- package/__tests__/adapters/car-mercedesme.test.js +0 -74
- package/__tests__/adapters/doc-baidu-netdisk.test.js +0 -102
- package/__tests__/adapters/doc-camscanner.test.js +0 -147
- package/__tests__/adapters/doc-platforms.test.js +0 -177
- package/__tests__/adapters/edu-huawei-learning-live.test.js +0 -198
- package/__tests__/adapters/edu-zuoyebang-live.test.js +0 -226
- package/__tests__/adapters/email-adapter-snapshot.test.js +0 -237
- package/__tests__/adapters/email-adapter.test.js +0 -742
- package/__tests__/adapters/email-classifier.test.js +0 -347
- package/__tests__/adapters/email-imap-session.test.js +0 -334
- package/__tests__/adapters/email-parser.test.js +0 -244
- package/__tests__/adapters/email-pdf-extractor.test.js +0 -529
- package/__tests__/adapters/email-providers.test.js +0 -84
- package/__tests__/adapters/email-retry-progress.test.js +0 -294
- package/__tests__/adapters/email-templates.test.js +0 -822
- package/__tests__/adapters/family-23-collectors-scaffold.test.js +0 -182
- package/__tests__/adapters/finance-alipay-live.test.js +0 -258
- package/__tests__/adapters/finance-dcep.test.js +0 -74
- package/__tests__/adapters/fitness-joyrun.test.js +0 -82
- package/__tests__/adapters/game-genshin-live.test.js +0 -238
- package/__tests__/adapters/game-genshin-scaffold.test.js +0 -108
- package/__tests__/adapters/game-honor-of-kings-live.test.js +0 -230
- package/__tests__/adapters/git-activity.test.js +0 -222
- package/__tests__/adapters/gov-12123.test.js +0 -103
- package/__tests__/adapters/gov-ixiamen.test.js +0 -150
- package/__tests__/adapters/gov-tax.test.js +0 -135
- package/__tests__/adapters/health-meiyou.test.js +0 -125
- package/__tests__/adapters/local-files.test.js +0 -264
- package/__tests__/adapters/local-im-pc.test.js +0 -154
- package/__tests__/adapters/messaging-whatsapp.test.js +0 -289
- package/__tests__/adapters/music-kugou.test.js +0 -187
- package/__tests__/adapters/music-qq.test.js +0 -112
- package/__tests__/adapters/netease-music-live.test.js +0 -244
- package/__tests__/adapters/netease-music.test.js +0 -74
- package/__tests__/adapters/pc-local-discovery.test.js +0 -141
- package/__tests__/adapters/qq-pc-direct-read.test.js +0 -227
- package/__tests__/adapters/reading-family.test.js +0 -108
- package/__tests__/adapters/recruit-boss.test.js +0 -180
- package/__tests__/adapters/shell-history.test.js +0 -180
- package/__tests__/adapters/shopping-base.test.js +0 -179
- package/__tests__/adapters/shopping-dianping.test.js +0 -239
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +0 -721
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +0 -346
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +0 -284
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +0 -343
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +0 -296
- package/__tests__/adapters/social-csdn.test.js +0 -175
- package/__tests__/adapters/social-dongchedi.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-aweme-detail.test.js +0 -165
- package/__tests__/adapters/social-douyin-adb-collector.test.js +0 -254
- package/__tests__/adapters/social-douyin-adb-db-extension.test.js +0 -114
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +0 -304
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +0 -216
- package/__tests__/adapters/social-douyin-adb-usage-profile.test.js +0 -229
- package/__tests__/adapters/social-douyin-adb-watch-history.test.js +0 -269
- package/__tests__/adapters/social-kuaishou-adb-api-client.test.js +0 -496
- package/__tests__/adapters/social-kuaishou-adb-collector.test.js +0 -276
- package/__tests__/adapters/social-kuaishou-adb-cookies-extension.test.js +0 -152
- package/__tests__/adapters/social-kuaishou-adb-snapshot-builder.test.js +0 -178
- package/__tests__/adapters/social-toutiao-adb-account-reader.test.js +0 -135
- package/__tests__/adapters/social-toutiao-adb-api-client.test.js +0 -626
- package/__tests__/adapters/social-toutiao-adb-article.test.js +0 -155
- package/__tests__/adapters/social-toutiao-adb-collector.test.js +0 -378
- package/__tests__/adapters/social-toutiao-adb-cookies-extension.test.js +0 -193
- package/__tests__/adapters/social-toutiao-adb-snapshot-builder.test.js +0 -196
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +0 -311
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +0 -362
- package/__tests__/adapters/social-weibo-adb-collector.test.js +0 -201
- package/__tests__/adapters/social-weibo-adb-cookies-extension.test.js +0 -167
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +0 -189
- package/__tests__/adapters/social-xiaohongshu-adb-api-client.test.js +0 -431
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +0 -207
- package/__tests__/adapters/social-xiaohongshu-adb-cookies-extension.test.js +0 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign-provider-injection.test.js +0 -351
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +0 -130
- package/__tests__/adapters/social-xiaohongshu-adb-snapshot-builder.test.js +0 -200
- package/__tests__/adapters/social-zhihu.test.js +0 -246
- package/__tests__/adapters/system-data-adapter.test.js +0 -443
- package/__tests__/adapters/system-data-android-ingest.test.js +0 -144
- package/__tests__/adapters/system-data-android.test.js +0 -519
- package/__tests__/adapters/system-data-disclosure.test.js +0 -153
- package/__tests__/adapters/travel-12306.test.js +0 -512
- package/__tests__/adapters/travel-amap.test.js +0 -219
- package/__tests__/adapters/travel-baidu-map.test.js +0 -305
- package/__tests__/adapters/travel-base.test.js +0 -205
- package/__tests__/adapters/travel-ctrip.test.js +0 -377
- package/__tests__/adapters/travel-didi-consumer.test.js +0 -66
- package/__tests__/adapters/travel-didi.test.js +0 -204
- package/__tests__/adapters/travel-tencent-map.test.js +0 -207
- package/__tests__/adapters/travel-tongcheng.test.js +0 -289
- package/__tests__/adapters/video-platforms.test.js +0 -152
- package/__tests__/adapters/video-xigua.test.js +0 -106
- package/__tests__/adapters/vscode.test.js +0 -299
- package/__tests__/adapters/wechat-bootstrap.test.js +0 -240
- package/__tests__/adapters/wechat-env-probe.test.js +0 -162
- package/__tests__/adapters/wechat-frida-agent.test.js +0 -322
- package/__tests__/adapters/wechat-frida-integration.test.js +0 -149
- package/__tests__/adapters/wechat-frida-key-provider.test.js +0 -188
- package/__tests__/adapters/wechat-md5-key-provider.test.js +0 -101
- package/__tests__/adapters/wechat-pc-direct-read.test.js +0 -365
- package/__tests__/adapters/wechat-pc-group-topic.test.js +0 -63
- package/__tests__/adapters/wechat-pc-v4-sidecar.test.js +0 -72
- package/__tests__/adapters/weread.test.js +0 -123
- package/__tests__/adapters/wework-pc.test.js +0 -124
- package/__tests__/adapters/win-recent.test.js +0 -192
- package/__tests__/analysis-skills.test.js +0 -754
- package/__tests__/analysis.test.js +0 -1845
- package/__tests__/audio-ximalaya-snapshot.test.js +0 -279
- package/__tests__/batch.test.js +0 -133
- package/__tests__/bridges-cc-kg.test.js +0 -231
- package/__tests__/bridges-cc-llm.test.js +0 -191
- package/__tests__/bridges-cc-rag.test.js +0 -162
- package/__tests__/categories.test.js +0 -92
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +0 -213
- package/__tests__/e2e/full-user-journey.test.js +0 -188
- package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +0 -146
- package/__tests__/entity-resolver-ingest-hook.test.js +0 -177
- package/__tests__/entity-resolver-stages.test.js +0 -411
- package/__tests__/entity-resolver-vault.test.js +0 -249
- package/__tests__/entity-resolver.test.js +0 -526
- package/__tests__/fitness-keep-snapshot.test.js +0 -224
- package/__tests__/fixtures/entity-resolver-200-mock.json +0 -96
- package/__tests__/ids.test.js +0 -45
- package/__tests__/integration/ai-chat-history-registry.test.js +0 -228
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +0 -282
- package/__tests__/integration/cross-adapter-pipelines.test.js +0 -396
- package/__tests__/integration/local-data-adapters-pipeline.test.js +0 -373
- package/__tests__/integration/social-bilibili-pipeline.test.js +0 -261
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +0 -390
- package/__tests__/key-providers.test.js +0 -126
- package/__tests__/kg-derive.test.js +0 -219
- package/__tests__/llm-client.test.js +0 -122
- package/__tests__/longtail-adapters.test.js +0 -281
- package/__tests__/messaging-qq-snapshot.test.js +0 -294
- package/__tests__/mobile-extractor-encrypted.test.js +0 -460
- package/__tests__/mobile-extractor.test.js +0 -288
- package/__tests__/mock-adapter.test.js +0 -93
- package/__tests__/prompt-builder.test.js +0 -249
- package/__tests__/query-parser.test.js +0 -365
- package/__tests__/rag-derive.test.js +0 -169
- package/__tests__/registry-readiness.test.js +0 -292
- package/__tests__/registry.test.js +0 -420
- package/__tests__/salvage-ingest.test.js +0 -97
- package/__tests__/schemas.test.js +0 -331
- package/__tests__/shopping-adapters.test.js +0 -392
- package/__tests__/shopping-eleme-snapshot.test.js +0 -454
- package/__tests__/shopping-pinduoduo-snapshot.test.js +0 -484
- package/__tests__/shopping-snapshot.test.js +0 -438
- package/__tests__/shopping-vipshop-snapshot.test.js +0 -425
- package/__tests__/shopping-xianyu-snapshot.test.js +0 -451
- package/__tests__/sidecar-contacts-cross-validate.test.js +0 -186
- package/__tests__/sidecar-supervisor.test.js +0 -128
- package/__tests__/sign-providers.test.js +0 -62
- package/__tests__/social-adapters.test.js +0 -280
- package/__tests__/social-bilibili-snapshot.test.js +0 -278
- package/__tests__/social-douban-snapshot.test.js +0 -351
- package/__tests__/social-douyin-im-direct-read.test.js +0 -377
- package/__tests__/social-douyin-salvage-collector.test.js +0 -98
- package/__tests__/social-douyin-salvage-mapper.test.js +0 -90
- package/__tests__/social-douyin-snapshot.test.js +0 -256
- package/__tests__/social-kuaishou-snapshot.test.js +0 -362
- package/__tests__/social-toutiao-snapshot.test.js +0 -366
- package/__tests__/social-weibo-snapshot.test.js +0 -234
- package/__tests__/social-weibo-sqlite-device.test.js +0 -174
- package/__tests__/social-xiaohongshu-snapshot.test.js +0 -232
- package/__tests__/sqlite-leaf-salvage.test.js +0 -97
- package/__tests__/travel-adapters.test.js +0 -483
- package/__tests__/travel-maps-snapshot.test.js +0 -426
- package/__tests__/vault-driver-error.test.js +0 -74
- package/__tests__/vault-search-helpers.test.js +0 -104
- package/__tests__/vault-search.test.js +0 -423
- package/__tests__/vault.test.js +0 -767
- package/__tests__/wechat-adapter.test.js +0 -594
- package/__tests__/whatsapp-adapter.test.js +0 -138
- package/scripts/_make-fixture-all.js +0 -126
- package/scripts/_make-fixture-contacts.js +0 -84
- package/scripts/evaluate-entity-resolver.js +0 -213
- package/scripts/run-native-tests-sandbox.sh +0 -55
- package/scripts/smoke-phase-5-5.js +0 -196
- package/scripts/smoke-phase-5-7.js +0 -181
- package/scripts/smoke-system-data-contacts.js +0 -309
- package/scripts/smoke-system-data.js +0 -312
- package/vitest.config.js +0 -88
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
4
|
-
|
|
5
|
-
const fs = require("node:fs");
|
|
6
|
-
const os = require("node:os");
|
|
7
|
-
const path = require("node:path");
|
|
8
|
-
|
|
9
|
-
const { LocalVault } = require("../lib/vault");
|
|
10
|
-
const { generateKeyHex } = require("../lib/key-providers");
|
|
11
|
-
const { AdapterRegistry } = require("../lib/registry");
|
|
12
|
-
const { MockAdapter } = require("../lib/mock-adapter");
|
|
13
|
-
|
|
14
|
-
// ─── Scaffolding ─────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
let tmpDir;
|
|
17
|
-
let vault;
|
|
18
|
-
|
|
19
|
-
function freshVault() {
|
|
20
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdh-reg-"));
|
|
21
|
-
vault = new LocalVault({
|
|
22
|
-
path: path.join(tmpDir, "vault.db"),
|
|
23
|
-
key: generateKeyHex(),
|
|
24
|
-
skipAudit: true,
|
|
25
|
-
});
|
|
26
|
-
vault.open();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
if (vault) {
|
|
31
|
-
try { vault.close(); } catch (_e) {}
|
|
32
|
-
vault = null;
|
|
33
|
-
}
|
|
34
|
-
if (tmpDir && fs.existsSync(tmpDir)) {
|
|
35
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// ─── Registration ────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
describe("AdapterRegistry registration", () => {
|
|
42
|
-
it("rejects construction without vault", () => {
|
|
43
|
-
expect(() => new AdapterRegistry({})).toThrow(/vault/);
|
|
44
|
-
expect(() => new AdapterRegistry()).toThrow();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("registers + lists + looks up adapters", () => {
|
|
48
|
-
freshVault();
|
|
49
|
-
const reg = new AdapterRegistry({ vault });
|
|
50
|
-
const a = new MockAdapter({ name: "mock-a" });
|
|
51
|
-
const b = new MockAdapter({ name: "mock-b" });
|
|
52
|
-
reg.register(a);
|
|
53
|
-
reg.register(b);
|
|
54
|
-
expect(reg.has("mock-a")).toBe(true);
|
|
55
|
-
expect(reg.list().map((x) => x.name).sort()).toEqual(["mock-a", "mock-b"]);
|
|
56
|
-
expect(reg.get("mock-a")).toBe(a);
|
|
57
|
-
expect(reg.get("unknown")).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("rejects double-register of same name", () => {
|
|
61
|
-
freshVault();
|
|
62
|
-
const reg = new AdapterRegistry({ vault });
|
|
63
|
-
reg.register(new MockAdapter({ name: "dup" }));
|
|
64
|
-
expect(() => reg.register(new MockAdapter({ name: "dup" }))).toThrow(/already registered/);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("rejects malformed adapter (assertAdapter gate)", () => {
|
|
68
|
-
freshVault();
|
|
69
|
-
const reg = new AdapterRegistry({ vault });
|
|
70
|
-
expect(() => reg.register({ name: "broken" })).toThrow(/invalid adapter/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("unregister removes; second call returns false", () => {
|
|
74
|
-
freshVault();
|
|
75
|
-
const reg = new AdapterRegistry({ vault });
|
|
76
|
-
reg.register(new MockAdapter({ name: "x" }));
|
|
77
|
-
expect(reg.unregister("x")).toBe(true);
|
|
78
|
-
expect(reg.unregister("x")).toBe(false);
|
|
79
|
-
expect(reg.has("x")).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ─── End-to-end sync ─────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe("AdapterRegistry.syncAdapter", () => {
|
|
86
|
-
it("runs a clean MockAdapter end-to-end: vault + watermark + report", async () => {
|
|
87
|
-
freshVault();
|
|
88
|
-
const reg = new AdapterRegistry({ vault });
|
|
89
|
-
reg.register(new MockAdapter({ count: 30, seed: 1 }));
|
|
90
|
-
|
|
91
|
-
const report = await reg.syncAdapter("mock");
|
|
92
|
-
|
|
93
|
-
expect(report.status).toBe("ok");
|
|
94
|
-
expect(report.rawCount).toBe(30);
|
|
95
|
-
expect(report.invalidCount).toBe(0);
|
|
96
|
-
expect(report.entityCounts.events).toBe(30);
|
|
97
|
-
// ~2/3 of variants have a person (variants 1 and 2)
|
|
98
|
-
expect(report.entityCounts.persons).toBeGreaterThan(0);
|
|
99
|
-
expect(report.error).toBeNull();
|
|
100
|
-
expect(report.watermark).toBe("30");
|
|
101
|
-
expect(report.durationMs).toBeGreaterThan(0);
|
|
102
|
-
|
|
103
|
-
// Vault was written
|
|
104
|
-
expect(vault.stats().events).toBe(30);
|
|
105
|
-
expect(vault.stats().rawEvents).toBe(30);
|
|
106
|
-
|
|
107
|
-
// Watermark stored
|
|
108
|
-
const wm = vault.getWatermark("mock");
|
|
109
|
-
expect(wm.watermark).toBe("30");
|
|
110
|
-
expect(wm.last_status).toBe("ok");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("incremental sync skips already-seen items via stored watermark", async () => {
|
|
114
|
-
freshVault();
|
|
115
|
-
const reg = new AdapterRegistry({ vault });
|
|
116
|
-
reg.register(new MockAdapter({ count: 50, seed: 1 }));
|
|
117
|
-
|
|
118
|
-
const first = await reg.syncAdapter("mock", { maxEvents: 20 });
|
|
119
|
-
expect(first.rawCount).toBe(20);
|
|
120
|
-
expect(first.watermark).toBe("20");
|
|
121
|
-
|
|
122
|
-
const second = await reg.syncAdapter("mock");
|
|
123
|
-
expect(second.rawCount).toBe(30); // 50 - 20
|
|
124
|
-
expect(second.watermark).toBe("50");
|
|
125
|
-
expect(vault.stats().rawEvents).toBe(50);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("unhealthy adapter aborts before sync; records audit", async () => {
|
|
129
|
-
freshVault();
|
|
130
|
-
const reg = new AdapterRegistry({ vault });
|
|
131
|
-
const a = new MockAdapter({ count: 10 });
|
|
132
|
-
a.shouldFailHealth = true;
|
|
133
|
-
reg.register(a);
|
|
134
|
-
|
|
135
|
-
const report = await reg.syncAdapter("mock");
|
|
136
|
-
expect(report.status).toBe("unhealthy");
|
|
137
|
-
expect(report.rawCount).toBe(0);
|
|
138
|
-
expect(report.error).toBeTruthy();
|
|
139
|
-
expect(vault.stats().events).toBe(0);
|
|
140
|
-
|
|
141
|
-
const audits = vault.queryAudit({ action: "adapter.sync.unhealthy" });
|
|
142
|
-
expect(audits.length).toBe(1);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("normalize failure on one item does NOT abort the whole sync", async () => {
|
|
146
|
-
freshVault();
|
|
147
|
-
const reg = new AdapterRegistry({ vault });
|
|
148
|
-
const a = new MockAdapter({ count: 10 });
|
|
149
|
-
a.normalizeShouldThrowAt(4); // throw on the 5th normalize call
|
|
150
|
-
reg.register(a);
|
|
151
|
-
|
|
152
|
-
const report = await reg.syncAdapter("mock");
|
|
153
|
-
expect(report.status).toBe("ok");
|
|
154
|
-
expect(report.rawCount).toBe(10);
|
|
155
|
-
expect(report.invalidCount).toBeGreaterThanOrEqual(1);
|
|
156
|
-
expect(report.entityCounts.events).toBeLessThan(10);
|
|
157
|
-
|
|
158
|
-
const audits = vault.queryAudit({ action: "adapter.sync.normalize_failed" });
|
|
159
|
-
expect(audits.length).toBe(1);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("mid-sync throw is captured as status=error with preserved watermark", async () => {
|
|
163
|
-
freshVault();
|
|
164
|
-
const reg = new AdapterRegistry({ vault });
|
|
165
|
-
const a = new MockAdapter({ count: 100 });
|
|
166
|
-
a.failAfter = 25;
|
|
167
|
-
reg.register(a);
|
|
168
|
-
|
|
169
|
-
const report = await reg.syncAdapter("mock");
|
|
170
|
-
expect(report.status).toBe("error");
|
|
171
|
-
expect(report.error).toContain("induced sync failure");
|
|
172
|
-
|
|
173
|
-
const wm = vault.getWatermark("mock");
|
|
174
|
-
expect(wm.last_status).toBe("error");
|
|
175
|
-
expect(wm.last_error).toContain("induced sync failure");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("refuses concurrent sync of two adapters in one registry", async () => {
|
|
179
|
-
freshVault();
|
|
180
|
-
const reg = new AdapterRegistry({ vault });
|
|
181
|
-
// Use 500 events (not 5000) — still big enough to be mid-flight when
|
|
182
|
-
// the second syncAdapter() lands, but fits comfortably in 10s.
|
|
183
|
-
reg.register(new MockAdapter({ name: "a", count: 500 }));
|
|
184
|
-
reg.register(new MockAdapter({ name: "b", count: 5 }));
|
|
185
|
-
|
|
186
|
-
const p1 = reg.syncAdapter("a");
|
|
187
|
-
// Should reject during the brief async window where a is mid-sync.
|
|
188
|
-
let racedReject = null;
|
|
189
|
-
try {
|
|
190
|
-
await reg.syncAdapter("b");
|
|
191
|
-
} catch (e) {
|
|
192
|
-
racedReject = e;
|
|
193
|
-
}
|
|
194
|
-
await p1;
|
|
195
|
-
// Depending on event-loop timing this might race the other way; we just
|
|
196
|
-
// assert no double-sync corruption. The active-sync invariant is
|
|
197
|
-
// additionally enforced by the activeSync flag.
|
|
198
|
-
expect(racedReject == null || /already syncing/.test(racedReject.message)).toBe(true);
|
|
199
|
-
}, 30_000);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// ─── KG + RAG sinks ──────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
describe("AdapterRegistry pluggable sinks", () => {
|
|
205
|
-
it("kgSink receives triples per batch; ragSink receives docs per batch", async () => {
|
|
206
|
-
freshVault();
|
|
207
|
-
const kgTriples = [];
|
|
208
|
-
const ragDocs = [];
|
|
209
|
-
const reg = new AdapterRegistry({
|
|
210
|
-
vault,
|
|
211
|
-
kgSink: (ts) => kgTriples.push(...ts),
|
|
212
|
-
ragSink: (ds) => ragDocs.push(...ds),
|
|
213
|
-
batchSize: 7, // forces multiple batches across 20 events
|
|
214
|
-
});
|
|
215
|
-
reg.register(new MockAdapter({ count: 20, seed: 1 }));
|
|
216
|
-
|
|
217
|
-
const report = await reg.syncAdapter("mock");
|
|
218
|
-
expect(report.status).toBe("ok");
|
|
219
|
-
expect(kgTriples.length).toBeGreaterThan(20); // each event yields multiple triples
|
|
220
|
-
expect(ragDocs.length).toBeGreaterThan(0);
|
|
221
|
-
// All docs should reference the mock adapter
|
|
222
|
-
expect(ragDocs.every((d) => d.metadata.adapter === "mock")).toBe(true);
|
|
223
|
-
// Triple counts in the report should match what was actually sent
|
|
224
|
-
expect(report.kgTripleCount).toBe(kgTriples.length);
|
|
225
|
-
expect(report.ragDocCount).toBe(ragDocs.length);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("sink throws don't abort sync; failure recorded as audit", async () => {
|
|
229
|
-
freshVault();
|
|
230
|
-
const reg = new AdapterRegistry({
|
|
231
|
-
vault,
|
|
232
|
-
kgSink: () => { throw new Error("downstream KG died"); },
|
|
233
|
-
ragSink: () => { throw new Error("downstream RAG died"); },
|
|
234
|
-
});
|
|
235
|
-
reg.register(new MockAdapter({ count: 3 }));
|
|
236
|
-
|
|
237
|
-
const report = await reg.syncAdapter("mock");
|
|
238
|
-
expect(report.status).toBe("ok");
|
|
239
|
-
expect(report.rawCount).toBe(3);
|
|
240
|
-
expect(report.entityCounts.events).toBe(3);
|
|
241
|
-
|
|
242
|
-
const kgAudits = vault.queryAudit({ action: "adapter.sync.kg_sink_failed" });
|
|
243
|
-
const ragAudits = vault.queryAudit({ action: "adapter.sync.rag_sink_failed" });
|
|
244
|
-
expect(kgAudits.length).toBeGreaterThan(0);
|
|
245
|
-
expect(ragAudits.length).toBeGreaterThan(0);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// ─── syncAll ─────────────────────────────────────────────────────────────
|
|
250
|
-
|
|
251
|
-
describe("AdapterRegistry.syncAll", () => {
|
|
252
|
-
it("returns one report per registered adapter, isolates failures", async () => {
|
|
253
|
-
freshVault();
|
|
254
|
-
const reg = new AdapterRegistry({ vault });
|
|
255
|
-
reg.register(new MockAdapter({ name: "ok-a", count: 5 }));
|
|
256
|
-
const bad = new MockAdapter({ name: "bad", count: 10 });
|
|
257
|
-
bad.shouldFailHealth = true;
|
|
258
|
-
reg.register(bad);
|
|
259
|
-
reg.register(new MockAdapter({ name: "ok-b", count: 7 }));
|
|
260
|
-
|
|
261
|
-
const reports = await reg.syncAll();
|
|
262
|
-
expect(reports.length).toBe(3);
|
|
263
|
-
const byName = Object.fromEntries(reports.map((r) => [r.adapter, r]));
|
|
264
|
-
expect(byName["ok-a"].status).toBe("ok");
|
|
265
|
-
expect(byName["bad"].status).toBe("unhealthy");
|
|
266
|
-
expect(byName["ok-b"].status).toBe("ok");
|
|
267
|
-
|
|
268
|
-
// The bad adapter's failure didn't prevent the others' data from landing.
|
|
269
|
-
expect(vault.stats().events).toBe(12);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ─── 1k events < 30s perf gate (architecture doc §15.2) ──────────────────
|
|
274
|
-
|
|
275
|
-
describe("Phase 2 perf gate: 1k events ingest", () => {
|
|
276
|
-
it("ingests 1k mock events well under the 30s budget", async () => {
|
|
277
|
-
freshVault();
|
|
278
|
-
let kgCount = 0;
|
|
279
|
-
let ragCount = 0;
|
|
280
|
-
const reg = new AdapterRegistry({
|
|
281
|
-
vault,
|
|
282
|
-
kgSink: (ts) => { kgCount += ts.length; },
|
|
283
|
-
ragSink: (ds) => { ragCount += ds.length; },
|
|
284
|
-
batchSize: 200,
|
|
285
|
-
});
|
|
286
|
-
reg.register(new MockAdapter({ count: 1000, seed: 7 }));
|
|
287
|
-
|
|
288
|
-
const start = Date.now();
|
|
289
|
-
const report = await reg.syncAdapter("mock");
|
|
290
|
-
const dur = Date.now() - start;
|
|
291
|
-
|
|
292
|
-
expect(report.status).toBe("ok");
|
|
293
|
-
expect(report.rawCount).toBe(1000);
|
|
294
|
-
expect(report.entityCounts.events).toBe(1000);
|
|
295
|
-
expect(kgCount).toBeGreaterThan(1000);
|
|
296
|
-
expect(ragCount).toBeGreaterThan(0);
|
|
297
|
-
expect(dur).toBeLessThan(30_000); // architecture doc target
|
|
298
|
-
|
|
299
|
-
// Sanity probe: querying back ~1k events should be fast.
|
|
300
|
-
const qStart = Date.now();
|
|
301
|
-
const events = vault.queryEvents({ limit: 1000 });
|
|
302
|
-
const qDur = Date.now() - qStart;
|
|
303
|
-
expect(events.length).toBe(1000);
|
|
304
|
-
expect(qDur).toBeLessThan(2000); // 1k-row read should be ms-scale
|
|
305
|
-
}, 60_000); // vitest test timeout — extra headroom for slow CI
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// ─── rederive (recover orphan raw_events) ────────────────────────────────
|
|
309
|
-
|
|
310
|
-
describe("AdapterRegistry.rederive", () => {
|
|
311
|
-
it("promotes raw_events to canonical events for registered adapter", async () => {
|
|
312
|
-
freshVault();
|
|
313
|
-
const reg = new AdapterRegistry({ vault });
|
|
314
|
-
const adapter = new MockAdapter({ name: "mock-rederive" });
|
|
315
|
-
reg.register(adapter);
|
|
316
|
-
|
|
317
|
-
// Simulate the failure mode: putRawEvent succeeded (raw landed) but
|
|
318
|
-
// putBatch failed at sync time (e.g. partial-index drift trap #25),
|
|
319
|
-
// so events table is empty while raw_events has 3 rows.
|
|
320
|
-
const baseTs = 1_700_000_000_000;
|
|
321
|
-
for (let i = 0; i < 3; i++) {
|
|
322
|
-
vault.putRawEvent({
|
|
323
|
-
adapter: "mock-rederive",
|
|
324
|
-
originalId: `raw-${i}`,
|
|
325
|
-
capturedAt: baseTs + i * 1000,
|
|
326
|
-
payload: {
|
|
327
|
-
variant: 1,
|
|
328
|
-
senderName: `Alice${i}`,
|
|
329
|
-
text: `hello ${i}`,
|
|
330
|
-
index: i,
|
|
331
|
-
},
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
expect(vault.stats().rawEvents).toBe(3);
|
|
335
|
-
expect(vault.stats().events).toBe(0);
|
|
336
|
-
|
|
337
|
-
const report = await reg.rederive();
|
|
338
|
-
|
|
339
|
-
expect(report.rawSeen).toBe(3);
|
|
340
|
-
expect(report.adapterMissing).toBe(0);
|
|
341
|
-
expect(report.invalidCount).toBe(0);
|
|
342
|
-
expect(report.entityCounts.events).toBe(3);
|
|
343
|
-
expect(report.entityCounts.persons).toBe(3); // MockAdapter variant 1 yields 1 person/raw
|
|
344
|
-
expect(report.errors).toEqual([]);
|
|
345
|
-
expect(vault.stats().events).toBe(3);
|
|
346
|
-
expect(vault.stats().persons).toBe(3);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("counts raws whose adapter is not registered", async () => {
|
|
350
|
-
freshVault();
|
|
351
|
-
const reg = new AdapterRegistry({ vault });
|
|
352
|
-
// No adapter registered — every raw is orphan
|
|
353
|
-
vault.putRawEvent({
|
|
354
|
-
adapter: "ghost-adapter",
|
|
355
|
-
originalId: "x",
|
|
356
|
-
capturedAt: Date.now(),
|
|
357
|
-
payload: {},
|
|
358
|
-
});
|
|
359
|
-
const report = await reg.rederive();
|
|
360
|
-
expect(report.rawSeen).toBe(1);
|
|
361
|
-
expect(report.adapterMissing).toBe(1);
|
|
362
|
-
expect(report.entityCounts.events).toBe(0);
|
|
363
|
-
expect(vault.stats().events).toBe(0);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("isolates per-raw normalize failures (invalidCount)", async () => {
|
|
367
|
-
freshVault();
|
|
368
|
-
const reg = new AdapterRegistry({ vault });
|
|
369
|
-
// MockAdapter.normalize throws on call #3 (1-indexed). 3 raws → 3rd throws,
|
|
370
|
-
// 1st+2nd succeed → events=2, invalidCount=1.
|
|
371
|
-
const adapter = new MockAdapter({ name: "mock-throw" });
|
|
372
|
-
adapter.normalizeShouldThrowAt(2);
|
|
373
|
-
reg.register(adapter);
|
|
374
|
-
for (let i = 0; i < 3; i++) {
|
|
375
|
-
vault.putRawEvent({
|
|
376
|
-
adapter: "mock-throw",
|
|
377
|
-
originalId: `raw-${i}`,
|
|
378
|
-
capturedAt: 1_700_000_000_000 + i * 1000,
|
|
379
|
-
payload: { variant: 1, senderName: `Bob${i}`, text: `t${i}`, index: i },
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
const report = await reg.rederive();
|
|
383
|
-
expect(report.rawSeen).toBe(3);
|
|
384
|
-
expect(report.invalidCount).toBe(1);
|
|
385
|
-
expect(report.entityCounts.events).toBe(2);
|
|
386
|
-
expect(vault.stats().events).toBe(2);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("filters by --adapter option", async () => {
|
|
390
|
-
freshVault();
|
|
391
|
-
const reg = new AdapterRegistry({ vault });
|
|
392
|
-
reg.register(new MockAdapter({ name: "adapter-a" }));
|
|
393
|
-
reg.register(new MockAdapter({ name: "adapter-b" }));
|
|
394
|
-
vault.putRawEvent({
|
|
395
|
-
adapter: "adapter-a", originalId: "a1", capturedAt: 1_700_000_000_001,
|
|
396
|
-
payload: { variant: 1, senderName: "S", text: "ta", index: 1 },
|
|
397
|
-
});
|
|
398
|
-
vault.putRawEvent({
|
|
399
|
-
adapter: "adapter-b", originalId: "b1", capturedAt: 1_700_000_000_002,
|
|
400
|
-
payload: { variant: 1, senderName: "S", text: "tb", index: 1 },
|
|
401
|
-
});
|
|
402
|
-
const report = await reg.rederive({ adapter: "adapter-a" });
|
|
403
|
-
expect(report.rawSeen).toBe(1); // only adapter-a was queried
|
|
404
|
-
expect(report.entityCounts.events).toBe(1);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("is idempotent: rerunning produces same events table (UPSERT via partial index)", async () => {
|
|
408
|
-
freshVault();
|
|
409
|
-
const reg = new AdapterRegistry({ vault });
|
|
410
|
-
reg.register(new MockAdapter({ name: "idemp" }));
|
|
411
|
-
vault.putRawEvent({
|
|
412
|
-
adapter: "idemp", originalId: "x", capturedAt: 1_700_000_000_000,
|
|
413
|
-
payload: { variant: 1, senderName: "Alice", text: "hi", index: 0 },
|
|
414
|
-
});
|
|
415
|
-
await reg.rederive();
|
|
416
|
-
const eventsAfterFirst = vault.stats().events;
|
|
417
|
-
await reg.rederive();
|
|
418
|
-
expect(vault.stats().events).toBe(eventsAfterFirst);
|
|
419
|
-
});
|
|
420
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
4
|
-
|
|
5
|
-
const fs = require("node:fs");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const os = require("node:os");
|
|
8
|
-
|
|
9
|
-
const { LocalVault } = require("../lib/vault");
|
|
10
|
-
const { generateKeyHex } = require("../lib/key-providers");
|
|
11
|
-
const {
|
|
12
|
-
buildSalvageEvents,
|
|
13
|
-
salvageDumpToVault,
|
|
14
|
-
resolveApp,
|
|
15
|
-
} = require("../lib/forensics/salvage-ingest");
|
|
16
|
-
|
|
17
|
-
// Build a real SQLite DB and treat its bytes as a memory dump; verify the
|
|
18
|
-
// generic salvage→vault path recovers messages AND tags them with the correct
|
|
19
|
-
// per-app source.adapter (multi-app de-silo). Real LocalVault → proves the
|
|
20
|
-
// hand-built events pass schema validation + are searchable.
|
|
21
|
-
describe("salvage-ingest — generic multi-app salvage → vault", () => {
|
|
22
|
-
let dir, dumpPath, vault, vdir;
|
|
23
|
-
const COLUMNS = ["msg_uuid", "conversation_id", "sender", "content", "created_time"];
|
|
24
|
-
|
|
25
|
-
beforeAll(() => {
|
|
26
|
-
const Database = require("better-sqlite3-multiple-ciphers");
|
|
27
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "salvage-ing-"));
|
|
28
|
-
dumpPath = path.join(dir, "u.db");
|
|
29
|
-
const db = new Database(dumpPath);
|
|
30
|
-
db.exec("CREATE TABLE msg(msg_uuid TEXT, conversation_id TEXT, sender INTEGER, content TEXT, created_time INTEGER)");
|
|
31
|
-
const ins = db.prepare("INSERT INTO msg VALUES(?,?,?,?,?)");
|
|
32
|
-
ins.run("u1", "conv-1", 111, "今天的会议改到下午三点 hi", 1700000000000);
|
|
33
|
-
ins.run("u2", "conv-1", 222, "收到 👌", 1700000001000);
|
|
34
|
-
db.close();
|
|
35
|
-
|
|
36
|
-
vdir = fs.mkdtempSync(path.join(os.tmpdir(), "salvage-vault-"));
|
|
37
|
-
vault = new LocalVault({ path: path.join(vdir, "v.db"), key: generateKeyHex() });
|
|
38
|
-
vault.open();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterAll(() => {
|
|
42
|
-
try { vault.close(); } catch (_e) {}
|
|
43
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
44
|
-
try { fs.rmSync(vdir, { recursive: true, force: true }); } catch (_e) {}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("resolveApp maps known apps to canonical source adapters; unknown → salvage:<app>", () => {
|
|
48
|
-
expect(resolveApp("douyin").sourceAdapter).toBe("social-douyin");
|
|
49
|
-
expect(resolveApp("toutiao").sourceAdapter).toBe("social-toutiao");
|
|
50
|
-
expect(resolveApp("wechat").sourceAdapter).toBe("wechat");
|
|
51
|
-
expect(resolveApp("kuaishou").sourceAdapter).toBe("social-kuaishou");
|
|
52
|
-
expect(resolveApp("bogusapp").sourceAdapter).toBe("salvage:bogusapp");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("buildSalvageEvents tags per-app source + builds valid message events", () => {
|
|
56
|
-
const recs = [{ rowid: "1", cols: ["u1", "conv-1", 111, "hello 世界", 1700000000000] }];
|
|
57
|
-
const built = buildSalvageEvents(recs, { app: "toutiao", columns: COLUMNS, now: 1700000099000 });
|
|
58
|
-
expect(built.events.length).toBe(1);
|
|
59
|
-
const e = built.events[0];
|
|
60
|
-
expect(e.source.adapter).toBe("social-toutiao");
|
|
61
|
-
expect(e.source.capturedBy).toBe("sqlite"); // schema enum; provenance in extra.salvaged
|
|
62
|
-
expect(e.subtype).toBe("message");
|
|
63
|
-
expect(e.content.text).toBe("hello 世界");
|
|
64
|
-
expect(e.extra.platform).toBe("toutiao");
|
|
65
|
-
expect(e.extra.salvaged).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("salvageDumpToVault ingests with douyin source + events are searchable", () => {
|
|
69
|
-
const r = salvageDumpToVault(vault, dumpPath, { app: "douyin", columns: COLUMNS, now: 1700000099000 });
|
|
70
|
-
expect(r.app).toBe("douyin");
|
|
71
|
-
expect(r.sourceAdapter).toBe("social-douyin");
|
|
72
|
-
expect(r.ingested).toBe(2);
|
|
73
|
-
// events landed under the correct source + are searchable
|
|
74
|
-
const events = vault.queryEvents({ limit: 100 }) || [];
|
|
75
|
-
const douyin = events.filter((e) => e.source && e.source.adapter === "social-douyin");
|
|
76
|
-
expect(douyin.length).toBe(2);
|
|
77
|
-
const texts = douyin.map((e) => e.content && e.content.text).sort();
|
|
78
|
-
expect(texts).toContain("收到 👌"); // UTF-8 emoji survives
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("same dump under a different app tags a different source (no cross-attribution)", () => {
|
|
82
|
-
const r = salvageDumpToVault(vault, dumpPath, { app: "toutiao", columns: COLUMNS, now: 1700000099000 });
|
|
83
|
-
expect(r.sourceAdapter).toBe("social-toutiao");
|
|
84
|
-
expect(r.ingested).toBe(2);
|
|
85
|
-
const events = vault.queryEvents({ limit: 100 }) || [];
|
|
86
|
-
expect(events.filter((e) => e.source && e.source.adapter === "social-toutiao").length).toBe(2);
|
|
87
|
-
// douyin events from prior test remain distinct
|
|
88
|
-
expect(events.filter((e) => e.source && e.source.adapter === "social-douyin").length).toBe(2);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("re-ingesting the same dump dedups (stable originalId)", () => {
|
|
92
|
-
const before = (vault.queryEvents({ limit: 200 }) || []).length;
|
|
93
|
-
salvageDumpToVault(vault, dumpPath, { app: "douyin", columns: COLUMNS, now: 1700000099000 });
|
|
94
|
-
const after = (vault.queryEvents({ limit: 200 }) || []).length;
|
|
95
|
-
expect(after).toBe(before); // ON CONFLICT(source_adapter, source_original_id) updates, no dupes
|
|
96
|
-
});
|
|
97
|
-
});
|