@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,396 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
AIChatHistoryAdapter,
|
|
7
|
-
CookieAuthSession,
|
|
8
|
-
NotImplementedYetError,
|
|
9
|
-
assertVendorSpec,
|
|
10
|
-
SUPPORTED_VENDORS,
|
|
11
|
-
DEFAULT_VENDOR_SPECS,
|
|
12
|
-
schemaMap,
|
|
13
|
-
} = require("../../lib/adapters/ai-chat-history");
|
|
14
|
-
const { assertAdapter } = require("../../lib/adapter-spec");
|
|
15
|
-
const {
|
|
16
|
-
EVENT_SUBTYPES,
|
|
17
|
-
PERSON_SUBTYPES,
|
|
18
|
-
ITEM_SUBTYPES,
|
|
19
|
-
} = require("../../lib/constants");
|
|
20
|
-
|
|
21
|
-
// ─── vendor-spec assertion ──────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
describe("assertVendorSpec — SUPPORTED_VENDORS", () => {
|
|
24
|
-
it("9 vendors are declared (Phase 10.2 +doubao scaffold)", () => {
|
|
25
|
-
expect(SUPPORTED_VENDORS).toEqual([
|
|
26
|
-
"deepseek",
|
|
27
|
-
"kimi",
|
|
28
|
-
"tongyi",
|
|
29
|
-
"zhipu",
|
|
30
|
-
"hunyuan",
|
|
31
|
-
"qianfan",
|
|
32
|
-
"coze",
|
|
33
|
-
"dreamina",
|
|
34
|
-
"doubao",
|
|
35
|
-
]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("DEFAULT_VENDOR_SPECS includes one entry per supported vendor", () => {
|
|
39
|
-
for (const v of SUPPORTED_VENDORS) {
|
|
40
|
-
expect(DEFAULT_VENDOR_SPECS[v]).toBeDefined();
|
|
41
|
-
expect(DEFAULT_VENDOR_SPECS[v].name).toBe(v);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("each shipped vendor spec passes assertVendorSpec", () => {
|
|
46
|
-
for (const v of SUPPORTED_VENDORS) {
|
|
47
|
-
const check = assertVendorSpec(DEFAULT_VENDOR_SPECS[v]);
|
|
48
|
-
if (!check.ok) {
|
|
49
|
-
throw new Error(`vendor ${v} failed: ${check.errors.join("; ")}`);
|
|
50
|
-
}
|
|
51
|
-
expect(check.ok).toBe(true);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("rejects spec missing rateLimits.perMinute", () => {
|
|
56
|
-
const bad = { ...DEFAULT_VENDOR_SPECS.deepseek, rateLimits: {} };
|
|
57
|
-
const check = assertVendorSpec(bad);
|
|
58
|
-
expect(check.ok).toBe(false);
|
|
59
|
-
expect(check.errors.some((e) => e.includes("rateLimits"))).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("rejects spec with non-https loginUrl", () => {
|
|
63
|
-
const bad = { ...DEFAULT_VENDOR_SPECS.deepseek, loginUrl: "http://chat.deepseek.com/" };
|
|
64
|
-
const check = assertVendorSpec(bad);
|
|
65
|
-
expect(check.ok).toBe(false);
|
|
66
|
-
expect(check.errors.some((e) => e.includes("loginUrl"))).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("rejects unsupported vendor name", () => {
|
|
70
|
-
const bad = { ...DEFAULT_VENDOR_SPECS.deepseek, name: "claude" };
|
|
71
|
-
const check = assertVendorSpec(bad);
|
|
72
|
-
expect(check.ok).toBe(false);
|
|
73
|
-
expect(check.errors.some((e) => e.includes("name"))).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// ─── PersonalDataAdapter contract ────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
describe("AIChatHistoryAdapter contract", () => {
|
|
80
|
-
it("freshly constructed adapter passes assertAdapter", () => {
|
|
81
|
-
const a = new AIChatHistoryAdapter();
|
|
82
|
-
const r = assertAdapter(a);
|
|
83
|
-
if (!r.ok) throw new Error(r.errors.join("; "));
|
|
84
|
-
expect(r.ok).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("declares name + version + capabilities + extractMode", () => {
|
|
88
|
-
const a = new AIChatHistoryAdapter();
|
|
89
|
-
expect(a.name).toBe("ai-chat-history");
|
|
90
|
-
expect(a.version).toMatch(/^0\.1\.\d+$/);
|
|
91
|
-
expect(a.capabilities).toContain("sync:cookie-multi-vendor");
|
|
92
|
-
expect(a.extractMode).toBe("web-api");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("dataDisclosure is high-sensitivity without legalGate (cookies, not third-party content)", () => {
|
|
96
|
-
const a = new AIChatHistoryAdapter();
|
|
97
|
-
expect(a.dataDisclosure.sensitivity).toBe("high");
|
|
98
|
-
expect(a.dataDisclosure.legalGate).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("constructor rejects invalid vendor spec override", () => {
|
|
102
|
-
expect(
|
|
103
|
-
() =>
|
|
104
|
-
new AIChatHistoryAdapter({
|
|
105
|
-
vendorSpecs: { deepseek: { ...DEFAULT_VENDOR_SPECS.deepseek, rateLimits: {} } },
|
|
106
|
-
}),
|
|
107
|
-
).toThrow(/rateLimits/);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ─── authenticate / healthCheck ──────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
describe("AIChatHistoryAdapter.authenticate", () => {
|
|
114
|
-
it("returns vendorsReady=[] when no sessions configured", async () => {
|
|
115
|
-
const a = new AIChatHistoryAdapter();
|
|
116
|
-
const r = await a.authenticate();
|
|
117
|
-
expect(r.ok).toBe(true);
|
|
118
|
-
expect(r.vendorsReady).toEqual([]);
|
|
119
|
-
expect(r.vendorsNeedingLogin.sort()).toEqual([...SUPPORTED_VENDORS].sort());
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("reflects configured sessions", async () => {
|
|
123
|
-
const a = new AIChatHistoryAdapter();
|
|
124
|
-
a.setSession("deepseek", new CookieAuthSession({ vendor: "deepseek", cookies: [{ name: "userToken", value: "x" }] }));
|
|
125
|
-
a.setSession("kimi", new CookieAuthSession({ vendor: "kimi", cookies: [{ name: "sess", value: "y" }] }));
|
|
126
|
-
const r = await a.authenticate();
|
|
127
|
-
expect(r.vendorsReady.sort()).toEqual(["deepseek", "kimi"]);
|
|
128
|
-
expect(r.vendorsNeedingLogin).not.toContain("deepseek");
|
|
129
|
-
expect(r.vendorsNeedingLogin).not.toContain("kimi");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("AIChatHistoryAdapter.healthCheck", () => {
|
|
134
|
-
it("reports per-vendor no-session when fresh", async () => {
|
|
135
|
-
const a = new AIChatHistoryAdapter();
|
|
136
|
-
const h = await a.healthCheck();
|
|
137
|
-
expect(h.ok).toBe(true);
|
|
138
|
-
for (const v of SUPPORTED_VENDORS) {
|
|
139
|
-
expect(h.perVendor[v]).toEqual({ ok: false, reason: "no-session" });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("calls vendor.validateCookie when session present", async () => {
|
|
144
|
-
const a = new AIChatHistoryAdapter({
|
|
145
|
-
vendorSpecs: {
|
|
146
|
-
deepseek: {
|
|
147
|
-
...DEFAULT_VENDOR_SPECS.deepseek,
|
|
148
|
-
validateCookie: async () => ({ ok: true, expiresAt: 999 }),
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
a.setSession("deepseek", new CookieAuthSession({ vendor: "deepseek", cookies: [{ name: "x", value: "1" }] }));
|
|
153
|
-
const h = await a.healthCheck();
|
|
154
|
-
expect(h.perVendor.deepseek).toEqual({ ok: true, expiresAt: 999 });
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// ─── sync — Phase 10.1 stub path ─────────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
describe("AIChatHistoryAdapter.sync — skeleton path", () => {
|
|
161
|
-
it("yields nothing when no sessions configured", async () => {
|
|
162
|
-
const a = new AIChatHistoryAdapter();
|
|
163
|
-
const out = [];
|
|
164
|
-
for await (const ev of a.sync()) out.push(ev);
|
|
165
|
-
expect(out).toEqual([]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("vendor-not-wired sentinel path: surfaced via NotImplementedYetError direct throw", async () => {
|
|
169
|
-
// Phase 10.2 complete — all 8 vendors are wired with real h5 API.
|
|
170
|
-
// The sentinel kind:"vendor-not-wired" path remains in the sync()
|
|
171
|
-
// catch-block for forward-compat (e.g. when adding new vendors in
|
|
172
|
-
// Phase 10.3+ before their wiring lands). Verify the dispatch by
|
|
173
|
-
// injecting a synthetic always-throwing vendor spec.
|
|
174
|
-
const { NotImplementedYetError } = require("../../lib/adapters/ai-chat-history/vendor-spec");
|
|
175
|
-
const fakeSpec = {
|
|
176
|
-
...require("../../lib/adapters/ai-chat-history").DEFAULT_VENDOR_SPECS.deepseek,
|
|
177
|
-
// eslint-disable-next-line require-yield
|
|
178
|
-
async *listConversations() { throw new NotImplementedYetError("deepseek", "listConversations"); },
|
|
179
|
-
};
|
|
180
|
-
const a = new AIChatHistoryAdapter({ vendorSpecs: { deepseek: fakeSpec } });
|
|
181
|
-
a.setSession(
|
|
182
|
-
"deepseek",
|
|
183
|
-
new CookieAuthSession({ vendor: "deepseek", cookies: [{ name: "userToken", value: "x" }] }),
|
|
184
|
-
);
|
|
185
|
-
const out = [];
|
|
186
|
-
for await (const ev of a.sync({ vendors: ["deepseek"] })) out.push(ev);
|
|
187
|
-
expect(out.length).toBe(1);
|
|
188
|
-
expect(out[0].payload.kind).toBe("vendor-not-wired");
|
|
189
|
-
expect(out[0].payload.vendor).toBe("deepseek");
|
|
190
|
-
expect(out[0].payload.error).toBe("VENDOR_NOT_WIRED");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("can be driven end-to-end with a mock vendor spec", async () => {
|
|
194
|
-
// Inject a faux vendor that yields one conversation + two messages.
|
|
195
|
-
const FAKE_CONV = {
|
|
196
|
-
vendor: "deepseek",
|
|
197
|
-
originalId: "conv-1",
|
|
198
|
-
title: "test title",
|
|
199
|
-
modelName: "deepseek-r1",
|
|
200
|
-
createdAt: Date.parse("2026-05-19T00:00:00Z"),
|
|
201
|
-
updatedAt: Date.parse("2026-05-19T01:00:00Z"),
|
|
202
|
-
messageCount: 2,
|
|
203
|
-
};
|
|
204
|
-
const FAKE_MSGS = [
|
|
205
|
-
{
|
|
206
|
-
vendor: "deepseek",
|
|
207
|
-
originalId: "m1",
|
|
208
|
-
conversationId: "conv-1",
|
|
209
|
-
role: "user",
|
|
210
|
-
content: { text: "hello" },
|
|
211
|
-
createdAt: Date.parse("2026-05-19T00:00:00Z"),
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
vendor: "deepseek",
|
|
215
|
-
originalId: "m2",
|
|
216
|
-
conversationId: "conv-1",
|
|
217
|
-
role: "assistant",
|
|
218
|
-
content: { text: "hi there" },
|
|
219
|
-
createdAt: Date.parse("2026-05-19T00:00:05Z"),
|
|
220
|
-
modelName: "deepseek-r1",
|
|
221
|
-
},
|
|
222
|
-
];
|
|
223
|
-
const a = new AIChatHistoryAdapter({
|
|
224
|
-
vendorSpecs: {
|
|
225
|
-
deepseek: {
|
|
226
|
-
...DEFAULT_VENDOR_SPECS.deepseek,
|
|
227
|
-
async validateCookie() { return { ok: true }; },
|
|
228
|
-
async *listConversations() { yield FAKE_CONV; },
|
|
229
|
-
async *listMessages() { for (const m of FAKE_MSGS) yield m; },
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
a.setSession(
|
|
234
|
-
"deepseek",
|
|
235
|
-
new CookieAuthSession({ vendor: "deepseek", cookies: [{ name: "userToken", value: "x" }] }),
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
const out = [];
|
|
239
|
-
for await (const ev of a.sync({ vendors: ["deepseek"] })) out.push(ev);
|
|
240
|
-
expect(out.length).toBe(3); // 1 conv + 2 msgs
|
|
241
|
-
expect(out[0].payload.kind).toBe("conversation");
|
|
242
|
-
expect(out[1].payload.kind).toBe("message");
|
|
243
|
-
expect(out[2].payload.kind).toBe("message");
|
|
244
|
-
|
|
245
|
-
// Drive normalize() over each
|
|
246
|
-
const batches = out.map((r) => a.normalize(r));
|
|
247
|
-
// First batch: conv → topic + vendor person
|
|
248
|
-
expect(batches[0].topics.length).toBe(1);
|
|
249
|
-
expect(batches[0].persons.length).toBe(1);
|
|
250
|
-
expect(batches[0].persons[0].subtype).toBe(PERSON_SUBTYPES.AI_AGENT);
|
|
251
|
-
expect(batches[0].events.length).toBe(0);
|
|
252
|
-
// Message batches: 1 event each
|
|
253
|
-
expect(batches[1].events.length).toBe(1);
|
|
254
|
-
expect(batches[1].events[0].subtype).toBe(EVENT_SUBTYPES.AI_MESSAGE);
|
|
255
|
-
expect(batches[1].events[0].actor).toBe("person-self");
|
|
256
|
-
expect(batches[2].events[0].actor).toBe("person-ai-deepseek");
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ─── schema-map — deterministic transforms ───────────────────────────────
|
|
261
|
-
|
|
262
|
-
describe("schema-map.conversationToBatch", () => {
|
|
263
|
-
it("produces vendor Person + Topic + Event per message", () => {
|
|
264
|
-
const conv = {
|
|
265
|
-
vendor: "kimi",
|
|
266
|
-
originalId: "abc",
|
|
267
|
-
title: "research notes",
|
|
268
|
-
createdAt: 1700000000000,
|
|
269
|
-
updatedAt: 1700001000000,
|
|
270
|
-
};
|
|
271
|
-
const msgs = [
|
|
272
|
-
{ vendor: "kimi", originalId: "m1", conversationId: "abc", role: "user",
|
|
273
|
-
content: { text: "summarize this paper" }, createdAt: 1700000000000 },
|
|
274
|
-
{ vendor: "kimi", originalId: "m2", conversationId: "abc", role: "assistant",
|
|
275
|
-
content: { text: "ok here is the summary..." }, createdAt: 1700000010000 },
|
|
276
|
-
];
|
|
277
|
-
const batch = schemaMap.conversationToBatch(conv, msgs, { displayName: "Kimi" });
|
|
278
|
-
expect(batch.events.length).toBe(2);
|
|
279
|
-
expect(batch.events[0].subtype).toBe(EVENT_SUBTYPES.AI_MESSAGE);
|
|
280
|
-
expect(batch.events[0].topics).toEqual(["topic-aiconv-kimi-abc"]);
|
|
281
|
-
expect(batch.events[1].actor).toBe("person-ai-kimi");
|
|
282
|
-
expect(batch.persons.length).toBe(1);
|
|
283
|
-
expect(batch.persons[0].id).toBe("person-ai-kimi");
|
|
284
|
-
expect(batch.persons[0].names).toEqual(["Kimi"]);
|
|
285
|
-
expect(batch.topics.length).toBe(1);
|
|
286
|
-
expect(batch.topics[0].id).toBe("topic-aiconv-kimi-abc");
|
|
287
|
-
expect(batch.topics[0].extra.modelName).toBeUndefined(); // not in this conv
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("upgrades to ai-image-generation subtype when generatedImages present", () => {
|
|
291
|
-
const conv = { vendor: "dreamina", originalId: "img-1", createdAt: 1, updatedAt: 1 };
|
|
292
|
-
const msgs = [
|
|
293
|
-
{
|
|
294
|
-
vendor: "dreamina",
|
|
295
|
-
originalId: "m1",
|
|
296
|
-
conversationId: "img-1",
|
|
297
|
-
role: "assistant",
|
|
298
|
-
content: {
|
|
299
|
-
generatedImages: [{ url: "https://cdn/x.png", prompt: "a cat" }],
|
|
300
|
-
},
|
|
301
|
-
createdAt: 1700000000000,
|
|
302
|
-
},
|
|
303
|
-
];
|
|
304
|
-
const batch = schemaMap.conversationToBatch(conv, msgs, { displayName: "Dreamina" });
|
|
305
|
-
expect(batch.events[0].subtype).toBe(EVENT_SUBTYPES.AI_IMAGE_GENERATION);
|
|
306
|
-
expect(batch.items.length).toBe(1);
|
|
307
|
-
expect(batch.items[0].subtype).toBe(ITEM_SUBTYPES.MEDIA);
|
|
308
|
-
expect(batch.items[0].extra.prompt).toBe("a cat");
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("throws when message vendor mismatches conversation vendor", () => {
|
|
312
|
-
const conv = { vendor: "kimi", originalId: "a", createdAt: 1, updatedAt: 1 };
|
|
313
|
-
const msgs = [{ vendor: "deepseek", originalId: "m", conversationId: "a", role: "user", content: {}, createdAt: 1 }];
|
|
314
|
-
expect(() => schemaMap.conversationToBatch(conv, msgs)).toThrow(/vendor/);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe("schema-map.mergeBatches", () => {
|
|
319
|
-
it("dedupes vendor Person by id", () => {
|
|
320
|
-
const conv1 = { vendor: "kimi", originalId: "a", createdAt: 1, updatedAt: 1 };
|
|
321
|
-
const conv2 = { vendor: "kimi", originalId: "b", createdAt: 1, updatedAt: 1 };
|
|
322
|
-
const b1 = schemaMap.conversationToBatch(conv1, [], { displayName: "Kimi" });
|
|
323
|
-
const b2 = schemaMap.conversationToBatch(conv2, [], { displayName: "Kimi" });
|
|
324
|
-
const merged = schemaMap.mergeBatches([b1, b2]);
|
|
325
|
-
expect(merged.persons.length).toBe(1); // dedup
|
|
326
|
-
expect(merged.topics.length).toBe(2); // distinct conversations
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// ─── CookieAuthSession ───────────────────────────────────────────────────
|
|
331
|
-
|
|
332
|
-
describe("CookieAuthSession", () => {
|
|
333
|
-
it("flattens cookies into a Cookie header", () => {
|
|
334
|
-
const s = new CookieAuthSession({
|
|
335
|
-
vendor: "deepseek",
|
|
336
|
-
cookies: [
|
|
337
|
-
{ name: "userToken", value: "abc" },
|
|
338
|
-
{ name: "sessId", value: "xyz" },
|
|
339
|
-
],
|
|
340
|
-
});
|
|
341
|
-
expect(s.toHeaderValue()).toBe("userToken=abc; sessId=xyz");
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it("filters by domain suffix when matchDomain provided", () => {
|
|
345
|
-
const s = new CookieAuthSession({
|
|
346
|
-
vendor: "coze",
|
|
347
|
-
cookies: [
|
|
348
|
-
{ name: "wwwTok", value: "1", domain: "www.coze.cn" },
|
|
349
|
-
{ name: "cdnTok", value: "2", domain: ".sf-cdn.com" },
|
|
350
|
-
],
|
|
351
|
-
});
|
|
352
|
-
expect(s.toHeaderValue("www.coze.cn")).toBe("wwwTok=1");
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("get(name) returns the matching cookie value or undefined", () => {
|
|
356
|
-
const s = new CookieAuthSession({
|
|
357
|
-
vendor: "deepseek",
|
|
358
|
-
cookies: [{ name: "userToken", value: "x" }],
|
|
359
|
-
});
|
|
360
|
-
expect(s.get("userToken")).toBe("x");
|
|
361
|
-
expect(s.get("nope")).toBeUndefined();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("serializes and round-trips via toJSON / fromJSON", () => {
|
|
365
|
-
const s = new CookieAuthSession({
|
|
366
|
-
vendor: "deepseek",
|
|
367
|
-
cookies: [{ name: "userToken", value: "x" }],
|
|
368
|
-
capturedAt: 12345,
|
|
369
|
-
});
|
|
370
|
-
const json = s.toJSON();
|
|
371
|
-
const restored = CookieAuthSession.fromJSON(json);
|
|
372
|
-
expect(restored.vendor).toBe("deepseek");
|
|
373
|
-
expect(restored.capturedAt).toBe(12345);
|
|
374
|
-
expect(restored.get("userToken")).toBe("x");
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("isExpired returns true when explicit expirationDate has passed", () => {
|
|
378
|
-
const past = Math.floor(Date.now() / 1000) - 3600;
|
|
379
|
-
const s = new CookieAuthSession({
|
|
380
|
-
vendor: "deepseek",
|
|
381
|
-
cookies: [{ name: "x", value: "y", expirationDate: past }],
|
|
382
|
-
});
|
|
383
|
-
expect(s.isExpired()).toBe(true);
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// ─── NotImplementedYetError ──────────────────────────────────────────────
|
|
388
|
-
|
|
389
|
-
describe("NotImplementedYetError", () => {
|
|
390
|
-
it("is throwable with code VENDOR_NOT_WIRED", () => {
|
|
391
|
-
const err = new NotImplementedYetError("deepseek", "listConversations");
|
|
392
|
-
expect(err.code).toBe("VENDOR_NOT_WIRED");
|
|
393
|
-
expect(err.vendor).toBe("deepseek");
|
|
394
|
-
expect(err.capability).toBe("listConversations");
|
|
395
|
-
});
|
|
396
|
-
});
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
HttpClient,
|
|
7
|
-
RateLimitedError,
|
|
8
|
-
CookieExpiredError,
|
|
9
|
-
} = require("../../lib/adapters/ai-chat-history/http-client");
|
|
10
|
-
const {
|
|
11
|
-
CookieAuthSession,
|
|
12
|
-
} = require("../../lib/adapters/ai-chat-history/cookie-auth");
|
|
13
|
-
|
|
14
|
-
// ─── helpers ─────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
function makeResponse({ status = 200, body = {}, headers = {} } = {}) {
|
|
17
|
-
return {
|
|
18
|
-
status,
|
|
19
|
-
ok: status >= 200 && status < 300,
|
|
20
|
-
headers: { get: (k) => headers[k.toLowerCase()] || null },
|
|
21
|
-
async json() { return body; },
|
|
22
|
-
async text() { return JSON.stringify(body); },
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function makeFetchStub(responses) {
|
|
27
|
-
const calls = [];
|
|
28
|
-
const queue = Array.isArray(responses) ? responses.slice() : [responses];
|
|
29
|
-
const stub = async (url, init) => {
|
|
30
|
-
calls.push({ url, init });
|
|
31
|
-
const next = queue.length > 1 ? queue.shift() : queue[0];
|
|
32
|
-
if (typeof next === "function") return next(url, init);
|
|
33
|
-
if (next instanceof Error) throw next;
|
|
34
|
-
return next;
|
|
35
|
-
};
|
|
36
|
-
return { stub, calls };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makeClock(start = 1_000_000) {
|
|
40
|
-
let t = start;
|
|
41
|
-
return {
|
|
42
|
-
now: () => t,
|
|
43
|
-
advance: (ms) => { t += ms; },
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function makeSleep(clock) {
|
|
48
|
-
return async (ms) => clock.advance(ms);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── basic GET / response handling ────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
describe("HttpClient.request", () => {
|
|
54
|
-
it("rejects construction without vendor", () => {
|
|
55
|
-
expect(() => new HttpClient({})).toThrow(/vendor/);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("injects cookies from a vendor-matched session", async () => {
|
|
59
|
-
const clk = makeClock();
|
|
60
|
-
const { stub, calls } = makeFetchStub(makeResponse({ body: { ok: 1 } }));
|
|
61
|
-
const c = new HttpClient({
|
|
62
|
-
vendor: "deepseek",
|
|
63
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
64
|
-
fetch: stub,
|
|
65
|
-
sleep: makeSleep(clk),
|
|
66
|
-
now: clk.now,
|
|
67
|
-
});
|
|
68
|
-
const session = new CookieAuthSession({
|
|
69
|
-
vendor: "deepseek",
|
|
70
|
-
cookies: [{ name: "userToken", value: "abc" }, { name: "session", value: "xyz" }],
|
|
71
|
-
});
|
|
72
|
-
await c.request("https://chat.deepseek.com/api/v0/user/get_user_info", { session });
|
|
73
|
-
expect(calls.length).toBe(1);
|
|
74
|
-
expect(calls[0].init.headers.Cookie).toBe("userToken=abc; session=xyz");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("rejects session whose vendor does not match", async () => {
|
|
78
|
-
const clk = makeClock();
|
|
79
|
-
const c = new HttpClient({
|
|
80
|
-
vendor: "deepseek",
|
|
81
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
82
|
-
fetch: () => makeResponse(),
|
|
83
|
-
sleep: makeSleep(clk),
|
|
84
|
-
now: clk.now,
|
|
85
|
-
});
|
|
86
|
-
const session = new CookieAuthSession({ vendor: "kimi", cookies: [] });
|
|
87
|
-
await expect(c.request("https://x", { session })).rejects.toThrow(/vendor.*mismatch/);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("getJson returns parsed JSON body", async () => {
|
|
91
|
-
const clk = makeClock();
|
|
92
|
-
const { stub } = makeFetchStub(makeResponse({ body: { hello: "world" } }));
|
|
93
|
-
const c = new HttpClient({
|
|
94
|
-
vendor: "deepseek",
|
|
95
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
96
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
97
|
-
});
|
|
98
|
-
expect(await c.getJson("https://x")).toEqual({ hello: "world" });
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("postJson sends body + content-type header", async () => {
|
|
102
|
-
const clk = makeClock();
|
|
103
|
-
const { stub, calls } = makeFetchStub(makeResponse({ body: { ok: 1 } }));
|
|
104
|
-
const c = new HttpClient({
|
|
105
|
-
vendor: "kimi",
|
|
106
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
107
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
108
|
-
});
|
|
109
|
-
await c.postJson("https://kimi.moonshot.cn/api/x", { last: "0", limit: 30 });
|
|
110
|
-
expect(calls[0].init.method).toBe("POST");
|
|
111
|
-
expect(calls[0].init.headers["content-type"]).toBe("application/json");
|
|
112
|
-
expect(JSON.parse(calls[0].init.body)).toEqual({ last: "0", limit: 30 });
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ─── error classification ────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
describe("HttpClient — error mapping", () => {
|
|
119
|
-
it("401 throws CookieExpiredError", async () => {
|
|
120
|
-
const clk = makeClock();
|
|
121
|
-
const { stub } = makeFetchStub(makeResponse({ status: 401 }));
|
|
122
|
-
const c = new HttpClient({
|
|
123
|
-
vendor: "deepseek",
|
|
124
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
125
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
126
|
-
});
|
|
127
|
-
await expect(c.request("https://x")).rejects.toBeInstanceOf(CookieExpiredError);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("403 throws CookieExpiredError", async () => {
|
|
131
|
-
const clk = makeClock();
|
|
132
|
-
const { stub } = makeFetchStub(makeResponse({ status: 403 }));
|
|
133
|
-
const c = new HttpClient({
|
|
134
|
-
vendor: "deepseek",
|
|
135
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
136
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
137
|
-
});
|
|
138
|
-
await expect(c.request("https://x")).rejects.toBeInstanceOf(CookieExpiredError);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("retries 5xx then throws after maxRetries", async () => {
|
|
142
|
-
const clk = makeClock();
|
|
143
|
-
const { stub, calls } = makeFetchStub(makeResponse({ status: 502 }));
|
|
144
|
-
const c = new HttpClient({
|
|
145
|
-
vendor: "deepseek",
|
|
146
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
147
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
148
|
-
maxRetries: 2, baseBackoffMs: 1,
|
|
149
|
-
});
|
|
150
|
-
await expect(c.request("https://x")).rejects.toThrow(/HTTP 502/);
|
|
151
|
-
expect(calls.length).toBe(3); // initial + 2 retries
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("recovers after 5xx if a later attempt succeeds", async () => {
|
|
155
|
-
const clk = makeClock();
|
|
156
|
-
const { stub, calls } = makeFetchStub([
|
|
157
|
-
makeResponse({ status: 503 }),
|
|
158
|
-
makeResponse({ status: 503 }),
|
|
159
|
-
makeResponse({ body: { ok: 1 } }),
|
|
160
|
-
]);
|
|
161
|
-
const c = new HttpClient({
|
|
162
|
-
vendor: "deepseek",
|
|
163
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
164
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
165
|
-
maxRetries: 3, baseBackoffMs: 1,
|
|
166
|
-
});
|
|
167
|
-
const resp = await c.request("https://x");
|
|
168
|
-
expect(resp.ok).toBe(true);
|
|
169
|
-
expect(calls.length).toBe(3);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("429 with retry-after header sleeps that many seconds, throws RateLimitedError after maxRetries", async () => {
|
|
173
|
-
const clk = makeClock();
|
|
174
|
-
const { stub, calls } = makeFetchStub(
|
|
175
|
-
makeResponse({ status: 429, headers: { "retry-after": "2" } }),
|
|
176
|
-
);
|
|
177
|
-
const c = new HttpClient({
|
|
178
|
-
vendor: "kimi",
|
|
179
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
180
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
181
|
-
maxRetries: 1, baseBackoffMs: 1,
|
|
182
|
-
});
|
|
183
|
-
await expect(c.request("https://x")).rejects.toBeInstanceOf(RateLimitedError);
|
|
184
|
-
expect(calls.length).toBe(2); // initial + 1 retry
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("network error retries then re-throws", async () => {
|
|
188
|
-
const clk = makeClock();
|
|
189
|
-
const { stub, calls } = makeFetchStub([
|
|
190
|
-
new Error("ECONNRESET"),
|
|
191
|
-
makeResponse({ body: { ok: 1 } }),
|
|
192
|
-
]);
|
|
193
|
-
const c = new HttpClient({
|
|
194
|
-
vendor: "deepseek",
|
|
195
|
-
rateLimits: { perMinute: 0, minIntervalMs: 0 },
|
|
196
|
-
fetch: stub, sleep: makeSleep(clk), now: clk.now,
|
|
197
|
-
maxRetries: 2, baseBackoffMs: 1,
|
|
198
|
-
});
|
|
199
|
-
const resp = await c.request("https://x");
|
|
200
|
-
expect(resp.ok).toBe(true);
|
|
201
|
-
expect(calls.length).toBe(2);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// ─── rate limit gating ───────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
describe("HttpClient — rate limits", () => {
|
|
208
|
-
it("enforces minIntervalMs between calls", async () => {
|
|
209
|
-
const clk = makeClock();
|
|
210
|
-
let sleepCalls = [];
|
|
211
|
-
const sleep = async (ms) => { sleepCalls.push(ms); clk.advance(ms); };
|
|
212
|
-
const { stub } = makeFetchStub(makeResponse({ body: { ok: 1 } }));
|
|
213
|
-
const c = new HttpClient({
|
|
214
|
-
vendor: "deepseek",
|
|
215
|
-
rateLimits: { perMinute: 0, minIntervalMs: 1500 },
|
|
216
|
-
fetch: stub, sleep, now: clk.now,
|
|
217
|
-
});
|
|
218
|
-
await c.request("https://x");
|
|
219
|
-
await c.request("https://x");
|
|
220
|
-
// The first call has no prior; the second waits 1500ms.
|
|
221
|
-
expect(sleepCalls).toContain(1500);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("enforces perMinute sliding-window cap", async () => {
|
|
225
|
-
const clk = makeClock();
|
|
226
|
-
let sleepCalls = [];
|
|
227
|
-
const sleep = async (ms) => { sleepCalls.push(ms); clk.advance(ms); };
|
|
228
|
-
const { stub } = makeFetchStub(makeResponse({ body: { ok: 1 } }));
|
|
229
|
-
const c = new HttpClient({
|
|
230
|
-
vendor: "kimi",
|
|
231
|
-
rateLimits: { perMinute: 2, minIntervalMs: 0 },
|
|
232
|
-
fetch: stub, sleep, now: clk.now,
|
|
233
|
-
});
|
|
234
|
-
await c.request("https://x");
|
|
235
|
-
await c.request("https://x");
|
|
236
|
-
await c.request("https://x"); // should wait
|
|
237
|
-
// The 3rd call's enforce path should sleep until the 60s window has slid
|
|
238
|
-
// past the first call's timestamp.
|
|
239
|
-
const waited = sleepCalls.find((ms) => ms > 0 && ms <= 60_000);
|
|
240
|
-
expect(waited).toBeDefined();
|
|
241
|
-
});
|
|
242
|
-
});
|