@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,219 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
deriveEventTriples,
|
|
7
|
-
derivePersonTriples,
|
|
8
|
-
derivePlaceTriples,
|
|
9
|
-
deriveItemTriples,
|
|
10
|
-
deriveTopicTriples,
|
|
11
|
-
deriveBatchTriples,
|
|
12
|
-
deriveEntityTriples,
|
|
13
|
-
triple,
|
|
14
|
-
} = require("../lib/kg-derive");
|
|
15
|
-
|
|
16
|
-
const { newId } = require("../lib/ids");
|
|
17
|
-
|
|
18
|
-
const sourceOk = (adapter = "test") => ({
|
|
19
|
-
adapter,
|
|
20
|
-
adapterVersion: "0.1.0",
|
|
21
|
-
capturedAt: Date.now(),
|
|
22
|
-
capturedBy: "api",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("triple()", () => {
|
|
26
|
-
it("makes an object triple with subject + predicate + object", () => {
|
|
27
|
-
const t = triple("a", "knows", { object: "b" });
|
|
28
|
-
expect(t).toEqual({ subject: "a", predicate: "knows", object: "b" });
|
|
29
|
-
});
|
|
30
|
-
it("makes a literal triple", () => {
|
|
31
|
-
const t = triple("a", "name", { literal: "Alice" });
|
|
32
|
-
expect(t).toEqual({ subject: "a", predicate: "name", literal: "Alice" });
|
|
33
|
-
});
|
|
34
|
-
it("skips object/literal when null/undefined", () => {
|
|
35
|
-
const t = triple("a", "p", {});
|
|
36
|
-
expect(t).toEqual({ subject: "a", predicate: "p" });
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("deriveEventTriples", () => {
|
|
41
|
-
it("emits the minimal set: rdf:type / subtype / occurred-at / source", () => {
|
|
42
|
-
const id = newId();
|
|
43
|
-
const e = {
|
|
44
|
-
id,
|
|
45
|
-
type: "event",
|
|
46
|
-
subtype: "message",
|
|
47
|
-
occurredAt: 1700000000000,
|
|
48
|
-
ingestedAt: 1700000000001,
|
|
49
|
-
content: { text: "hi" },
|
|
50
|
-
source: sourceOk("wechat"),
|
|
51
|
-
};
|
|
52
|
-
const ts = deriveEventTriples(e);
|
|
53
|
-
expect(ts).toContainEqual({ subject: id, predicate: "rdf:type", literal: "event" });
|
|
54
|
-
expect(ts).toContainEqual({ subject: id, predicate: "subtype", literal: "message" });
|
|
55
|
-
expect(ts).toContainEqual({ subject: id, predicate: "occurred-at", literal: 1700000000000 });
|
|
56
|
-
expect(ts).toContainEqual({ subject: id, predicate: "source", literal: "wechat" });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("emits 'by' for actor and 'involves' for each participant", () => {
|
|
60
|
-
const id = newId();
|
|
61
|
-
const e = {
|
|
62
|
-
id,
|
|
63
|
-
type: "event",
|
|
64
|
-
subtype: "payment",
|
|
65
|
-
occurredAt: 1,
|
|
66
|
-
ingestedAt: 1,
|
|
67
|
-
actor: "person-self",
|
|
68
|
-
participants: ["person-self", "person-mom"],
|
|
69
|
-
content: { text: "transfer" },
|
|
70
|
-
source: sourceOk(),
|
|
71
|
-
};
|
|
72
|
-
const ts = deriveEventTriples(e);
|
|
73
|
-
expect(ts).toContainEqual({ subject: id, predicate: "by", object: "person-self" });
|
|
74
|
-
expect(ts).toContainEqual({ subject: id, predicate: "involves", object: "person-self" });
|
|
75
|
-
expect(ts).toContainEqual({ subject: id, predicate: "involves", object: "person-mom" });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("emits amount components when content.amount present", () => {
|
|
79
|
-
const id = newId();
|
|
80
|
-
const e = {
|
|
81
|
-
id,
|
|
82
|
-
type: "event",
|
|
83
|
-
subtype: "order",
|
|
84
|
-
occurredAt: 1,
|
|
85
|
-
ingestedAt: 1,
|
|
86
|
-
content: { amount: { value: 288.5, currency: "CNY", direction: "out" } },
|
|
87
|
-
source: sourceOk(),
|
|
88
|
-
};
|
|
89
|
-
const ts = deriveEventTriples(e);
|
|
90
|
-
expect(ts).toContainEqual({ subject: id, predicate: "amount-value", literal: 288.5 });
|
|
91
|
-
expect(ts).toContainEqual({ subject: id, predicate: "amount-currency", literal: "CNY" });
|
|
92
|
-
expect(ts).toContainEqual({ subject: id, predicate: "amount-direction", literal: "out" });
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("derivePersonTriples", () => {
|
|
97
|
-
it("emits rdf:type + subtype + every name + identifier", () => {
|
|
98
|
-
const id = newId();
|
|
99
|
-
const p = {
|
|
100
|
-
id,
|
|
101
|
-
type: "person",
|
|
102
|
-
subtype: "contact",
|
|
103
|
-
names: ["妈妈", "陈某某"],
|
|
104
|
-
identifiers: { phone: ["13800001111", "13900002222"], wechatId: "wxid_xyz" },
|
|
105
|
-
ingestedAt: 1,
|
|
106
|
-
source: sourceOk(),
|
|
107
|
-
};
|
|
108
|
-
const ts = derivePersonTriples(p);
|
|
109
|
-
expect(ts).toContainEqual({ subject: id, predicate: "rdf:type", literal: "person" });
|
|
110
|
-
expect(ts).toContainEqual({ subject: id, predicate: "subtype", literal: "contact" });
|
|
111
|
-
expect(ts).toContainEqual({ subject: id, predicate: "has-name", literal: "妈妈" });
|
|
112
|
-
expect(ts).toContainEqual({ subject: id, predicate: "has-name", literal: "陈某某" });
|
|
113
|
-
expect(ts).toContainEqual({ subject: id, predicate: "id:phone", literal: "13800001111" });
|
|
114
|
-
expect(ts).toContainEqual({ subject: id, predicate: "id:phone", literal: "13900002222" });
|
|
115
|
-
expect(ts).toContainEqual({ subject: id, predicate: "id:wechatId", literal: "wxid_xyz" });
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe("derivePlaceTriples", () => {
|
|
120
|
-
it("emits has-name + has-alias (deduped) + located-at + address", () => {
|
|
121
|
-
const id = newId();
|
|
122
|
-
const pl = {
|
|
123
|
-
id,
|
|
124
|
-
type: "place",
|
|
125
|
-
name: "妈妈家",
|
|
126
|
-
aliases: ["妈妈家", "妈家", "home"],
|
|
127
|
-
coordinates: { lat: 24.5, lng: 118.1 },
|
|
128
|
-
address: "厦门思明区",
|
|
129
|
-
category: "home",
|
|
130
|
-
ingestedAt: 1,
|
|
131
|
-
source: sourceOk(),
|
|
132
|
-
};
|
|
133
|
-
const ts = derivePlaceTriples(pl);
|
|
134
|
-
expect(ts).toContainEqual({ subject: id, predicate: "has-name", literal: "妈妈家" });
|
|
135
|
-
// The primary name shouldn't duplicate as alias
|
|
136
|
-
expect(ts.filter((t) => t.predicate === "has-name").length).toBe(1);
|
|
137
|
-
expect(ts.some((t) => t.predicate === "has-alias" && t.literal === "妈家")).toBe(true);
|
|
138
|
-
expect(ts).toContainEqual({ subject: id, predicate: "located-at", literal: "24.5,118.1" });
|
|
139
|
-
expect(ts).toContainEqual({ subject: id, predicate: "address", literal: "厦门思明区" });
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("deriveItemTriples", () => {
|
|
144
|
-
it("emits price + merchant", () => {
|
|
145
|
-
const id = newId();
|
|
146
|
-
const i = {
|
|
147
|
-
id,
|
|
148
|
-
type: "item",
|
|
149
|
-
subtype: "product",
|
|
150
|
-
name: "蛋白粉",
|
|
151
|
-
price: { value: 288, currency: "CNY" },
|
|
152
|
-
merchant: "person-xy-store",
|
|
153
|
-
ingestedAt: 1,
|
|
154
|
-
source: sourceOk(),
|
|
155
|
-
};
|
|
156
|
-
const ts = deriveItemTriples(i);
|
|
157
|
-
expect(ts).toContainEqual({ subject: id, predicate: "priced-at", literal: "288 CNY" });
|
|
158
|
-
expect(ts).toContainEqual({ subject: id, predicate: "sold-by", object: "person-xy-store" });
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("deriveTopicTriples", () => {
|
|
163
|
-
it("emits parent + derived-from for each event", () => {
|
|
164
|
-
const id = newId();
|
|
165
|
-
const e1 = newId();
|
|
166
|
-
const e2 = newId();
|
|
167
|
-
const t = {
|
|
168
|
-
id,
|
|
169
|
-
type: "topic",
|
|
170
|
-
name: "母亲健康",
|
|
171
|
-
parentTopic: "topic-family",
|
|
172
|
-
derivedFromEvents: [e1, e2],
|
|
173
|
-
ingestedAt: 1,
|
|
174
|
-
source: sourceOk(),
|
|
175
|
-
};
|
|
176
|
-
const ts = deriveTopicTriples(t);
|
|
177
|
-
expect(ts).toContainEqual({ subject: id, predicate: "parent", object: "topic-family" });
|
|
178
|
-
expect(ts).toContainEqual({ subject: id, predicate: "derived-from", object: e1 });
|
|
179
|
-
expect(ts).toContainEqual({ subject: id, predicate: "derived-from", object: e2 });
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("deriveBatchTriples + deriveEntityTriples", () => {
|
|
184
|
-
it("walks all 5 entity kinds in a batch", () => {
|
|
185
|
-
const batch = {
|
|
186
|
-
events: [{ id: "e", type: "event", subtype: "message", occurredAt: 1, ingestedAt: 1, content: {}, source: sourceOk() }],
|
|
187
|
-
persons: [{ id: "p", type: "person", subtype: "contact", names: ["x"], ingestedAt: 1, source: sourceOk() }],
|
|
188
|
-
places: [{ id: "pl", type: "place", name: "home", aliases: [], ingestedAt: 1, source: sourceOk() }],
|
|
189
|
-
items: [{ id: "i", type: "item", subtype: "product", name: "thing", ingestedAt: 1, source: sourceOk() }],
|
|
190
|
-
topics: [{ id: "t", type: "topic", name: "x", ingestedAt: 1, source: sourceOk() }],
|
|
191
|
-
};
|
|
192
|
-
const ts = deriveBatchTriples(batch);
|
|
193
|
-
expect(ts.length).toBeGreaterThan(5); // at least the 5 rdf:type rows
|
|
194
|
-
expect(ts.some((x) => x.subject === "e" && x.predicate === "rdf:type")).toBe(true);
|
|
195
|
-
expect(ts.some((x) => x.subject === "p" && x.predicate === "rdf:type")).toBe(true);
|
|
196
|
-
expect(ts.some((x) => x.subject === "pl" && x.predicate === "rdf:type")).toBe(true);
|
|
197
|
-
expect(ts.some((x) => x.subject === "i" && x.predicate === "rdf:type")).toBe(true);
|
|
198
|
-
expect(ts.some((x) => x.subject === "t" && x.predicate === "rdf:type")).toBe(true);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("deriveEntityTriples dispatches by .type and returns [] for unknown", () => {
|
|
202
|
-
expect(deriveEntityTriples(null)).toEqual([]);
|
|
203
|
-
expect(deriveEntityTriples({ type: "frobnicate" })).toEqual([]);
|
|
204
|
-
const ts = deriveEntityTriples({
|
|
205
|
-
id: "p",
|
|
206
|
-
type: "person",
|
|
207
|
-
subtype: "contact",
|
|
208
|
-
names: ["x"],
|
|
209
|
-
ingestedAt: 1,
|
|
210
|
-
source: sourceOk(),
|
|
211
|
-
});
|
|
212
|
-
expect(ts.length).toBeGreaterThan(0);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("returns [] for non-object batch input", () => {
|
|
216
|
-
expect(deriveBatchTriples(null)).toEqual([]);
|
|
217
|
-
expect(deriveBatchTriples("oops")).toEqual([]);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { MockLLMClient, OllamaClient } = require("../lib/llm-client");
|
|
6
|
-
|
|
7
|
-
// ─── MockLLMClient ────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
describe("MockLLMClient", () => {
|
|
10
|
-
it("isLocal is always true", () => {
|
|
11
|
-
expect(new MockLLMClient().isLocal).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("returns the configured static reply", async () => {
|
|
15
|
-
const c = new MockLLMClient({ reply: "hello [evt-1]" });
|
|
16
|
-
const r = await c.chat([{ role: "user", content: "x" }]);
|
|
17
|
-
expect(r.text).toBe("hello [evt-1]");
|
|
18
|
-
expect(r.model).toBe("mock-llm");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("records every call for assertion", async () => {
|
|
22
|
-
const c = new MockLLMClient({ reply: "ok" });
|
|
23
|
-
await c.chat([{ role: "user", content: "a" }]);
|
|
24
|
-
await c.chat([{ role: "user", content: "b" }]);
|
|
25
|
-
expect(c.calls.length).toBe(2);
|
|
26
|
-
expect(c.calls[0].messages[0].content).toBe("a");
|
|
27
|
-
expect(c.calls[1].messages[0].content).toBe("b");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("function reply gets the messages", async () => {
|
|
31
|
-
const c = new MockLLMClient({ reply: (messages) => `you said: ${messages[0].content}` });
|
|
32
|
-
const r = await c.chat([{ role: "user", content: "hi" }]);
|
|
33
|
-
expect(r.text).toBe("you said: hi");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("replies array exhausts and throws after", async () => {
|
|
37
|
-
const c = new MockLLMClient({ replies: ["a", "b"] });
|
|
38
|
-
expect((await c.chat([])).text).toBe("a");
|
|
39
|
-
expect((await c.chat([])).text).toBe("b");
|
|
40
|
-
await expect(c.chat([])).rejects.toThrow(/exhausted/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("returns a usage object even if it's an approximation", async () => {
|
|
44
|
-
const c = new MockLLMClient({ reply: "reply text" });
|
|
45
|
-
const r = await c.chat([{ role: "user", content: "question content" }]);
|
|
46
|
-
expect(r.usage).toBeDefined();
|
|
47
|
-
expect(typeof r.usage.promptTokens).toBe("number");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// ─── OllamaClient ─────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
describe("OllamaClient", () => {
|
|
54
|
-
it("declares isLocal = true unconditionally", () => {
|
|
55
|
-
const c = new OllamaClient({ fetch: async () => ({ ok: true }) });
|
|
56
|
-
expect(c.isLocal).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("uses sensible defaults (baseUrl + model)", () => {
|
|
60
|
-
const c = new OllamaClient({ fetch: async () => ({}) });
|
|
61
|
-
expect(c.baseUrl).toBe("http://localhost:11434");
|
|
62
|
-
expect(c.model).toContain("qwen2.5");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("posts to /api/chat with the configured model + messages", async () => {
|
|
66
|
-
let captured = null;
|
|
67
|
-
const fakeFetch = async (url, init) => {
|
|
68
|
-
captured = { url, body: JSON.parse(init.body) };
|
|
69
|
-
return {
|
|
70
|
-
ok: true,
|
|
71
|
-
status: 200,
|
|
72
|
-
json: async () => ({
|
|
73
|
-
message: { role: "assistant", content: "hi" },
|
|
74
|
-
prompt_eval_count: 12,
|
|
75
|
-
eval_count: 7,
|
|
76
|
-
}),
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
const c = new OllamaClient({ fetch: fakeFetch, model: "llama3:8b" });
|
|
80
|
-
const r = await c.chat([{ role: "user", content: "ping" }]);
|
|
81
|
-
expect(captured.url).toBe("http://localhost:11434/api/chat");
|
|
82
|
-
expect(captured.body.model).toBe("llama3:8b");
|
|
83
|
-
expect(captured.body.stream).toBe(false);
|
|
84
|
-
expect(captured.body.messages[0].content).toBe("ping");
|
|
85
|
-
expect(r.text).toBe("hi");
|
|
86
|
-
expect(r.usage.promptTokens).toBe(12);
|
|
87
|
-
expect(r.usage.completionTokens).toBe(7);
|
|
88
|
-
expect(r.usage.totalTokens).toBe(19);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("wraps fetch errors with cause preserved", async () => {
|
|
92
|
-
const fakeFetch = async () => { throw new Error("ECONNREFUSED"); };
|
|
93
|
-
const c = new OllamaClient({ fetch: fakeFetch });
|
|
94
|
-
await expect(c.chat([{ role: "user", content: "x" }])).rejects.toThrow(/request failed/);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("throws on non-OK status with body excerpt", async () => {
|
|
98
|
-
const fakeFetch = async () => ({
|
|
99
|
-
ok: false,
|
|
100
|
-
status: 500,
|
|
101
|
-
statusText: "Internal Server Error",
|
|
102
|
-
text: async () => "internal model crashed",
|
|
103
|
-
});
|
|
104
|
-
const c = new OllamaClient({ fetch: fakeFetch });
|
|
105
|
-
await expect(c.chat([{ role: "user", content: "x" }])).rejects.toThrow(/500/);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("health() returns ok when /api/tags responds 200", async () => {
|
|
109
|
-
const fakeFetch = async () => ({ ok: true, status: 200 });
|
|
110
|
-
const c = new OllamaClient({ fetch: fakeFetch });
|
|
111
|
-
const h = await c.health();
|
|
112
|
-
expect(h.ok).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("health() returns ok=false on error", async () => {
|
|
116
|
-
const fakeFetch = async () => { throw new Error("down"); };
|
|
117
|
-
const c = new OllamaClient({ fetch: fakeFetch });
|
|
118
|
-
const h = await c.health();
|
|
119
|
-
expect(h.ok).toBe(false);
|
|
120
|
-
expect(h.error).toContain("down");
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const fs = require("node:fs");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const os = require("node:os");
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
DouyinAdapter,
|
|
11
|
-
XiaohongshuAdapter,
|
|
12
|
-
QQAdapter,
|
|
13
|
-
TelegramAdapter,
|
|
14
|
-
} = require("../lib");
|
|
15
|
-
const { assertAdapter } = require("../lib/adapter-spec");
|
|
16
|
-
const { validateBatch } = require("../lib/batch");
|
|
17
|
-
|
|
18
|
-
function makeMockDriver(scriptedRows) {
|
|
19
|
-
return function () {
|
|
20
|
-
return {
|
|
21
|
-
prepare(sql) {
|
|
22
|
-
return {
|
|
23
|
-
all() {
|
|
24
|
-
for (const [matchSubstr, rows] of scriptedRows) {
|
|
25
|
-
if (sql.includes(matchSubstr)) return rows;
|
|
26
|
-
}
|
|
27
|
-
throw new Error("no such table");
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
},
|
|
31
|
-
pragma() {},
|
|
32
|
-
close() {},
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function tmpDb() {
|
|
38
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "longtail-"));
|
|
39
|
-
const dbPath = path.join(dir, "fake.db");
|
|
40
|
-
fs.writeFileSync(dbPath, "fake");
|
|
41
|
-
return { dir, dbPath };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function cleanup(dir) {
|
|
45
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ─── DouyinAdapter ──────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe("DouyinAdapter", () => {
|
|
51
|
-
it("contract conformance", () => {
|
|
52
|
-
const a = new DouyinAdapter({ account: { uid: "u-1" } });
|
|
53
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("snapshot mode constructs without account.uid (stateless)", () => {
|
|
57
|
-
// §A8 v0.2: constructor loosened — snapshot mode pulls account from the
|
|
58
|
-
// snapshot file. Sqlite mode still requires account.uid, checked at sync
|
|
59
|
-
// time not construction (see _syncViaSqlite throw at runtime).
|
|
60
|
-
const a = new DouyinAdapter({});
|
|
61
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
62
|
-
expect(a.account).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("sync yields history + favourite + search", async () => {
|
|
66
|
-
const { dir, dbPath } = tmpDb();
|
|
67
|
-
try {
|
|
68
|
-
const mockDriver = makeMockDriver([
|
|
69
|
-
["FROM video_history", [{ id: 1, aweme_id: "v1", title: "Cat", view_time: 1700000000, author: "@cat", duration: 30 }]],
|
|
70
|
-
["FROM history", []],
|
|
71
|
-
["FROM user_favorite", [{ id: 1, aweme_id: "v2", title: "Saved", create_time: 1700001000 }]],
|
|
72
|
-
["FROM favourite", []],
|
|
73
|
-
["FROM search_history", [{ id: 1, keyword: "music", time: 1700002000 }]],
|
|
74
|
-
]);
|
|
75
|
-
const a = new DouyinAdapter({ account: { uid: "u-1" }, dbPath, dbDriverFactory: () => mockDriver });
|
|
76
|
-
const raws = [];
|
|
77
|
-
for await (const r of a.sync()) raws.push(r);
|
|
78
|
-
expect(raws.length).toBe(3);
|
|
79
|
-
for (const r of raws) {
|
|
80
|
-
expect(validateBatch(a.normalize(r)).valid).toBe(true);
|
|
81
|
-
}
|
|
82
|
-
} finally { cleanup(dir); }
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// ─── XiaohongshuAdapter ─────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
describe("XiaohongshuAdapter", () => {
|
|
89
|
-
it("contract conformance", () => {
|
|
90
|
-
const a = new XiaohongshuAdapter({ account: { uid: "u-1" } });
|
|
91
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
92
|
-
expect(a.capabilities).toContain("sync:snapshot");
|
|
93
|
-
expect(a.capabilities).toContain("sync:sqlite");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("snapshot mode constructs without account.uid (stateless)", () => {
|
|
97
|
-
// §A8 v0.2: constructor loosened — snapshot mode pulls account from the
|
|
98
|
-
// snapshot file. Sqlite mode still requires account.uid, checked at sync
|
|
99
|
-
// time not construction.
|
|
100
|
-
const a = new XiaohongshuAdapter({});
|
|
101
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
102
|
-
expect(a.account).toBeNull();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("sync yields history + likes + favourites", async () => {
|
|
106
|
-
const { dir, dbPath } = tmpDb();
|
|
107
|
-
try {
|
|
108
|
-
const mockDriver = makeMockDriver([
|
|
109
|
-
["FROM browse_history", [{ id: 1, note_id: "n1", title: "Recipe", view_time: 1700000000, author: "chef" }]],
|
|
110
|
-
["FROM note", []],
|
|
111
|
-
["FROM liked_note", [{ id: 1, note_id: "n2", title: "Liked", like_time: 1700001000 }]],
|
|
112
|
-
["FROM favourite", [{ id: 1, note_id: "n3", title: "Saved", save_time: 1700002000 }]],
|
|
113
|
-
]);
|
|
114
|
-
const a = new XiaohongshuAdapter({ account: { uid: "u-1" }, dbPath, dbDriverFactory: () => mockDriver });
|
|
115
|
-
const raws = [];
|
|
116
|
-
for await (const r of a.sync()) raws.push(r);
|
|
117
|
-
expect(raws.length).toBe(3);
|
|
118
|
-
for (const r of raws) {
|
|
119
|
-
const batch = a.normalize(r);
|
|
120
|
-
expect(validateBatch(batch).valid).toBe(true);
|
|
121
|
-
}
|
|
122
|
-
} finally { cleanup(dir); }
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// ─── QQAdapter ──────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
describe("QQAdapter", () => {
|
|
129
|
-
it("contract conformance", () => {
|
|
130
|
-
const a = new QQAdapter({ account: { qq: "12345" } });
|
|
131
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
132
|
-
expect(a.dataDisclosure.legalGate).toBe(true);
|
|
133
|
-
expect(a.capabilities).toContain("sync:snapshot");
|
|
134
|
-
expect(a.capabilities).toContain("sync:sqlite");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("snapshot mode constructs without account.qq (stateless)", () => {
|
|
138
|
-
// §Phase 13.5 v0.2: constructor loosened — snapshot mode pulls account
|
|
139
|
-
// from the snapshot file. Sqlite mode still requires account.qq, checked
|
|
140
|
-
// at sync time not construction. Mirror of weibo / bilibili A8 pattern.
|
|
141
|
-
const a = new QQAdapter({});
|
|
142
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
143
|
-
expect(a.account).toBeNull();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("sqlite mode throws at sync time when account.qq missing", async () => {
|
|
147
|
-
const { dir, dbPath } = tmpDb();
|
|
148
|
-
try {
|
|
149
|
-
const a = new QQAdapter({ dbPath, keyProvider: { getKey: async () => "k" } });
|
|
150
|
-
let threw = null;
|
|
151
|
-
try {
|
|
152
|
-
for await (const _r of a.sync()) { /* drain */ }
|
|
153
|
-
} catch (err) {
|
|
154
|
-
threw = err;
|
|
155
|
-
}
|
|
156
|
-
expect(threw).toBeTruthy();
|
|
157
|
-
expect(String(threw.message)).toMatch(/account\.qq/);
|
|
158
|
-
} finally { cleanup(dir); }
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("authenticate({}) without inputPath nor dbPath returns NO_INPUT", async () => {
|
|
162
|
-
const a = new QQAdapter({ account: { qq: "12345" }, keyProvider: { getKey: async () => "k" } });
|
|
163
|
-
const r = await a.authenticate();
|
|
164
|
-
expect(r.ok).toBe(false);
|
|
165
|
-
expect(r.reason).toBe("NO_INPUT");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("authenticate fails without keyProvider", async () => {
|
|
169
|
-
const { dir, dbPath } = tmpDb();
|
|
170
|
-
try {
|
|
171
|
-
const a = new QQAdapter({ account: { qq: "12345" }, dbPath });
|
|
172
|
-
const r = await a.authenticate();
|
|
173
|
-
expect(r.reason).toBe("NO_KEY_PROVIDER");
|
|
174
|
-
} finally { cleanup(dir); }
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("sync yields contact + group + message types", async () => {
|
|
178
|
-
const { dir, dbPath } = tmpDb();
|
|
179
|
-
try {
|
|
180
|
-
// §Phase 13.5 v0.2 mock — new SQL targets:
|
|
181
|
-
// - Friends / friends / tb_recent_contact (probe order)
|
|
182
|
-
// - TroopInfoV2
|
|
183
|
-
// - sqlite_master LIKE 'mr_friend_%_New'
|
|
184
|
-
// - mr_friend_<MD5(peer).upper()>_New
|
|
185
|
-
const mockDriver = makeMockDriver([
|
|
186
|
-
["FROM Friends", [{ uin: "999", nickname: "好友A", remark: "" }]],
|
|
187
|
-
["FROM TroopInfoV2", [{ troop_uin: "888", troop_name: "测试群", member_count: 5, owner_uin: "777" }]],
|
|
188
|
-
["FROM sqlite_master", [{ name: "mr_friend_ABCDEF1234_New" }]],
|
|
189
|
-
[
|
|
190
|
-
"FROM mr_friend_ABCDEF1234_New",
|
|
191
|
-
[{
|
|
192
|
-
msgId: "m1", msgtype: -1000, senderuin: "999", time: 1700000000,
|
|
193
|
-
// msgData is XOR-encrypted bytes; with imei="123456789012345" key,
|
|
194
|
-
// "hi" encrypts to bytes [0x51, 0x5d] (0x68^0x31=0x59 — actually
|
|
195
|
-
// depends on imei[0] = '1' = 0x31). Use Buffer for cross-platform.
|
|
196
|
-
msgData: Buffer.from([0x68 ^ 0x31, 0x69 ^ 0x32]),
|
|
197
|
-
issend: 0, frienduin: "999", troopuin: null,
|
|
198
|
-
}],
|
|
199
|
-
],
|
|
200
|
-
]);
|
|
201
|
-
const a = new QQAdapter({
|
|
202
|
-
account: { qq: "12345" },
|
|
203
|
-
dbPath,
|
|
204
|
-
keyProvider: { getKey: async () => "123456789012345" },
|
|
205
|
-
dbDriverFactory: () => mockDriver,
|
|
206
|
-
});
|
|
207
|
-
const raws = [];
|
|
208
|
-
for await (const r of a.sync()) raws.push(r);
|
|
209
|
-
expect(raws.length).toBe(3); // contact + group + message
|
|
210
|
-
const contact = raws.find((r) => r.kind === "contact");
|
|
211
|
-
const group = raws.find((r) => r.kind === "group");
|
|
212
|
-
const message = raws.find((r) => r.kind === "message");
|
|
213
|
-
expect(contact).toBeDefined();
|
|
214
|
-
expect(group).toBeDefined();
|
|
215
|
-
expect(message).toBeDefined();
|
|
216
|
-
// XOR-decrypt round-trip: bytes(0x68^0x31, 0x69^0x32) XOR "12" = "hi"
|
|
217
|
-
expect(message.payload.text).toBe("hi");
|
|
218
|
-
for (const r of raws) {
|
|
219
|
-
expect(validateBatch(a.normalize(r)).valid).toBe(true);
|
|
220
|
-
}
|
|
221
|
-
} finally { cleanup(dir); }
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// ─── TelegramAdapter ────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
describe("TelegramAdapter", () => {
|
|
228
|
-
it("contract conformance", () => {
|
|
229
|
-
const a = new TelegramAdapter({ account: { userId: "u-1" } });
|
|
230
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
231
|
-
expect(a.dataDisclosure.legalGate).toBe(true);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("no-arg ctor passes contract (2026-05-25 v0.6 — account.userId OPTIONAL for snapshot mode)", () => {
|
|
235
|
-
const a = new TelegramAdapter();
|
|
236
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
237
|
-
expect(a.capabilities).toContain("sync:snapshot");
|
|
238
|
-
expect(a.capabilities).toContain("sync:sqlite");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("authenticate(ctx.inputPath) returns ok when file readable (snapshot mode)", async () => {
|
|
242
|
-
const { dir, dbPath } = tmpDb();
|
|
243
|
-
try {
|
|
244
|
-
const a = new TelegramAdapter();
|
|
245
|
-
const auth = await a.authenticate({ inputPath: dbPath });
|
|
246
|
-
expect(auth.ok).toBe(true);
|
|
247
|
-
expect(auth.mode).toBe("snapshot-file");
|
|
248
|
-
} finally { cleanup(dir); }
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("sync yields user + chat + messages (no key needed)", async () => {
|
|
252
|
-
const { dir, dbPath } = tmpDb();
|
|
253
|
-
try {
|
|
254
|
-
const mockDriver = makeMockDriver([
|
|
255
|
-
["FROM users", [{ uid: "111", name: "Alice", username: "alice", phone: "13800001111" }]],
|
|
256
|
-
["FROM chats", [{ uid: "222", name: "Group A" }]],
|
|
257
|
-
["FROM messages_v2", [{ mid: "m1", uid: "111", message: "Hi", date: 1700000000, out: 0 }]],
|
|
258
|
-
["FROM messages", []],
|
|
259
|
-
]);
|
|
260
|
-
const a = new TelegramAdapter({ account: { userId: "u-1" }, dbPath, dbDriverFactory: () => mockDriver });
|
|
261
|
-
const raws = [];
|
|
262
|
-
for await (const r of a.sync()) raws.push(r);
|
|
263
|
-
expect(raws.length).toBe(3);
|
|
264
|
-
for (const r of raws) {
|
|
265
|
-
expect(validateBatch(a.normalize(r)).valid).toBe(true);
|
|
266
|
-
}
|
|
267
|
-
} finally { cleanup(dir); }
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("normalize contact includes phone identifier", async () => {
|
|
271
|
-
const a = new TelegramAdapter({ account: { userId: "u-1" } });
|
|
272
|
-
const batch = a.normalize({
|
|
273
|
-
adapter: "messaging-telegram",
|
|
274
|
-
originalId: "user-111",
|
|
275
|
-
capturedAt: Date.now(),
|
|
276
|
-
payload: { kind: "contact", row: { uid: "111", name: "Bob", phone: "13800001111" } },
|
|
277
|
-
});
|
|
278
|
-
expect(batch.persons[0].identifiers.telegramId).toBe("111");
|
|
279
|
-
expect(batch.persons[0].identifiers.phone).toEqual(["13800001111"]);
|
|
280
|
-
});
|
|
281
|
-
});
|