@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
package/lib/prompt-builder.js
CHANGED
|
@@ -48,11 +48,25 @@ const CROSS_APP_HEADER = "CROSS_APP_OVERVIEW (跨 app 汇聚画像 — 各 app
|
|
|
48
48
|
* Trim an event down to the fields the LLM actually needs. Saves tokens +
|
|
49
49
|
* reduces prompt injection surface (no raw `extra` blob).
|
|
50
50
|
*/
|
|
51
|
+
// Local-time "YYYY-MM-DD HH:mm" for the LLM. Passing the raw epoch-ms integer
|
|
52
|
+
// (e.g. 1781706182375) made the model unreliable on "when did I…" questions —
|
|
53
|
+
// it can't dependably convert epoch ms to a date. buildPrompt runs on the
|
|
54
|
+
// user's own machine (cc hub / desktop), so local getters are the user's TZ.
|
|
55
|
+
function fmtLocalDateTime(ms) {
|
|
56
|
+
const d = new Date(ms);
|
|
57
|
+
if (!Number.isFinite(d.getTime())) return null;
|
|
58
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
59
|
+
return (
|
|
60
|
+
`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +
|
|
61
|
+
`${p(d.getHours())}:${p(d.getMinutes())}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
function summarizeEvent(e) {
|
|
52
66
|
const out = {
|
|
53
67
|
id: e.id,
|
|
54
68
|
type: e.subtype,
|
|
55
|
-
at: e.occurredAt,
|
|
69
|
+
at: fmtLocalDateTime(e.occurredAt) || e.occurredAt,
|
|
56
70
|
source: e.source && e.source.adapter,
|
|
57
71
|
};
|
|
58
72
|
if (e.actor) out.actor = e.actor;
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainlesschain/personal-data-hub",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"description": "Personal Data Hub — UnifiedSchema + validators + KG ingest helpers for the data-back-to-the-individual middleware",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./lib/index.js",
|
|
9
12
|
"./constants": "./lib/constants.js",
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { getAdapterGuide, ADAPTER_OVERRIDES } = require("../lib/adapter-guide");
|
|
6
|
-
|
|
7
|
-
describe("adapter-guide", () => {
|
|
8
|
-
it("wechat-pc guide reflects the 4.0 one-click reality (no manual PyWxDump as primary)", () => {
|
|
9
|
-
const g = getAdapterGuide("wechat-pc", "device");
|
|
10
|
-
// primary method is the automatic one-click, not manual decryption
|
|
11
|
-
const primary = g.methods[0];
|
|
12
|
-
expect(primary.recommended).toBe(true);
|
|
13
|
-
expect(primary.label).toMatch(/一键|自动/);
|
|
14
|
-
expect(primary.steps.join(" ")).toMatch(/一键采集|自动/);
|
|
15
|
-
// summary mentions the full coverage we now capture
|
|
16
|
-
expect(g.summary).toMatch(/公众号/);
|
|
17
|
-
expect(g.summary).toMatch(/朋友圈/);
|
|
18
|
-
expect(g.summary).toMatch(/收藏/);
|
|
19
|
-
// manual 3.x path is still offered as a fallback
|
|
20
|
-
expect(g.methods.some((m) => /3\.x|PyWxDump|手动/.test(m.label + m.steps.join(" ")))).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("the 6 social platforms all have a tailored one-click ADB guide", () => {
|
|
24
|
-
for (const name of [
|
|
25
|
-
"social-bilibili",
|
|
26
|
-
"social-weibo",
|
|
27
|
-
"social-douyin",
|
|
28
|
-
"social-xiaohongshu",
|
|
29
|
-
"social-toutiao",
|
|
30
|
-
"social-kuaishou",
|
|
31
|
-
]) {
|
|
32
|
-
expect(ADAPTER_OVERRIDES[name]).toBeTruthy();
|
|
33
|
-
const g = getAdapterGuide(name, "device");
|
|
34
|
-
const primary = g.methods[0];
|
|
35
|
-
expect(primary.recommended).toBe(true);
|
|
36
|
-
// recommended path is root-phone + one-click, not "go log in on the web"
|
|
37
|
-
expect(primary.label + primary.steps.join(" ")).toMatch(/一键|ADB|USB|root/i);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("unknown adapter falls back to a category guide without throwing", () => {
|
|
42
|
-
const g = getAdapterGuide("totally-unknown", "snapshot");
|
|
43
|
-
expect(g.category).toBe("snapshot");
|
|
44
|
-
expect(Array.isArray(g.methods)).toBe(true);
|
|
45
|
-
expect(g.methods.length).toBeGreaterThan(0);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const { assertAdapter, SENSITIVITY_LEVELS } = require("../lib/adapter-spec");
|
|
6
|
-
const { MockAdapter } = require("../lib/mock-adapter");
|
|
7
|
-
|
|
8
|
-
describe("assertAdapter", () => {
|
|
9
|
-
it("accepts a fully-valid adapter (MockAdapter)", () => {
|
|
10
|
-
const r = assertAdapter(new MockAdapter());
|
|
11
|
-
expect(r.ok).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("rejects non-object input", () => {
|
|
15
|
-
expect(assertAdapter(null).ok).toBe(false);
|
|
16
|
-
expect(assertAdapter(undefined).ok).toBe(false);
|
|
17
|
-
expect(assertAdapter("oops").ok).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("rejects missing required fields (collects all errors, no throw)", () => {
|
|
21
|
-
const r = assertAdapter({});
|
|
22
|
-
expect(r.ok).toBe(false);
|
|
23
|
-
// Many fields missing — at least name + version + capabilities + dataDisclosure + methods.
|
|
24
|
-
expect(r.errors.length).toBeGreaterThan(4);
|
|
25
|
-
expect(r.errors.some((e) => e.includes("name"))).toBe(true);
|
|
26
|
-
expect(r.errors.some((e) => e.includes("version"))).toBe(true);
|
|
27
|
-
expect(r.errors.some((e) => e.includes("authenticate"))).toBe(true);
|
|
28
|
-
expect(r.errors.some((e) => e.includes("sync"))).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("rejects invalid sensitivity", () => {
|
|
32
|
-
const a = new MockAdapter();
|
|
33
|
-
a.dataDisclosure = { ...a.dataDisclosure, sensitivity: "extreme" };
|
|
34
|
-
const r = assertAdapter(a);
|
|
35
|
-
expect(r.ok).toBe(false);
|
|
36
|
-
expect(r.errors.some((e) => e.includes("sensitivity"))).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects non-boolean legalGate", () => {
|
|
40
|
-
const a = new MockAdapter();
|
|
41
|
-
a.dataDisclosure = { ...a.dataDisclosure, legalGate: "yes" };
|
|
42
|
-
const r = assertAdapter(a);
|
|
43
|
-
expect(r.ok).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("rejects non-array capabilities", () => {
|
|
47
|
-
const a = new MockAdapter();
|
|
48
|
-
a.capabilities = "sync";
|
|
49
|
-
const r = assertAdapter(a);
|
|
50
|
-
expect(r.ok).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("rejects rateLimits with negative value", () => {
|
|
54
|
-
const a = new MockAdapter();
|
|
55
|
-
a.rateLimits = { perMinute: -1 };
|
|
56
|
-
expect(assertAdapter(a).ok).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("accepts adapter without rateLimits (optional field)", () => {
|
|
60
|
-
const a = new MockAdapter();
|
|
61
|
-
delete a.rateLimits;
|
|
62
|
-
expect(assertAdapter(a).ok).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("rejects non-function authenticate / sync / normalize / healthCheck", () => {
|
|
66
|
-
const a = new MockAdapter();
|
|
67
|
-
a.authenticate = "not a function";
|
|
68
|
-
expect(assertAdapter(a).ok).toBe(false);
|
|
69
|
-
|
|
70
|
-
const b = new MockAdapter();
|
|
71
|
-
b.normalize = 42;
|
|
72
|
-
expect(assertAdapter(b).ok).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("SENSITIVITY_LEVELS lists low/medium/high", () => {
|
|
76
|
-
expect(SENSITIVITY_LEVELS).toEqual(["low", "medium", "high"]);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
COOKIE_SPEC_VERSION,
|
|
7
|
-
KNOWN_VENDORS,
|
|
8
|
-
COOKIE_CAPTURE_SPECS,
|
|
9
|
-
getSpec,
|
|
10
|
-
listVendors,
|
|
11
|
-
classifyProbedCookies,
|
|
12
|
-
validateCookieCaptureSpec,
|
|
13
|
-
_internal,
|
|
14
|
-
} = require("../../lib/adapters/ai-chat-history/cookie-capture-spec");
|
|
15
|
-
|
|
16
|
-
describe("cookie-capture-spec — Phase 10.3.1 matrix", () => {
|
|
17
|
-
it("exposes a positive integer COOKIE_SPEC_VERSION", () => {
|
|
18
|
-
expect(Number.isInteger(COOKIE_SPEC_VERSION)).toBe(true);
|
|
19
|
-
expect(COOKIE_SPEC_VERSION).toBeGreaterThanOrEqual(1);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("KNOWN_VENDORS contains exactly the 9 wired vendors", () => {
|
|
23
|
-
expect(KNOWN_VENDORS).toEqual([
|
|
24
|
-
"deepseek",
|
|
25
|
-
"kimi",
|
|
26
|
-
"tongyi",
|
|
27
|
-
"zhipu",
|
|
28
|
-
"hunyuan",
|
|
29
|
-
"qianfan",
|
|
30
|
-
"coze",
|
|
31
|
-
"dreamina",
|
|
32
|
-
"doubao",
|
|
33
|
-
]);
|
|
34
|
-
// Defensive — frozen so contributors don't accidentally mutate at runtime.
|
|
35
|
-
expect(Object.isFrozen(KNOWN_VENDORS)).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("ships 9 specs, one per KNOWN_VENDORS entry, no duplicates", () => {
|
|
39
|
-
expect(COOKIE_CAPTURE_SPECS.length).toBe(KNOWN_VENDORS.length);
|
|
40
|
-
const seen = new Set();
|
|
41
|
-
for (const s of COOKIE_CAPTURE_SPECS) {
|
|
42
|
-
expect(KNOWN_VENDORS).toContain(s.vendor);
|
|
43
|
-
expect(seen.has(s.vendor)).toBe(false);
|
|
44
|
-
seen.add(s.vendor);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("validates the shipped spec set without errors", () => {
|
|
49
|
-
const r = validateCookieCaptureSpec(undefined, { throwOnError: false });
|
|
50
|
-
expect(r.errors).toEqual([]);
|
|
51
|
-
expect(r.ok).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("each loginUrl host matches at least one cookieDomains entry", () => {
|
|
55
|
-
for (const s of COOKIE_CAPTURE_SPECS) {
|
|
56
|
-
const host = new URL(s.loginUrl).host;
|
|
57
|
-
const matched = s.cookieDomains.some((d) =>
|
|
58
|
-
d.startsWith(".") ? host.endsWith(d.slice(1)) : host === d,
|
|
59
|
-
);
|
|
60
|
-
expect({ vendor: s.vendor, host, matched }).toEqual({ vendor: s.vendor, host, matched: true });
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("each spec has non-empty requiredCookies + postLoginPathHints + positive maxAge", () => {
|
|
65
|
-
for (const s of COOKIE_CAPTURE_SPECS) {
|
|
66
|
-
expect(Array.isArray(s.requiredCookies)).toBe(true);
|
|
67
|
-
expect(s.requiredCookies.length).toBeGreaterThan(0);
|
|
68
|
-
expect(Array.isArray(s.postLoginPathHints)).toBe(true);
|
|
69
|
-
expect(s.postLoginPathHints.length).toBeGreaterThan(0);
|
|
70
|
-
expect(Number.isInteger(s.cookieMaxAgeHintDays)).toBe(true);
|
|
71
|
-
expect(s.cookieMaxAgeHintDays).toBeGreaterThan(0);
|
|
72
|
-
expect(typeof s.notes).toBe("string");
|
|
73
|
-
expect(s.notes.length).toBeGreaterThan(0);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("getSpec returns the right spec for a known vendor and null for unknown", () => {
|
|
78
|
-
const ds = getSpec("deepseek");
|
|
79
|
-
expect(ds).toBeTruthy();
|
|
80
|
-
expect(ds.vendor).toBe("deepseek");
|
|
81
|
-
expect(getSpec("notarealvendor")).toBeNull();
|
|
82
|
-
expect(getSpec("")).toBeNull();
|
|
83
|
-
expect(getSpec(undefined)).toBeNull();
|
|
84
|
-
expect(getSpec(null)).toBeNull();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("listVendors returns a copy (mutation does not affect KNOWN_VENDORS)", () => {
|
|
88
|
-
const arr = listVendors();
|
|
89
|
-
expect(arr).toEqual([...KNOWN_VENDORS]);
|
|
90
|
-
arr.push("hacked");
|
|
91
|
-
expect(KNOWN_VENDORS.includes("hacked")).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("classifyProbedCookies — required vs optional vs missing", () => {
|
|
96
|
-
it("returns ok=true when all required cookies present (object input)", () => {
|
|
97
|
-
const r = classifyProbedCookies("deepseek", {
|
|
98
|
-
userToken: "abc",
|
|
99
|
-
"intercom-session-deepseek": "xyz",
|
|
100
|
-
});
|
|
101
|
-
expect(r.ok).toBe(true);
|
|
102
|
-
expect(r.foundRequired).toEqual(["userToken"]);
|
|
103
|
-
expect(r.missingRequired).toEqual([]);
|
|
104
|
-
expect(r.foundOptional).toEqual(["intercom-session-deepseek"]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("returns ok=false when a required cookie is missing", () => {
|
|
108
|
-
const r = classifyProbedCookies("kimi", { refresh_token: "rt", session_id: "sid" });
|
|
109
|
-
expect(r.ok).toBe(false);
|
|
110
|
-
expect(r.missingRequired).toEqual(["access_token"]);
|
|
111
|
-
expect(r.foundOptional.sort()).toEqual(["refresh_token", "session_id"]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("accepts Electron Cookie[] shape (array of { name, value })", () => {
|
|
115
|
-
const r = classifyProbedCookies("zhipu", [
|
|
116
|
-
{ name: "chatglm_token", value: "tok" },
|
|
117
|
-
{ name: "cgsessionid", value: "sid" },
|
|
118
|
-
{ name: "unrelated", value: "x" },
|
|
119
|
-
]);
|
|
120
|
-
expect(r.ok).toBe(true);
|
|
121
|
-
expect(r.foundRequired).toEqual(["chatglm_token"]);
|
|
122
|
-
expect(r.foundOptional).toContain("cgsessionid");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("accepts raw 'k=v; k=v' string (web-shell paste fallback)", () => {
|
|
126
|
-
const raw = "sessionid=abc; sid_guard=xyz; passport_csrf_token=csrf; ;junk";
|
|
127
|
-
const r = classifyProbedCookies("doubao", raw);
|
|
128
|
-
expect(r.ok).toBe(true);
|
|
129
|
-
expect(r.foundRequired).toEqual(["sessionid"]);
|
|
130
|
-
expect(r.foundOptional.sort()).toEqual(["passport_csrf_token", "sid_guard"]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("string parser tolerates values containing '=' (e.g. base64)", () => {
|
|
134
|
-
// sessionid is base64 with '=' padding; only the FIRST '=' is the delimiter.
|
|
135
|
-
const raw = "sessionid=YWJjZGVmZ2g=; sid_guard=v1=";
|
|
136
|
-
const r = classifyProbedCookies("coze", raw);
|
|
137
|
-
expect(r.ok).toBe(true);
|
|
138
|
-
expect(r.foundRequired).toEqual(["sessionid"]);
|
|
139
|
-
// raw cookie value must be preserved verbatim including the trailing '='
|
|
140
|
-
const jar = _internal._normalizeCookieJar(raw);
|
|
141
|
-
expect(jar.sessionid).toBe("YWJjZGVmZ2g=");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("empty string / null / undefined / wrong type all produce ok=false", () => {
|
|
145
|
-
for (const input of ["", null, undefined, 42, true]) {
|
|
146
|
-
const r = classifyProbedCookies("doubao", input);
|
|
147
|
-
expect(r.ok).toBe(false);
|
|
148
|
-
expect(r.missingRequired).toEqual(["sessionid"]);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("returns UNKNOWN_VENDOR reason for an unregistered vendor name", () => {
|
|
153
|
-
const r = classifyProbedCookies("notarealvendor", { anything: "x" });
|
|
154
|
-
expect(r.ok).toBe(false);
|
|
155
|
-
expect(r.reason).toBe("UNKNOWN_VENDOR");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("treats empty-string cookie value as missing (not present)", () => {
|
|
159
|
-
const r = classifyProbedCookies("deepseek", { userToken: "" });
|
|
160
|
-
expect(r.ok).toBe(false);
|
|
161
|
-
expect(r.foundRequired).toEqual([]);
|
|
162
|
-
expect(r.missingRequired).toEqual(["userToken"]);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("validateCookieCaptureSpec — defensive guard catches malformed specs", () => {
|
|
167
|
-
it("flags unknown vendor", () => {
|
|
168
|
-
const { ok, errors } = validateCookieCaptureSpec(
|
|
169
|
-
[{ ...COOKIE_CAPTURE_SPECS[0], vendor: "ghostvendor" }],
|
|
170
|
-
{ throwOnError: false },
|
|
171
|
-
);
|
|
172
|
-
expect(ok).toBe(false);
|
|
173
|
-
expect(errors.join(" ")).toMatch(/unknown vendor/);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("flags loginUrl host not matching any cookieDomain", () => {
|
|
177
|
-
const broken = {
|
|
178
|
-
...COOKIE_CAPTURE_SPECS[0],
|
|
179
|
-
loginUrl: "https://malicious.example.com/",
|
|
180
|
-
};
|
|
181
|
-
const { ok, errors } = validateCookieCaptureSpec([broken], { throwOnError: false });
|
|
182
|
-
expect(ok).toBe(false);
|
|
183
|
-
expect(errors.join(" ")).toMatch(/does not match any cookieDomain/);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("flags empty requiredCookies / postLoginPathHints / invalid maxAge", () => {
|
|
187
|
-
const broken = {
|
|
188
|
-
...COOKIE_CAPTURE_SPECS[0],
|
|
189
|
-
requiredCookies: [],
|
|
190
|
-
postLoginPathHints: [],
|
|
191
|
-
cookieMaxAgeHintDays: 0,
|
|
192
|
-
};
|
|
193
|
-
const { ok, errors } = validateCookieCaptureSpec([broken], { throwOnError: false });
|
|
194
|
-
expect(ok).toBe(false);
|
|
195
|
-
const joined = errors.join(" ");
|
|
196
|
-
expect(joined).toMatch(/requiredCookies/);
|
|
197
|
-
expect(joined).toMatch(/postLoginPathHints/);
|
|
198
|
-
expect(joined).toMatch(/cookieMaxAgeHintDays/);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("flags duplicate vendor entries", () => {
|
|
202
|
-
const dup = [COOKIE_CAPTURE_SPECS[0], { ...COOKIE_CAPTURE_SPECS[0] }];
|
|
203
|
-
const { ok, errors } = validateCookieCaptureSpec(dup, { throwOnError: false });
|
|
204
|
-
expect(ok).toBe(false);
|
|
205
|
-
expect(errors.join(" ")).toMatch(/duplicate vendor/);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("throws by default when malformed (no opts)", () => {
|
|
209
|
-
expect(() => validateCookieCaptureSpec([{ vendor: "ghost" }])).toThrow(/Invalid cookie capture spec/);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
createAIChatHealthChecker,
|
|
7
|
-
DEFAULT_INTERVAL_MS,
|
|
8
|
-
DEFAULT_FIRST_RUN_DELAY_MS,
|
|
9
|
-
} = require("../../lib/adapters/ai-chat-history/health-checker");
|
|
10
|
-
|
|
11
|
-
// ─── fakes ────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
function makeFakeAccountsStore({ initial = {} } = {}) {
|
|
14
|
-
const store = new Map(Object.entries(initial));
|
|
15
|
-
return {
|
|
16
|
-
get: vi.fn(async (v) => store.get(v) || null),
|
|
17
|
-
put: vi.fn(async (v, e) => store.set(v, e)),
|
|
18
|
-
delete: vi.fn(async (v) => store.delete(v)),
|
|
19
|
-
list: vi.fn(async () => Array.from(store.values())),
|
|
20
|
-
_store: store,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeFakeAdapter({ behaviors = {} } = {}) {
|
|
25
|
-
// behaviors = { vendor: ({ok, reason?, throw?: msg}) }
|
|
26
|
-
return {
|
|
27
|
-
registerVendor: vi.fn(async (vendor, _cookies) => {
|
|
28
|
-
const b = behaviors[vendor];
|
|
29
|
-
if (!b) return { ok: true, userId: "u_" + vendor };
|
|
30
|
-
if (b.throw) throw new Error(b.throw);
|
|
31
|
-
return b;
|
|
32
|
-
}),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function makeFakeTimers() {
|
|
37
|
-
// Deterministic timer dispatcher we can step through. We track scheduled
|
|
38
|
-
// callbacks + their delays in a queue and let tests `flush` them in order.
|
|
39
|
-
const scheduled = [];
|
|
40
|
-
let _now = 0;
|
|
41
|
-
let _id = 0;
|
|
42
|
-
const setTimeout = (fn, ms) => {
|
|
43
|
-
const id = ++_id;
|
|
44
|
-
scheduled.push({ id, type: "timeout", fireAt: _now + ms, fn });
|
|
45
|
-
return id;
|
|
46
|
-
};
|
|
47
|
-
const setInterval = (fn, ms) => {
|
|
48
|
-
const id = ++_id;
|
|
49
|
-
scheduled.push({ id, type: "interval", fireAt: _now + ms, ms, fn });
|
|
50
|
-
return id;
|
|
51
|
-
};
|
|
52
|
-
const clearTimeout = (id) => {
|
|
53
|
-
const i = scheduled.findIndex((s) => s.id === id);
|
|
54
|
-
if (i >= 0) scheduled.splice(i, 1);
|
|
55
|
-
};
|
|
56
|
-
const clearInterval = clearTimeout;
|
|
57
|
-
async function advance(ms) {
|
|
58
|
-
_now += ms;
|
|
59
|
-
while (true) {
|
|
60
|
-
const due = scheduled
|
|
61
|
-
.filter((s) => s.fireAt <= _now)
|
|
62
|
-
.sort((a, b) => a.fireAt - b.fireAt);
|
|
63
|
-
if (due.length === 0) break;
|
|
64
|
-
const next = due[0];
|
|
65
|
-
const idx = scheduled.indexOf(next);
|
|
66
|
-
scheduled.splice(idx, 1);
|
|
67
|
-
if (next.type === "interval") {
|
|
68
|
-
scheduled.push({ ...next, fireAt: next.fireAt + next.ms });
|
|
69
|
-
}
|
|
70
|
-
await next.fn();
|
|
71
|
-
// Flush any fire-and-forget microtasks the callback kicked off
|
|
72
|
-
// (runOnce returns a promise; the callback doesn't await it).
|
|
73
|
-
await new Promise((r) => setImmediate(r));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
setTimeout, setInterval, clearTimeout, clearInterval,
|
|
78
|
-
clock: () => _now,
|
|
79
|
-
advance,
|
|
80
|
-
_scheduled: scheduled,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ─── construction guards ─────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
describe("createAIChatHealthChecker — guards", () => {
|
|
87
|
-
it("throws when accountsStore.list missing", () => {
|
|
88
|
-
expect(() => createAIChatHealthChecker({ accountsStore: {}, vendorAdapter: { registerVendor: () => {} } })).toThrow(/accountsStore.list/);
|
|
89
|
-
});
|
|
90
|
-
it("throws when accountsStore.put missing", () => {
|
|
91
|
-
expect(() => createAIChatHealthChecker({ accountsStore: { list: () => [] }, vendorAdapter: { registerVendor: () => {} } })).toThrow(/accountsStore.put/);
|
|
92
|
-
});
|
|
93
|
-
it("throws when vendorAdapter.registerVendor missing", () => {
|
|
94
|
-
expect(() => createAIChatHealthChecker({ accountsStore: makeFakeAccountsStore() })).toThrow(/vendorAdapter.registerVendor/);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ─── runOnce behavior ────────────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
describe("runOnce — health classification", () => {
|
|
101
|
-
let timers;
|
|
102
|
-
beforeEach(() => {
|
|
103
|
-
timers = makeFakeTimers();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("marks vendors ok when adapter returns ok=true", async () => {
|
|
107
|
-
const store = makeFakeAccountsStore({
|
|
108
|
-
initial: {
|
|
109
|
-
deepseek: { vendor: "deepseek", cookies: { userToken: "x" }, cookieSpecVersion: 1 },
|
|
110
|
-
kimi: { vendor: "kimi", cookies: { access_token: "y" }, cookieSpecVersion: 1 },
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
const adapter = makeFakeAdapter();
|
|
114
|
-
const hc = createAIChatHealthChecker({
|
|
115
|
-
accountsStore: store, vendorAdapter: adapter, _deps: timers,
|
|
116
|
-
});
|
|
117
|
-
const r = await hc.runOnce();
|
|
118
|
-
expect(r).toMatchObject({ checked: 2, ok: 2, failed: 0, mismatch: 0 });
|
|
119
|
-
expect(store._store.get("deepseek").lastHealth.ok).toBe(true);
|
|
120
|
-
expect(store._store.get("kimi").lastHealth.ok).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("marks vendors failed when adapter returns ok=false (with reason)", async () => {
|
|
124
|
-
const store = makeFakeAccountsStore({
|
|
125
|
-
initial: { deepseek: { vendor: "deepseek", cookies: {}, cookieSpecVersion: 1 } },
|
|
126
|
-
});
|
|
127
|
-
const adapter = makeFakeAdapter({
|
|
128
|
-
behaviors: { deepseek: { ok: false, reason: "COOKIE_EXPIRED" } },
|
|
129
|
-
});
|
|
130
|
-
const hc = createAIChatHealthChecker({
|
|
131
|
-
accountsStore: store, vendorAdapter: adapter, _deps: timers,
|
|
132
|
-
});
|
|
133
|
-
const r = await hc.runOnce();
|
|
134
|
-
expect(r).toMatchObject({ checked: 1, failed: 1, ok: 0 });
|
|
135
|
-
expect(store._store.get("deepseek").lastHealth).toMatchObject({
|
|
136
|
-
ok: false, reason: "COOKIE_EXPIRED",
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("marks SPEC_VERSION_MISMATCH when entry.cookieSpecVersion < specVersion", async () => {
|
|
141
|
-
const store = makeFakeAccountsStore({
|
|
142
|
-
initial: { deepseek: { vendor: "deepseek", cookies: { x: "y" }, cookieSpecVersion: 0 } },
|
|
143
|
-
});
|
|
144
|
-
const adapter = makeFakeAdapter();
|
|
145
|
-
const hc = createAIChatHealthChecker({
|
|
146
|
-
accountsStore: store, vendorAdapter: adapter, specVersion: 2, _deps: timers,
|
|
147
|
-
});
|
|
148
|
-
const r = await hc.runOnce();
|
|
149
|
-
expect(r).toMatchObject({ checked: 1, mismatch: 1, ok: 0, failed: 0 });
|
|
150
|
-
expect(store._store.get("deepseek").lastHealth).toMatchObject({
|
|
151
|
-
ok: false, reason: "SPEC_VERSION_MISMATCH",
|
|
152
|
-
});
|
|
153
|
-
// Adapter was NOT called because the version gate fired first
|
|
154
|
-
expect(adapter.registerVendor).not.toHaveBeenCalled();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("captures ADAPTER_THREW when registerVendor rejects", async () => {
|
|
158
|
-
const store = makeFakeAccountsStore({
|
|
159
|
-
initial: { deepseek: { vendor: "deepseek", cookies: { x: "y" }, cookieSpecVersion: 1 } },
|
|
160
|
-
});
|
|
161
|
-
const adapter = makeFakeAdapter({ behaviors: { deepseek: { throw: "net down" } } });
|
|
162
|
-
const hc = createAIChatHealthChecker({
|
|
163
|
-
accountsStore: store, vendorAdapter: adapter, _deps: timers,
|
|
164
|
-
});
|
|
165
|
-
const r = await hc.runOnce();
|
|
166
|
-
expect(r).toMatchObject({ checked: 1, failed: 1 });
|
|
167
|
-
expect(store._store.get("deepseek").lastHealth).toMatchObject({
|
|
168
|
-
ok: false, reason: "ADAPTER_THREW",
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("skips a run if previous one is still in flight", async () => {
|
|
173
|
-
const store = makeFakeAccountsStore({
|
|
174
|
-
initial: { deepseek: { vendor: "deepseek", cookies: {}, cookieSpecVersion: 1 } },
|
|
175
|
-
});
|
|
176
|
-
let release;
|
|
177
|
-
const adapter = {
|
|
178
|
-
registerVendor: vi.fn(() => new Promise((resolve) => {
|
|
179
|
-
release = () => resolve({ ok: true, userId: "u" });
|
|
180
|
-
})),
|
|
181
|
-
};
|
|
182
|
-
const hc = createAIChatHealthChecker({
|
|
183
|
-
accountsStore: store, vendorAdapter: adapter, _deps: timers,
|
|
184
|
-
});
|
|
185
|
-
const p1 = hc.runOnce();
|
|
186
|
-
const r2 = await hc.runOnce();
|
|
187
|
-
expect(r2.skipped).toBe(true);
|
|
188
|
-
release();
|
|
189
|
-
await p1;
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("returns 0 counts on empty accounts list", async () => {
|
|
193
|
-
const hc = createAIChatHealthChecker({
|
|
194
|
-
accountsStore: makeFakeAccountsStore(),
|
|
195
|
-
vendorAdapter: makeFakeAdapter(),
|
|
196
|
-
_deps: timers,
|
|
197
|
-
});
|
|
198
|
-
const r = await hc.runOnce();
|
|
199
|
-
expect(r).toMatchObject({ checked: 0, ok: 0, failed: 0, mismatch: 0 });
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ─── start / stop / interval scheduling ──────────────────────────────────
|
|
204
|
-
|
|
205
|
-
describe("start / stop / interval", () => {
|
|
206
|
-
let timers, store, adapter;
|
|
207
|
-
beforeEach(() => {
|
|
208
|
-
timers = makeFakeTimers();
|
|
209
|
-
store = makeFakeAccountsStore({
|
|
210
|
-
initial: { deepseek: { vendor: "deepseek", cookies: { userToken: "x" }, cookieSpecVersion: 1 } },
|
|
211
|
-
});
|
|
212
|
-
adapter = makeFakeAdapter();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("first run happens after firstRunDelayMs (30s default), then interval (6h)", async () => {
|
|
216
|
-
const hc = createAIChatHealthChecker({
|
|
217
|
-
accountsStore: store, vendorAdapter: adapter,
|
|
218
|
-
intervalMs: 6 * 3600_000, firstRunDelayMs: 30_000,
|
|
219
|
-
_deps: timers,
|
|
220
|
-
});
|
|
221
|
-
hc.start();
|
|
222
|
-
expect(hc.status().started).toBe(true);
|
|
223
|
-
// 10s in — nothing has fired yet
|
|
224
|
-
await timers.advance(10_000);
|
|
225
|
-
expect(adapter.registerVendor).not.toHaveBeenCalled();
|
|
226
|
-
// 30s mark — first run fires
|
|
227
|
-
await timers.advance(20_000);
|
|
228
|
-
expect(adapter.registerVendor).toHaveBeenCalledTimes(1);
|
|
229
|
-
// Another 6h — interval run #1
|
|
230
|
-
await timers.advance(6 * 3600_000);
|
|
231
|
-
expect(adapter.registerVendor).toHaveBeenCalledTimes(2);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("start is idempotent — second call returns false", () => {
|
|
235
|
-
const hc = createAIChatHealthChecker({
|
|
236
|
-
accountsStore: store, vendorAdapter: adapter, _deps: timers,
|
|
237
|
-
});
|
|
238
|
-
expect(hc.start()).toBe(true);
|
|
239
|
-
expect(hc.start()).toBe(false);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("stop clears pending first-run + interval", async () => {
|
|
243
|
-
const hc = createAIChatHealthChecker({
|
|
244
|
-
accountsStore: store, vendorAdapter: adapter, firstRunDelayMs: 30_000, _deps: timers,
|
|
245
|
-
});
|
|
246
|
-
hc.start();
|
|
247
|
-
hc.stop();
|
|
248
|
-
expect(hc.status().started).toBe(false);
|
|
249
|
-
// 60s in — nothing fires since we stopped
|
|
250
|
-
await timers.advance(60_000);
|
|
251
|
-
expect(adapter.registerVendor).not.toHaveBeenCalled();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("uses default 6h interval + 30s delay when not specified", () => {
|
|
255
|
-
const hc = createAIChatHealthChecker({
|
|
256
|
-
accountsStore: store, vendorAdapter: adapter, _deps: timers,
|
|
257
|
-
});
|
|
258
|
-
const s = hc.status();
|
|
259
|
-
expect(s.intervalMs).toBe(DEFAULT_INTERVAL_MS);
|
|
260
|
-
expect(s.firstRunDelayMs).toBe(DEFAULT_FIRST_RUN_DELAY_MS);
|
|
261
|
-
});
|
|
262
|
-
});
|