@chainlesschain/personal-data-hub 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +211 -0
- package/__tests__/adapters/ai-chat-health-checker.test.js +262 -0
- package/__tests__/adapters/ai-chat-history.test.js +396 -0
- package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
- package/__tests__/adapters/ai-chat-vendors.test.js +874 -0
- package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
- package/__tests__/adapters/email-adapter.test.js +138 -1
- package/__tests__/adapters/email-classifier.test.js +347 -0
- package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
- package/__tests__/adapters/email-retry-progress.test.js +294 -0
- package/__tests__/adapters/email-templates.test.js +699 -0
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +269 -0
- package/__tests__/adapters/system-data-adapter.test.js +440 -0
- package/__tests__/adapters/system-data-android-ingest.test.js +144 -0
- package/__tests__/adapters/system-data-android.test.js +387 -0
- package/__tests__/adapters/system-data-disclosure.test.js +153 -0
- package/__tests__/adapters/wechat-bootstrap.test.js +240 -0
- package/__tests__/adapters/wechat-env-probe.test.js +162 -0
- package/__tests__/adapters/wechat-frida-agent.test.js +191 -0
- package/__tests__/adapters/wechat-frida-integration.test.js +149 -0
- package/__tests__/adapters/wechat-frida-key-provider.test.js +188 -0
- package/__tests__/adapters/wechat-md5-key-provider.test.js +101 -0
- package/__tests__/analysis-skills.test.js +556 -0
- package/__tests__/analysis.test.js +329 -1
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +213 -0
- package/__tests__/e2e/full-user-journey.test.js +188 -0
- package/__tests__/entity-resolver-ingest-hook.test.js +177 -0
- package/__tests__/entity-resolver-stages.test.js +411 -0
- package/__tests__/entity-resolver-vault.test.js +246 -0
- package/__tests__/entity-resolver.test.js +526 -0
- package/__tests__/fixtures/entity-resolver-200-mock.json +96 -0
- package/__tests__/integration/ai-chat-history-registry.test.js +228 -0
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +282 -0
- package/__tests__/integration/cross-adapter-pipelines.test.js +396 -0
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +390 -0
- package/__tests__/longtail-adapters.test.js +217 -0
- package/__tests__/mobile-extractor.test.js +288 -0
- package/__tests__/registry.test.js +4 -2
- package/__tests__/shopping-adapters.test.js +296 -0
- package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
- package/__tests__/sidecar-supervisor.test.js +120 -0
- package/__tests__/social-adapters.test.js +206 -0
- package/__tests__/travel-adapters.test.js +325 -0
- package/__tests__/vault.test.js +3 -3
- package/__tests__/wechat-adapter.test.js +476 -0
- package/__tests__/whatsapp-adapter.test.js +135 -0
- package/lib/adapter-spec.js +12 -0
- package/lib/adapters/_python-sidecar-base.js +207 -0
- package/lib/adapters/ai-chat-history/ai-chat-adapter.js +374 -0
- package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
- package/lib/adapters/ai-chat-history/cookie-capture-spec.js +331 -0
- package/lib/adapters/ai-chat-history/health-checker.js +210 -0
- package/lib/adapters/ai-chat-history/http-client.js +211 -0
- package/lib/adapters/ai-chat-history/index.js +28 -0
- package/lib/adapters/ai-chat-history/schema-map.js +258 -0
- package/lib/adapters/ai-chat-history/vendor-spec.js +86 -0
- package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
- package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
- package/lib/adapters/ai-chat-history/vendors/doubao.js +255 -0
- package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
- package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
- package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
- package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
- package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
- package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
- package/lib/adapters/ai-chat-history/wizard-controller.js +473 -0
- package/lib/adapters/alipay-bill/alipay-bill-adapter.js +311 -0
- package/lib/adapters/alipay-bill/counterparty.js +129 -0
- package/lib/adapters/alipay-bill/csv-parser.js +217 -0
- package/lib/adapters/alipay-bill/index.js +41 -0
- package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
- package/lib/adapters/email-imap/classifier.js +495 -0
- package/lib/adapters/email-imap/email-adapter.js +419 -8
- package/lib/adapters/email-imap/index.js +42 -0
- package/lib/adapters/email-imap/pdf-extractor.js +192 -0
- package/lib/adapters/email-imap/templates/bill.js +232 -0
- package/lib/adapters/email-imap/templates/government.js +120 -0
- package/lib/adapters/email-imap/templates/index.js +78 -0
- package/lib/adapters/email-imap/templates/order.js +186 -0
- package/lib/adapters/email-imap/templates/other.js +114 -0
- package/lib/adapters/email-imap/templates/register.js +113 -0
- package/lib/adapters/email-imap/templates/travel.js +157 -0
- package/lib/adapters/email-imap/templates/utils.js +275 -0
- package/lib/adapters/email-imap/transactions.js +234 -0
- package/lib/adapters/messaging-qq/index.js +158 -0
- package/lib/adapters/messaging-telegram/index.js +142 -0
- package/lib/adapters/messaging-whatsapp/index.js +189 -0
- package/lib/adapters/shopping-base/index.js +208 -0
- package/lib/adapters/shopping-jd/index.js +150 -0
- package/lib/adapters/shopping-meituan/index.js +154 -0
- package/lib/adapters/shopping-taobao/index.js +176 -0
- package/lib/adapters/social-bilibili/index.js +171 -0
- package/lib/adapters/social-douyin/index.js +116 -0
- package/lib/adapters/social-kuaishou/index.js +237 -0
- package/lib/adapters/social-toutiao/index.js +236 -0
- package/lib/adapters/social-weibo/index.js +164 -0
- package/lib/adapters/social-xiaohongshu/index.js +96 -0
- package/lib/adapters/system-data/disclosure.js +166 -0
- package/lib/adapters/system-data/index.js +34 -0
- package/lib/adapters/system-data/system-data-adapter.js +344 -0
- package/lib/adapters/system-data-android/adapter.js +348 -0
- package/lib/adapters/system-data-android/index.js +76 -0
- package/lib/adapters/travel-12306/index.js +151 -0
- package/lib/adapters/travel-amap/index.js +164 -0
- package/lib/adapters/travel-baidu-map/index.js +162 -0
- package/lib/adapters/travel-base/index.js +240 -0
- package/lib/adapters/travel-ctrip/index.js +151 -0
- package/lib/adapters/wechat/bootstrap.js +146 -0
- package/lib/adapters/wechat/content-parser.js +326 -0
- package/lib/adapters/wechat/db-reader.js +209 -0
- package/lib/adapters/wechat/env-probe.js +218 -0
- package/lib/adapters/wechat/frida-agent/loader.js +67 -0
- package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +126 -0
- package/lib/adapters/wechat/index.js +37 -0
- package/lib/adapters/wechat/key-extractor.js +158 -0
- package/lib/adapters/wechat/key-providers/frida-key-provider.js +244 -0
- package/lib/adapters/wechat/key-providers/index.js +22 -0
- package/lib/adapters/wechat/key-providers/key-provider-base.js +44 -0
- package/lib/adapters/wechat/key-providers/md5-key-provider.js +81 -0
- package/lib/adapters/wechat/normalize.js +220 -0
- package/lib/adapters/wechat/wechat-adapter.js +205 -0
- package/lib/analysis-skills/base.js +113 -0
- package/lib/analysis-skills/footprint.js +167 -0
- package/lib/analysis-skills/index.js +58 -0
- package/lib/analysis-skills/interests.js +161 -0
- package/lib/analysis-skills/relations.js +226 -0
- package/lib/analysis-skills/spending.js +219 -0
- package/lib/analysis-skills/timeline.js +167 -0
- package/lib/analysis.js +191 -2
- package/lib/entity-resolver/embedding-stage.js +198 -0
- package/lib/entity-resolver/entity-resolver.js +384 -0
- package/lib/entity-resolver/index.js +42 -0
- package/lib/entity-resolver/llm-stage.js +191 -0
- package/lib/entity-resolver/rule-stage.js +208 -0
- package/lib/entity-resolver/worker.js +149 -0
- package/lib/index.js +131 -0
- package/lib/migrations.js +73 -0
- package/lib/mobile-extractor/android.js +193 -0
- package/lib/mobile-extractor/index.js +9 -0
- package/lib/mobile-extractor/ios.js +223 -0
- package/lib/prompt-builder.js +11 -1
- package/lib/query-parser.js +7 -1
- package/lib/registry.js +42 -0
- package/lib/sidecar/index.js +15 -0
- package/lib/sidecar/supervisor.js +359 -0
- package/lib/vault.js +343 -0
- package/package.json +36 -3
- package/scripts/_make-fixture-all.js +126 -0
- package/scripts/_make-fixture-contacts.js +84 -0
- package/scripts/evaluate-entity-resolver.js +213 -0
- package/scripts/smoke-phase-5-5.js +196 -0
- package/scripts/smoke-phase-5-7.js +181 -0
- package/scripts/smoke-system-data-contacts.js +309 -0
- package/scripts/smoke-system-data.js +312 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration tests — cross-adapter pipelines.
|
|
5
|
+
*
|
|
6
|
+
* Exercises the full ingest → vault → EntityResolver → analysis skill
|
|
7
|
+
* flow with multiple adapters feeding the same vault. Each test scenario
|
|
8
|
+
* mirrors a real-world user journey from architecture-doc §7.1.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
const os = require("node:os");
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
LocalVault, generateKeyHex, AdapterRegistry,
|
|
19
|
+
EntityResolver,
|
|
20
|
+
SpendingSkill, RelationsSkill, FootprintSkill, TimelineSkill,
|
|
21
|
+
} = require("../../lib");
|
|
22
|
+
|
|
23
|
+
function makeRig() {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pdh-int-"));
|
|
25
|
+
const vault = new LocalVault({ path: path.join(dir, "v.db"), key: generateKeyHex() });
|
|
26
|
+
vault.open();
|
|
27
|
+
const resolver = new EntityResolver({ vault });
|
|
28
|
+
const registry = new AdapterRegistry({ vault, entityResolver: resolver });
|
|
29
|
+
return { vault, resolver, registry, dir };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cleanup({ vault, dir }) {
|
|
33
|
+
try { vault.close(); } catch (_e) {}
|
|
34
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Minimal adapter for integration tests — yields pre-baked normalized
|
|
38
|
+
// batches so we don't depend on real Email/Alipay/etc subprocesses
|
|
39
|
+
class FixtureAdapter {
|
|
40
|
+
constructor(name, batches, opts = {}) {
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.version = "1.0.0";
|
|
43
|
+
this.capabilities = ["sync:fixture"];
|
|
44
|
+
this.extractMode = opts.extractMode || "web-api";
|
|
45
|
+
this.rateLimits = {};
|
|
46
|
+
this.dataDisclosure = { fields: [], sensitivity: "low", legalGate: false };
|
|
47
|
+
this._batches = batches;
|
|
48
|
+
}
|
|
49
|
+
async authenticate() { return { ok: true }; }
|
|
50
|
+
async healthCheck() { return { ok: true, lastChecked: Date.now() }; }
|
|
51
|
+
async *sync() {
|
|
52
|
+
for (const b of this._batches) {
|
|
53
|
+
yield {
|
|
54
|
+
adapter: this.name,
|
|
55
|
+
originalId: b.originalId,
|
|
56
|
+
capturedAt: b.capturedAt || Date.now(),
|
|
57
|
+
payload: { batch: b.batch },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
normalize(raw) { return raw.payload.batch; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function source(adapter, originalId) {
|
|
65
|
+
return {
|
|
66
|
+
adapter, adapterVersion: "1.0.0", originalId,
|
|
67
|
+
capturedAt: Date.now(), capturedBy: "api",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Scenario 1: Email + Alipay → cross-source person merge ─────────────
|
|
72
|
+
|
|
73
|
+
describe("Integration — Email + Alipay → EntityResolver merges same person", () => {
|
|
74
|
+
let rig;
|
|
75
|
+
afterEach(() => cleanup(rig));
|
|
76
|
+
|
|
77
|
+
it("same email across both adapters → one merge group, analysis sees combined spending", async () => {
|
|
78
|
+
rig = makeRig();
|
|
79
|
+
const src = (a, oid) => source(a, oid);
|
|
80
|
+
|
|
81
|
+
// Email adapter: mom@163.com sends an order confirmation
|
|
82
|
+
const emailBatch = {
|
|
83
|
+
events: [{
|
|
84
|
+
id: "evt-email-1", type: "event", subtype: "message",
|
|
85
|
+
occurredAt: Date.parse("2026-05-15T10:00:00Z"),
|
|
86
|
+
actor: "person-email-mom",
|
|
87
|
+
content: { title: "妈 forwarded a link", text: "看看这个" },
|
|
88
|
+
ingestedAt: Date.now(),
|
|
89
|
+
source: src("email-imap", "msg-1"),
|
|
90
|
+
}],
|
|
91
|
+
persons: [{
|
|
92
|
+
id: "person-email-mom",
|
|
93
|
+
type: "person", subtype: "contact",
|
|
94
|
+
names: ["妈"],
|
|
95
|
+
identifiers: { email: ["mom@163.com"] },
|
|
96
|
+
ingestedAt: Date.now(),
|
|
97
|
+
source: src("email-imap", "person-1"),
|
|
98
|
+
}],
|
|
99
|
+
places: [], items: [], topics: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Alipay adapter: 转账给 with same email associated
|
|
103
|
+
const alipayBatch = {
|
|
104
|
+
events: [{
|
|
105
|
+
id: "evt-alipay-1", type: "event", subtype: "transfer",
|
|
106
|
+
occurredAt: Date.parse("2026-05-20T12:00:00Z"),
|
|
107
|
+
actor: "person-self",
|
|
108
|
+
participants: ["person-self", "person-alipay-chen"],
|
|
109
|
+
content: {
|
|
110
|
+
title: "转账给 陈XX",
|
|
111
|
+
amount: { value: 500, currency: "CNY", direction: "out" },
|
|
112
|
+
},
|
|
113
|
+
ingestedAt: Date.now(),
|
|
114
|
+
source: src("alipay-bill", "tx-1"),
|
|
115
|
+
}],
|
|
116
|
+
persons: [{
|
|
117
|
+
id: "person-alipay-chen",
|
|
118
|
+
type: "person", subtype: "contact",
|
|
119
|
+
names: ["陈XX"],
|
|
120
|
+
identifiers: { email: ["mom@163.com"] }, // same email → R1 merge
|
|
121
|
+
ingestedAt: Date.now(),
|
|
122
|
+
source: src("alipay-bill", "person-1"),
|
|
123
|
+
}],
|
|
124
|
+
places: [], items: [], topics: [],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
rig.registry.register(new FixtureAdapter("email-imap", [
|
|
128
|
+
{ originalId: "msg-1", batch: emailBatch },
|
|
129
|
+
]));
|
|
130
|
+
rig.registry.register(new FixtureAdapter("alipay-bill", [
|
|
131
|
+
{ originalId: "tx-1", batch: alipayBatch },
|
|
132
|
+
]));
|
|
133
|
+
|
|
134
|
+
const r1 = await rig.registry.syncAdapter("email-imap");
|
|
135
|
+
const r2 = await rig.registry.syncAdapter("alipay-bill");
|
|
136
|
+
|
|
137
|
+
expect(r1.status).toBe("ok");
|
|
138
|
+
expect(r2.status).toBe("ok");
|
|
139
|
+
// EntityResolver auto-merged via email rule
|
|
140
|
+
expect(r2.entityResolver.sameImmediate).toBeGreaterThanOrEqual(1);
|
|
141
|
+
|
|
142
|
+
// Both persons now in same merge group
|
|
143
|
+
const members = rig.vault.getMergeGroupMembers("person-email-mom").sort();
|
|
144
|
+
expect(members).toEqual(["person-alipay-chen", "person-email-mom"]);
|
|
145
|
+
|
|
146
|
+
// RelationsSkill with merge-group expansion picks up BOTH events
|
|
147
|
+
const skill = new RelationsSkill({ vault: rig.vault });
|
|
148
|
+
const result = await skill.run({ personId: "person-email-mom" });
|
|
149
|
+
expect(result.profile.totalInteractions).toBeGreaterThanOrEqual(2);
|
|
150
|
+
expect(result.profile.totalSpend).toBe(500);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("non-overlapping persons stay separate", async () => {
|
|
154
|
+
rig = makeRig();
|
|
155
|
+
const src = (a, oid) => source(a, oid);
|
|
156
|
+
|
|
157
|
+
const emailBatch = {
|
|
158
|
+
events: [], topics: [], places: [], items: [],
|
|
159
|
+
persons: [{
|
|
160
|
+
id: "p-alice", type: "person", subtype: "contact",
|
|
161
|
+
names: ["Alice"], identifiers: { email: ["alice@x.com"] },
|
|
162
|
+
ingestedAt: Date.now(), source: src("email-imap", "p1"),
|
|
163
|
+
}],
|
|
164
|
+
};
|
|
165
|
+
const alipayBatch = {
|
|
166
|
+
events: [], topics: [], places: [], items: [],
|
|
167
|
+
persons: [{
|
|
168
|
+
id: "p-bob", type: "person", subtype: "contact",
|
|
169
|
+
names: ["Bob"], identifiers: { phone: ["13999998888"] },
|
|
170
|
+
ingestedAt: Date.now(), source: src("alipay-bill", "p1"),
|
|
171
|
+
}],
|
|
172
|
+
};
|
|
173
|
+
rig.registry.register(new FixtureAdapter("email-imap", [{ originalId: "1", batch: emailBatch }]));
|
|
174
|
+
rig.registry.register(new FixtureAdapter("alipay-bill", [{ originalId: "1", batch: alipayBatch }]));
|
|
175
|
+
await rig.registry.syncAdapter("email-imap");
|
|
176
|
+
await rig.registry.syncAdapter("alipay-bill");
|
|
177
|
+
// No merge group should form
|
|
178
|
+
expect(rig.vault.stats().mergeGroups).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── Scenario 2: Multi-adapter spending analysis ────────────────────────
|
|
183
|
+
|
|
184
|
+
describe("Integration — SpendingSkill aggregates across adapters", () => {
|
|
185
|
+
let rig;
|
|
186
|
+
afterEach(() => cleanup(rig));
|
|
187
|
+
|
|
188
|
+
it("sums spend from Alipay + Shopping (Taobao) + Travel (12306)", async () => {
|
|
189
|
+
rig = makeRig();
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
const src = (a, oid) => source(a, oid);
|
|
192
|
+
|
|
193
|
+
function payEvent(adapter, id, amount, merchant, subtype = "payment") {
|
|
194
|
+
return {
|
|
195
|
+
events: [{
|
|
196
|
+
id, type: "event", subtype,
|
|
197
|
+
occurredAt: now - 24 * 3600_000,
|
|
198
|
+
actor: "person-self",
|
|
199
|
+
participants: ["person-self", `person-${adapter}-${merchant}`],
|
|
200
|
+
content: { title: `${merchant} 消费`, amount: { value: amount, currency: "CNY", direction: "out" } },
|
|
201
|
+
ingestedAt: now,
|
|
202
|
+
source: src(adapter, id),
|
|
203
|
+
extra: { counterparty: merchant },
|
|
204
|
+
}],
|
|
205
|
+
persons: [{
|
|
206
|
+
id: `person-${adapter}-${merchant}`, type: "person", subtype: "merchant",
|
|
207
|
+
names: [merchant], identifiers: {},
|
|
208
|
+
ingestedAt: now, source: src(adapter, `p-${merchant}`),
|
|
209
|
+
}],
|
|
210
|
+
places: [], items: [], topics: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
rig.registry.register(new FixtureAdapter("alipay-bill", [
|
|
215
|
+
{ originalId: "alipay-1", batch: payEvent("alipay-bill", "evt-1", 38.50, "美团") },
|
|
216
|
+
{ originalId: "alipay-2", batch: payEvent("alipay-bill", "evt-2", 299, "淘宝") },
|
|
217
|
+
]));
|
|
218
|
+
rig.registry.register(new FixtureAdapter("shopping-taobao", [
|
|
219
|
+
{ originalId: "tb-1", batch: payEvent("shopping-taobao", "evt-3", 999, "Apple官方", "order") },
|
|
220
|
+
]));
|
|
221
|
+
rig.registry.register(new FixtureAdapter("travel-12306", [
|
|
222
|
+
{ originalId: "12306-1", batch: payEvent("travel-12306", "evt-4", 553, "12306", "payment") },
|
|
223
|
+
]));
|
|
224
|
+
|
|
225
|
+
await rig.registry.syncAdapter("alipay-bill");
|
|
226
|
+
await rig.registry.syncAdapter("shopping-taobao");
|
|
227
|
+
await rig.registry.syncAdapter("travel-12306");
|
|
228
|
+
|
|
229
|
+
const skill = new SpendingSkill({ vault: rig.vault });
|
|
230
|
+
const result = await skill.run({});
|
|
231
|
+
expect(result.summary.eventCount).toBe(4);
|
|
232
|
+
expect(result.summary.totalSpend).toBeCloseTo(38.50 + 299 + 999 + 553, 2);
|
|
233
|
+
expect(result.summary.uniqueCounterparties).toBe(4);
|
|
234
|
+
// Top merchant should be Apple
|
|
235
|
+
expect(result.breakdown[0].key).toBe("Apple官方");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ─── Scenario 3: Travel adapters + footprint ────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("Integration — Travel adapters → FootprintSkill", () => {
|
|
242
|
+
let rig;
|
|
243
|
+
afterEach(() => cleanup(rig));
|
|
244
|
+
|
|
245
|
+
it("Amap routes + 12306 tickets feed unified footprint", async () => {
|
|
246
|
+
rig = makeRig();
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const src = (a, oid) => source(a, oid);
|
|
249
|
+
|
|
250
|
+
function tripEvent(adapter, id, from, to, ts) {
|
|
251
|
+
return {
|
|
252
|
+
events: [{
|
|
253
|
+
id, type: "event", subtype: "trip",
|
|
254
|
+
occurredAt: ts,
|
|
255
|
+
actor: "person-self",
|
|
256
|
+
content: { title: `${from} → ${to}` },
|
|
257
|
+
ingestedAt: now,
|
|
258
|
+
source: src(adapter, id),
|
|
259
|
+
extra: { from, to },
|
|
260
|
+
}],
|
|
261
|
+
persons: [], places: [], items: [], topics: [],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
rig.registry.register(new FixtureAdapter("travel-12306", [
|
|
266
|
+
{ originalId: "t1", batch: tripEvent("travel-12306", "evt-1", "上海虹桥", "北京南", now - 7 * 24 * 3600_000) },
|
|
267
|
+
{ originalId: "t2", batch: tripEvent("travel-12306", "evt-2", "北京南", "上海虹桥", now - 5 * 24 * 3600_000) },
|
|
268
|
+
]));
|
|
269
|
+
rig.registry.register(new FixtureAdapter("travel-amap", [
|
|
270
|
+
{ originalId: "a1", batch: tripEvent("travel-amap", "evt-3", "家", "公司", now - 4 * 24 * 3600_000) },
|
|
271
|
+
]));
|
|
272
|
+
|
|
273
|
+
await rig.registry.syncAdapter("travel-12306");
|
|
274
|
+
await rig.registry.syncAdapter("travel-amap");
|
|
275
|
+
|
|
276
|
+
const skill = new FootprintSkill({ vault: rig.vault });
|
|
277
|
+
const result = await skill.run({});
|
|
278
|
+
expect(result.summary.totalTrips).toBeGreaterThanOrEqual(3);
|
|
279
|
+
expect(result.topPlaces.length).toBeGreaterThanOrEqual(3);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ─── Scenario 4: Timeline weaves multiple adapter sources ──────────────
|
|
284
|
+
|
|
285
|
+
describe("Integration — TimelineSkill weaves messaging + payment events", () => {
|
|
286
|
+
let rig;
|
|
287
|
+
afterEach(() => cleanup(rig));
|
|
288
|
+
|
|
289
|
+
it("chronological merge of WeChat + Alipay events", async () => {
|
|
290
|
+
rig = makeRig();
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const src = (a, oid) => source(a, oid);
|
|
293
|
+
|
|
294
|
+
const wechatBatch = {
|
|
295
|
+
events: [{
|
|
296
|
+
id: "wc-1", type: "event", subtype: "message",
|
|
297
|
+
occurredAt: now - 1 * 3600_000, // 1h ago
|
|
298
|
+
actor: "person-wechat-friend",
|
|
299
|
+
content: { title: "妈: 吃饭了么", text: "吃饭了么" },
|
|
300
|
+
ingestedAt: now,
|
|
301
|
+
source: src("wechat", "msg-1"),
|
|
302
|
+
}],
|
|
303
|
+
persons: [], places: [], items: [], topics: [],
|
|
304
|
+
};
|
|
305
|
+
const alipayBatch = {
|
|
306
|
+
events: [{
|
|
307
|
+
id: "ap-1", type: "event", subtype: "payment",
|
|
308
|
+
occurredAt: now - 30 * 60_000, // 30min ago
|
|
309
|
+
actor: "person-self",
|
|
310
|
+
content: { title: "美团外卖", amount: { value: 30, currency: "CNY", direction: "out" } },
|
|
311
|
+
ingestedAt: now,
|
|
312
|
+
source: src("alipay-bill", "tx-1"),
|
|
313
|
+
extra: { counterparty: "美团" },
|
|
314
|
+
}],
|
|
315
|
+
persons: [], places: [], items: [], topics: [],
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
rig.registry.register(new FixtureAdapter("wechat", [{ originalId: "wc-1", batch: wechatBatch }]));
|
|
319
|
+
rig.registry.register(new FixtureAdapter("alipay-bill", [{ originalId: "tx-1", batch: alipayBatch }]));
|
|
320
|
+
await rig.registry.syncAdapter("wechat");
|
|
321
|
+
await rig.registry.syncAdapter("alipay-bill");
|
|
322
|
+
|
|
323
|
+
const skill = new TimelineSkill({ vault: rig.vault });
|
|
324
|
+
const result = await skill.run({ sinceDays: 1 });
|
|
325
|
+
expect(result.entries.length).toBe(2);
|
|
326
|
+
// WeChat msg older → first; Alipay payment newer → second
|
|
327
|
+
expect(result.entries[0].adapter).toBe("wechat");
|
|
328
|
+
expect(result.entries[1].adapter).toBe("alipay-bill");
|
|
329
|
+
expect(result.summary.byAdapter.wechat).toBe(1);
|
|
330
|
+
expect(result.summary.byAdapter["alipay-bill"]).toBe(1);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ─── Scenario 5: EntityResolver + analysis cascade ─────────────────────
|
|
335
|
+
|
|
336
|
+
describe("Integration — EntityResolver review queue + manual merge unlocks combined view", () => {
|
|
337
|
+
let rig;
|
|
338
|
+
afterEach(() => cleanup(rig));
|
|
339
|
+
|
|
340
|
+
it("uncertain pair queued → user merges → analysis picks up combined events", async () => {
|
|
341
|
+
rig = makeRig();
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const src = (a, oid) => source(a, oid);
|
|
344
|
+
|
|
345
|
+
// Two "张三" persons across email + alipay with no overlapping ids
|
|
346
|
+
const emailBatch = {
|
|
347
|
+
events: [{
|
|
348
|
+
id: "evt-1", type: "event", subtype: "message",
|
|
349
|
+
occurredAt: now, actor: "person-email-zs",
|
|
350
|
+
content: { title: "msg" }, ingestedAt: now,
|
|
351
|
+
source: src("email-imap", "m1"),
|
|
352
|
+
}],
|
|
353
|
+
persons: [{
|
|
354
|
+
id: "person-email-zs", type: "person", subtype: "contact",
|
|
355
|
+
names: ["张三"], identifiers: {},
|
|
356
|
+
ingestedAt: now, source: src("email-imap", "p1"),
|
|
357
|
+
}],
|
|
358
|
+
places: [], items: [], topics: [],
|
|
359
|
+
};
|
|
360
|
+
const alipayBatch = {
|
|
361
|
+
events: [{
|
|
362
|
+
id: "evt-2", type: "event", subtype: "transfer",
|
|
363
|
+
occurredAt: now, actor: "person-self",
|
|
364
|
+
participants: ["person-self", "person-alipay-zs"],
|
|
365
|
+
content: { title: "转账", amount: { value: 100, currency: "CNY", direction: "out" } },
|
|
366
|
+
ingestedAt: now,
|
|
367
|
+
source: src("alipay-bill", "t1"),
|
|
368
|
+
}],
|
|
369
|
+
persons: [{
|
|
370
|
+
id: "person-alipay-zs", type: "person", subtype: "contact",
|
|
371
|
+
names: ["张三"], identifiers: {},
|
|
372
|
+
ingestedAt: now, source: src("alipay-bill", "p1"),
|
|
373
|
+
}],
|
|
374
|
+
places: [], items: [], topics: [],
|
|
375
|
+
};
|
|
376
|
+
rig.registry.register(new FixtureAdapter("email-imap", [{ originalId: "1", batch: emailBatch }]));
|
|
377
|
+
rig.registry.register(new FixtureAdapter("alipay-bill", [{ originalId: "1", batch: alipayBatch }]));
|
|
378
|
+
await rig.registry.syncAdapter("email-imap");
|
|
379
|
+
const r2 = await rig.registry.syncAdapter("alipay-bill");
|
|
380
|
+
|
|
381
|
+
// Without identifier overlap, rule stage = uncertain → queued
|
|
382
|
+
expect(r2.entityResolver.enqueued).toBeGreaterThanOrEqual(1);
|
|
383
|
+
expect(rig.vault.stats().mergeGroups).toBe(0);
|
|
384
|
+
|
|
385
|
+
// User manually merges via UI
|
|
386
|
+
rig.resolver.manualMerge({ aId: "person-email-zs", bId: "person-alipay-zs" });
|
|
387
|
+
expect(rig.vault.getMergeGroupMembers("person-email-zs").sort()).toEqual(
|
|
388
|
+
["person-alipay-zs", "person-email-zs"]
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Now relations skill sees combined view
|
|
392
|
+
const skill = new RelationsSkill({ vault: rig.vault });
|
|
393
|
+
const result = await skill.run({ personId: "person-email-zs" });
|
|
394
|
+
expect(result.profile.totalInteractions).toBe(2);
|
|
395
|
+
});
|
|
396
|
+
});
|