@chainlesschain/personal-data-hub 0.4.18 → 0.4.23

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.
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const os = require("node:os");
7
+ const crypto = require("node:crypto");
8
+
9
+ const {
10
+ TianyanchaAdapter,
11
+ extractData,
12
+ NAME,
13
+ VERSION,
14
+ SNAPSHOT_SCHEMA_VERSION,
15
+ } = require("../../lib/adapters/biz-tianyancha");
16
+
17
+ function writeTmp(content) {
18
+ const p = path.join(os.tmpdir(), `cc-tyc-${crypto.randomUUID()}.json`);
19
+ fs.writeFileSync(p, content, "utf-8");
20
+ return p;
21
+ }
22
+ async function collect(gen) {
23
+ const out = [];
24
+ for await (const x of gen) out.push(x);
25
+ return out;
26
+ }
27
+
28
+ const COOKIES = "auth_token=abc; TYCID=xyz";
29
+
30
+ const SNAP = JSON.stringify({
31
+ schemaVersion: 1,
32
+ snapshottedAt: 1716383000000,
33
+ account: { userId: "u1" },
34
+ events: [
35
+ { kind: "monitor", id: "mon-G1", companyId: "G1", companyName: "字节跳动有限公司", legalPerson: "张利东", regStatus: "存续", capturedAt: 1716300000000 },
36
+ { kind: "search", id: "s-1", query: "小米科技", companyName: "小米科技有限责任公司", capturedAt: 1716320000000 },
37
+ ],
38
+ });
39
+
40
+ describe("constants + extractData", () => {
41
+ it("name/version/schema", () => {
42
+ expect(NAME).toBe("biz-tianyancha");
43
+ expect(VERSION).toBe("0.1.0");
44
+ expect(SNAPSHOT_SCHEMA_VERSION).toBe(1);
45
+ });
46
+ it("extractData tolerant", () => {
47
+ expect(extractData({ data: [{ id: 1 }] })).toHaveLength(1);
48
+ expect(extractData({ data: { resultList: [{ id: 1 }] } })).toHaveLength(1);
49
+ expect(extractData({ list: [{ id: 1 }] })).toHaveLength(1);
50
+ expect(extractData({})).toEqual([]);
51
+ });
52
+ });
53
+
54
+ describe("TianyanchaAdapter snapshot mode", () => {
55
+ it("authenticate validates inputPath", async () => {
56
+ const p = writeTmp(SNAP);
57
+ try {
58
+ const a = new TianyanchaAdapter();
59
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
60
+ expect((await a.authenticate({ inputPath: path.join(os.tmpdir(), "no-tyc.json") })).reason).toBe("INPUT_PATH_UNREADABLE");
61
+ } finally {
62
+ fs.unlinkSync(p);
63
+ }
64
+ });
65
+
66
+ it("sync 2 kinds + normalize monitor→like / search→interaction", async () => {
67
+ const p = writeTmp(SNAP);
68
+ try {
69
+ const a = new TianyanchaAdapter();
70
+ const items = await collect(a.sync({ inputPath: p }));
71
+ expect(items.map((x) => x.kind)).toEqual(["monitor", "search"]);
72
+
73
+ const mon = a.normalize(items[0]);
74
+ expect(mon.events[0].subtype).toBe("like");
75
+ expect(mon.events[0].content.title).toBe("关注公司: 字节跳动有限公司");
76
+ expect(mon.events[0].extra.legalPerson).toBe("张利东");
77
+ expect(mon.events[0].extra.regStatus).toBe("存续");
78
+
79
+ const search = a.normalize(items[1]);
80
+ expect(search.events[0].subtype).toBe("interaction");
81
+ expect(search.events[0].content.title).toBe("搜索企业: 小米科技");
82
+ expect(search.events[0].extra.companyName).toBe("小米科技有限责任公司");
83
+ } finally {
84
+ fs.unlinkSync(p);
85
+ }
86
+ });
87
+
88
+ it("include + limit + schema mismatch + unknown kind", async () => {
89
+ const p = writeTmp(SNAP);
90
+ try {
91
+ const a = new TianyanchaAdapter();
92
+ expect((await collect(a.sync({ inputPath: p, include: { monitor: false } }))).map((x) => x.kind)).toEqual(["search"]);
93
+ expect(await collect(a.sync({ inputPath: p, limit: 1 }))).toHaveLength(1);
94
+ expect(() => a.normalize({ kind: "bogus", payload: {} })).toThrow(/unknown kind/);
95
+ } finally {
96
+ fs.unlinkSync(p);
97
+ }
98
+ const bad = writeTmp(JSON.stringify({ schemaVersion: 9, events: [] }));
99
+ try {
100
+ const a = new TianyanchaAdapter();
101
+ await expect(collect(a.sync({ inputPath: bad }))).rejects.toThrow(/schemaVersion mismatch/);
102
+ } finally {
103
+ fs.unlinkSync(bad);
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("TianyanchaAdapter cookie-api mode", () => {
109
+ it("authenticate cookie (userId optional)", async () => {
110
+ const a = new TianyanchaAdapter({ account: { cookies: COOKIES } });
111
+ expect(await a.authenticate()).toEqual({ ok: true, account: null, mode: "cookie" });
112
+ });
113
+
114
+ it("sync fetches monitor + search, normalizes", async () => {
115
+ const byUrl = (u) => (u.includes("monitor") ? "monitor" : "search");
116
+ const data = {
117
+ monitor: [{ graphId: "G9", companyName: "腾讯科技", legalPersonName: "马化腾", createTime: 1716300000 }],
118
+ search: [{ id: "h1", keyword: "阿里巴巴", searchTime: 1716320000 }],
119
+ };
120
+ const calls = [];
121
+ const a = new TianyanchaAdapter({
122
+ account: { cookies: COOKIES, userId: "u1" },
123
+ fetchFn: async ({ url, cookies, query, sign }) => {
124
+ const k = byUrl(url);
125
+ calls.push({ k, cookies, pageNum: query.pageNum, sign });
126
+ return { data: { list: query.pageNum === 1 ? data[k] : [] } };
127
+ },
128
+ });
129
+ const items = await collect(a.sync({}));
130
+ expect(items.map((x) => x.kind).sort()).toEqual(["monitor", "search"]);
131
+ expect(calls.every((c) => c.cookies === COOKIES && c.sign === null)).toBe(true);
132
+ const mon = a.normalize(items.find((x) => x.kind === "monitor"));
133
+ expect(mon.events[0].content.title).toBe("关注公司: 腾讯科技");
134
+ expect(mon.events[0].extra.legalPerson).toBe("马化腾");
135
+ const search = a.normalize(items.find((x) => x.kind === "search"));
136
+ expect(search.events[0].content.title).toBe("搜索企业: 阿里巴巴");
137
+ });
138
+
139
+ it("invokes signProvider + limit + empty + default fetch + no input", async () => {
140
+ const signCalls = [];
141
+ const a = new TianyanchaAdapter({
142
+ account: { cookies: COOKIES },
143
+ fetchFn: async ({ query }) => ({ data: { list: query.pageNum === 1 ? [{ graphId: "G1", companyName: "a" }, { graphId: "G2", companyName: "b" }] : [] } }),
144
+ signProvider: async (ctx) => { signCalls.push(ctx); return "sig"; },
145
+ });
146
+ expect(await collect(a.sync({ limit: 1, include: { search: false } }))).toHaveLength(1);
147
+ expect(signCalls.length).toBeGreaterThan(0);
148
+ expect(signCalls[0].cookies).toBe(COOKIES);
149
+
150
+ const a2 = new TianyanchaAdapter({ account: { cookies: COOKIES }, fetchFn: async () => "<html>login</html>" });
151
+ expect(await collect(a2.sync({}))).toEqual([]);
152
+
153
+ const a3 = new TianyanchaAdapter({ account: { cookies: COOKIES } });
154
+ await expect(collect(a3.sync({}))).rejects.toThrow(/no fetchFn configured/);
155
+
156
+ const a4 = new TianyanchaAdapter();
157
+ await expect(collect(a4.sync({}))).rejects.toThrow(/needs opts.inputPath/);
158
+ });
159
+ });
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const os = require("node:os");
7
+ const crypto = require("node:crypto");
8
+
9
+ const cs = require("../../lib/adapters/doc-camscanner");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-cs-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+
22
+ const COOKIES = "INTSIG_TOKEN=abc; PHPSESSID=xyz";
23
+
24
+ describe("doc-camscanner mappers", () => {
25
+ it("name/version", () => {
26
+ expect(cs.NAME).toBe("doc-camscanner");
27
+ expect(cs.VERSION).toBe("0.1.0");
28
+ });
29
+ it("mapDoc maps CamScanner fields; doc_type + extension → docType", () => {
30
+ const rec = cs.mapDoc({
31
+ sync_doc_id: "DOC123",
32
+ title: "营业执照",
33
+ doc_type: 2,
34
+ page_num: 3,
35
+ create_time: 1716383000,
36
+ modify_time: 1716390000,
37
+ pdf_url: "https://cs/DOC123.pdf",
38
+ tags: ["证件"],
39
+ });
40
+ expect(rec).toMatchObject({ docId: "DOC123", title: "营业执照", docType: "certificate" });
41
+ expect(rec.createdMs).toBe(1716383000000);
42
+ expect(rec.updatedMs).toBe(1716390000000);
43
+ expect(rec.url).toBe("https://cs/DOC123.pdf");
44
+ expect(rec.extra.pageNum).toBe(3);
45
+ expect(rec.extra.tags).toEqual(["证件"]);
46
+ // extension fallback when no doc_type
47
+ expect(cs.mapDoc({ doc_id: 1, title: "报表.xlsx" }).docType).toBe("excel");
48
+ expect(cs.mapDoc({ doc_id: 2, title: "合同.pdf" }).docType).toBe("pdf");
49
+ // default → scan
50
+ expect(cs.mapDoc({ doc_id: 3, title: "随手拍" }).docType).toBe("scan");
51
+ // no id → null
52
+ expect(cs.mapDoc({ title: "noid" })).toBe(null);
53
+ });
54
+ it("extractDocs tolerant across shapes", () => {
55
+ expect(cs.extractDocs({ docs: [{ doc_id: 1 }] })).toHaveLength(1);
56
+ expect(cs.extractDocs({ list: [{ doc_id: 1 }] })).toHaveLength(1);
57
+ expect(cs.extractDocs({ data: { docs: [{ doc_id: 1 }] } })).toHaveLength(1);
58
+ expect(cs.extractDocs({})).toEqual([]);
59
+ });
60
+ });
61
+
62
+ describe("CamScannerDocAdapter (via _document-base)", () => {
63
+ const SNAP = JSON.stringify({
64
+ schemaVersion: 1,
65
+ snapshottedAt: 1716383000000,
66
+ account: { userId: "u1" },
67
+ events: [
68
+ {
69
+ kind: "document",
70
+ id: "doc-S1",
71
+ docId: "S1",
72
+ title: "身份证扫描",
73
+ docType: "certificate",
74
+ updatedTime: 1716383000,
75
+ url: "https://cs/S1.pdf",
76
+ },
77
+ ],
78
+ });
79
+
80
+ it("snapshot sync + normalize → event(post)+item(document)", async () => {
81
+ const p = writeTmp(SNAP);
82
+ try {
83
+ const a = new cs.CamScannerDocAdapter();
84
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
85
+ const items = await collect(a.sync({ inputPath: p }));
86
+ expect(items).toHaveLength(1);
87
+ const batch = a.normalize(items[0]);
88
+ expect(batch.events[0].subtype).toBe("post");
89
+ expect(batch.items[0].subtype).toBe("document");
90
+ expect(batch.items[0].name).toBe("身份证扫描");
91
+ expect(batch.items[0].extra.platform).toBe("camscanner");
92
+ } finally {
93
+ fs.unlinkSync(p);
94
+ }
95
+ });
96
+
97
+ it("cookie-api: fetch + paginate + normalize", async () => {
98
+ const pages = [
99
+ { docs: [{ sync_doc_id: 7, title: "发票.pdf", doc_type: "pdf", modify_time: 1716383000 }] },
100
+ { docs: [] },
101
+ ];
102
+ const calls = [];
103
+ const a = new cs.CamScannerDocAdapter({
104
+ account: { cookies: COOKIES, userId: "u1" },
105
+ fetchFn: async ({ url, cookies, query, sign }) => {
106
+ calls.push({ url, cookies, offset: query.offset, sign });
107
+ return query.offset === 0 ? pages[0] : pages[1];
108
+ },
109
+ });
110
+ expect(await a.authenticate()).toEqual({ ok: true, account: "u1", mode: "cookie" });
111
+ const items = await collect(a.sync({}));
112
+ expect(items).toHaveLength(1);
113
+ expect(items[0].originalId).toBe("camscanner:document:7");
114
+ expect(calls[0].cookies).toBe(COOKIES);
115
+ expect(calls[0].sign).toBe(null);
116
+ const batch = a.normalize(items[0]);
117
+ expect(batch.items[0].name).toBe("发票.pdf");
118
+ expect(batch.items[0].extra.docType).toBe("pdf");
119
+ });
120
+
121
+ it("cookie-api: signProvider seam invoked when present", async () => {
122
+ let seen = null;
123
+ const a = new cs.CamScannerDocAdapter({
124
+ account: { cookies: COOKIES, userId: "u1" },
125
+ signProvider: async ({ url, query }) => {
126
+ seen = { url, offset: query.offset };
127
+ return "sig-abc";
128
+ },
129
+ fetchFn: async ({ sign, query }) => {
130
+ return query.offset === 0
131
+ ? { docs: [{ sync_doc_id: "X", title: "t", _sign: sign }] }
132
+ : { docs: [] };
133
+ },
134
+ });
135
+ const items = await collect(a.sync({}));
136
+ expect(items).toHaveLength(1);
137
+ expect(seen.offset).toBe(0);
138
+ expect(seen.url).toContain("intsig");
139
+ });
140
+
141
+ it("default fetch throws; no input throws", async () => {
142
+ const a = new cs.CamScannerDocAdapter({ account: { cookies: COOKIES } });
143
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
144
+ const b = new cs.CamScannerDocAdapter();
145
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
146
+ });
147
+ });
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const os = require("node:os");
7
+ const crypto = require("node:crypto");
8
+
9
+ const ix = require("../../lib/adapters/gov-ixiamen");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-ix-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+
22
+ const COOKIES = "XMGOV_SSO=abc; tgc=xyz";
23
+
24
+ describe("gov-ixiamen mappers", () => {
25
+ it("name/version/capabilities", () => {
26
+ expect(ix.NAME).toBe("gov-ixiamen");
27
+ expect(ix.VERSION).toBe("0.1.0");
28
+ });
29
+ it("inferCategory: explicit wins, else keyword, else fallback", () => {
30
+ expect(ix.inferCategory("随便", "医保")).toBe("医保");
31
+ expect(ix.inferCategory("城乡居民医保缴费")).toBe("医保");
32
+ expect(ix.inferCategory("住房公积金提取")).toBe("公积金");
33
+ expect(ix.inferCategory("机动车违章处理")).toBe("车驾管");
34
+ expect(ix.inferCategory("某个没见过的事项")).toBe("其他政务");
35
+ });
36
+ it("mapService maps gov-service fields; no id → null", () => {
37
+ const rec = ix.mapService({
38
+ service_id: "S1",
39
+ service_name: "养老保险待遇资格认证",
40
+ handle_time: 1716383000,
41
+ status: "已办结",
42
+ deptName: "厦门市社保中心",
43
+ });
44
+ expect(rec).toMatchObject({ serviceId: "S1", category: "社保", status: "已办结", dept: "厦门市社保中心" });
45
+ expect(rec.handledMs).toBe(1716383000000);
46
+ expect(ix.mapService({ service_name: "noid" })).toBe(null);
47
+ });
48
+ it("extractList tolerant", () => {
49
+ expect(ix.extractList({ list: [{ id: 1 }] })).toHaveLength(1);
50
+ expect(ix.extractList({ data: { records: [{ id: 1 }] } })).toHaveLength(1);
51
+ expect(ix.extractList({})).toEqual([]);
52
+ });
53
+ });
54
+
55
+ describe("IXiamenAdapter (snapshot + cookie-api)", () => {
56
+ const SNAP = JSON.stringify({
57
+ schemaVersion: 1,
58
+ snapshottedAt: 1716383000000,
59
+ account: { userId: "u1", name: "张三" },
60
+ events: [
61
+ {
62
+ kind: "service",
63
+ id: "svc-S1",
64
+ serviceId: "S1",
65
+ serviceName: "住房公积金缴存明细查询",
66
+ handledTime: 1716383000,
67
+ status: "已办结",
68
+ dept: "厦门住房公积金中心",
69
+ },
70
+ ],
71
+ });
72
+
73
+ it("snapshot sync + normalize → INTERACTION event + category topic", async () => {
74
+ const p = writeTmp(SNAP);
75
+ try {
76
+ const a = new ix.IXiamenAdapter();
77
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
78
+ const items = await collect(a.sync({ inputPath: p }));
79
+ expect(items).toHaveLength(1);
80
+ expect(items[0].originalId).toBe("ixiamen:service:S1");
81
+ const batch = a.normalize(items[0]);
82
+ expect(batch.events[0].subtype).toBe("interaction");
83
+ expect(batch.events[0].content.title).toBe("办理: 住房公积金缴存明细查询");
84
+ expect(batch.events[0].extra.category).toBe("公积金");
85
+ expect(batch.topics[0].name).toBe("公积金");
86
+ expect(batch.topics[0].id).toBe("topic-ixiamen-cat-公积金");
87
+ expect(batch.events[0].extra.topicRef).toBe("topic-ixiamen-cat-公积金");
88
+ } finally {
89
+ fs.unlinkSync(p);
90
+ }
91
+ });
92
+
93
+ it("dataDisclosure: high sensitivity + legalGate (gov real-name)", () => {
94
+ const a = new ix.IXiamenAdapter();
95
+ expect(a.dataDisclosure.sensitivity).toBe("high");
96
+ expect(a.dataDisclosure.legalGate).toBe(true);
97
+ });
98
+
99
+ it("cookie-api: best-effort fetch + paginate + unverified flag", async () => {
100
+ const pages = [
101
+ { list: [{ id: 7, name: "门诊报销", category: "医保", handledTime: 1716383000 }] },
102
+ { list: [] },
103
+ ];
104
+ const calls = [];
105
+ const a = new ix.IXiamenAdapter({
106
+ account: { cookies: COOKIES, userId: "u1" },
107
+ fetchFn: async ({ url, cookies, query, sign }) => {
108
+ calls.push({ url, cookies, page: query.page, sign });
109
+ return query.page === 1 ? pages[0] : pages[1];
110
+ },
111
+ });
112
+ const auth = await a.authenticate();
113
+ expect(auth).toMatchObject({ ok: true, mode: "cookie", unverified: true });
114
+ const items = await collect(a.sync({}));
115
+ expect(items).toHaveLength(1);
116
+ expect(items[0].originalId).toBe("ixiamen:service:7");
117
+ expect(calls[0].cookies).toBe(COOKIES);
118
+ expect(calls[0].sign).toBe(null);
119
+ const batch = a.normalize(items[0]);
120
+ expect(batch.events[0].extra.category).toBe("医保");
121
+ });
122
+
123
+ it("cookie-api: signProvider seam invoked + sinceWatermark stops early", async () => {
124
+ let seen = null;
125
+ const a = new ix.IXiamenAdapter({
126
+ account: { cookies: COOKIES },
127
+ signProvider: async ({ url, query }) => {
128
+ seen = { url, page: query.page };
129
+ return "gov-sig";
130
+ },
131
+ fetchFn: async () => ({
132
+ list: [
133
+ { id: "new", name: "新事项", handledTime: 1716390000 },
134
+ { id: "old", name: "旧事项", handledTime: 1700000000 },
135
+ ],
136
+ }),
137
+ });
138
+ const items = await collect(a.sync({ sinceWatermark: 1716000000000 }));
139
+ expect(items).toHaveLength(1); // old one below watermark stops iteration
140
+ expect(items[0].originalId).toBe("ixiamen:service:new");
141
+ expect(seen.url).toContain("ixm.gov.cn");
142
+ });
143
+
144
+ it("default fetch throws; no input throws", async () => {
145
+ const a = new ix.IXiamenAdapter({ account: { cookies: COOKIES } });
146
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
147
+ const b = new ix.IXiamenAdapter();
148
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
149
+ });
150
+ });
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ import { describe, it, expect } from "vitest";
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const os = require("node:os");
7
+ const crypto = require("node:crypto");
8
+
9
+ const tx = require("../../lib/adapters/gov-tax");
10
+
11
+ function writeTmp(content) {
12
+ const p = path.join(os.tmpdir(), `cc-tax-${crypto.randomUUID()}.json`);
13
+ fs.writeFileSync(p, content, "utf-8");
14
+ return p;
15
+ }
16
+ async function collect(gen) {
17
+ const out = [];
18
+ for await (const x of gen) out.push(x);
19
+ return out;
20
+ }
21
+
22
+ const COOKIES = "ETAX_SSO=abc; sid=xyz";
23
+
24
+ describe("gov-tax mappers", () => {
25
+ it("name/version", () => {
26
+ expect(tx.NAME).toBe("gov-tax");
27
+ expect(tx.VERSION).toBe("0.1.0");
28
+ });
29
+ it("periodToMs / toAmount", () => {
30
+ expect(tx.periodToMs("2025-03")).toBe(Date.parse("2025-03-01T00:00:00Z"));
31
+ expect(tx.periodToMs("202503")).toBe(Date.parse("2025-03-01T00:00:00Z"));
32
+ expect(tx.toAmount("¥1,234.56")).toBeCloseTo(1234.56, 2);
33
+ expect(tx.toAmount(2000)).toBe(2000);
34
+ expect(tx.toAmount("abc")).toBe(null);
35
+ });
36
+ it("mapIncome / mapDeclaration field aliases; no id → null", () => {
37
+ const inc = tx.mapIncome({ record_id: "I1", tax_period: "2025-03", income_type: "工资薪金", amount: "20,000", withheldTax: 1234.56, company: "某某公司", companyId: "9144" });
38
+ expect(inc).toMatchObject({ recordId: "I1", incomeType: "工资薪金", payerName: "某某公司", payerId: "9144" });
39
+ expect(inc.amount).toBe(20000);
40
+ expect(inc.withheld).toBeCloseTo(1234.56, 2);
41
+ expect(tx.mapIncome({ amount: 1 })).toBe(null);
42
+ const dec = tx.mapDeclaration({ id: "D1", tax_year: 2024, type: "综合所得年度汇算", status: "已退税", amount: -800 });
43
+ expect(dec).toMatchObject({ recordId: "D1", year: 2024, declType: "综合所得年度汇算", status: "已退税", settleAmount: -800 });
44
+ expect(tx.mapDeclaration({ year: 2024 })).toBe(null);
45
+ });
46
+ it("extractList tolerant", () => {
47
+ expect(tx.extractList({ list: [{ id: 1 }] })).toHaveLength(1);
48
+ expect(tx.extractList({ data: { records: [{ id: 1 }] } })).toHaveLength(1);
49
+ expect(tx.extractList({})).toEqual([]);
50
+ });
51
+ });
52
+
53
+ describe("TaxAdapter (snapshot + cookie-api)", () => {
54
+ const SNAP = JSON.stringify({
55
+ schemaVersion: 1,
56
+ snapshottedAt: 1716383000000,
57
+ account: { userId: "u1" },
58
+ events: [
59
+ { kind: "income", id: "inc-I1", recordId: "I1", period: "2025-03", incomeType: "工资薪金", amount: 20000, withheld: 1234.56, payerName: "某某科技有限公司", payerId: "9144ABC" },
60
+ { kind: "declaration", id: "dec-D1", recordId: "D1", year: 2024, declType: "综合所得年度汇算", status: "已退税", settleAmount: -800, declaredAt: 1716383000 },
61
+ ],
62
+ });
63
+
64
+ it("income → INCOME event + employer Person(MERCHANT)", async () => {
65
+ const p = writeTmp(SNAP);
66
+ try {
67
+ const a = new tx.TaxAdapter();
68
+ expect((await a.authenticate({ inputPath: p })).mode).toBe("snapshot-file");
69
+ const items = await collect(a.sync({ inputPath: p }));
70
+ expect(items).toHaveLength(2);
71
+ const inc = a.normalize(items[0]);
72
+ expect(inc.events[0].subtype).toBe("income");
73
+ expect(inc.events[0].extra.incomeType).toBe("工资薪金");
74
+ expect(inc.events[0].extra.amount).toBe(20000);
75
+ expect(inc.persons).toHaveLength(1);
76
+ expect(inc.persons[0].subtype).toBe("merchant");
77
+ expect(inc.persons[0].names[0]).toBe("某某科技有限公司");
78
+ expect(inc.events[0].extra.payerRef).toBe(inc.persons[0].id);
79
+ expect(items[0].originalId).toBe("tax:income:I1");
80
+ } finally {
81
+ fs.unlinkSync(p);
82
+ }
83
+ });
84
+
85
+ it("declaration → OTHER event with settleAmount", async () => {
86
+ const p = writeTmp(SNAP);
87
+ try {
88
+ const a = new tx.TaxAdapter();
89
+ const items = await collect(a.sync({ inputPath: p }));
90
+ const dec = a.normalize(items[1]);
91
+ expect(dec.events[0].subtype).toBe("other");
92
+ expect(dec.events[0].content.title).toContain("个税申报");
93
+ expect(dec.events[0].extra.settleAmount).toBe(-800);
94
+ expect(dec.events[0].extra.year).toBe(2024);
95
+ expect(items[1].originalId).toBe("tax:declaration:D1");
96
+ } finally {
97
+ fs.unlinkSync(p);
98
+ }
99
+ });
100
+
101
+ it("dataDisclosure: high sensitivity + legalGate (financial/tax)", () => {
102
+ const a = new tx.TaxAdapter();
103
+ expect(a.dataDisclosure.sensitivity).toBe("high");
104
+ expect(a.dataDisclosure.legalGate).toBe(true);
105
+ });
106
+
107
+ it("cookie-api: best-effort both kinds + unverified flag + sign seam", async () => {
108
+ let signed = 0;
109
+ const a = new tx.TaxAdapter({
110
+ account: { cookies: COOKIES, userId: "u1" },
111
+ signProvider: async () => {
112
+ signed += 1;
113
+ return "sig";
114
+ },
115
+ fetchFn: async ({ url, query }) => {
116
+ if (query.page > 1) return { list: [] };
117
+ if (url.includes("/income")) return { list: [{ recordId: "I9", period: "2025-01", incomeType: "劳务报酬", amount: 5000 }] };
118
+ return { list: [{ recordId: "D9", year: 2024, declType: "年度汇算", settleAmount: 300 }] };
119
+ },
120
+ });
121
+ const auth = await a.authenticate();
122
+ expect(auth).toMatchObject({ ok: true, mode: "cookie", unverified: true });
123
+ const items = await collect(a.sync({}));
124
+ expect(items).toHaveLength(2);
125
+ expect(items.map((i) => i.originalId).sort()).toEqual(["tax:declaration:D9", "tax:income:I9"]);
126
+ expect(signed).toBeGreaterThan(0);
127
+ });
128
+
129
+ it("default fetch throws; no input throws", async () => {
130
+ const a = new tx.TaxAdapter({ account: { cookies: COOKIES } });
131
+ await expect(collect(a.sync({}))).rejects.toThrow(/no fetchFn configured/);
132
+ const b = new tx.TaxAdapter();
133
+ await expect(collect(b.sync({}))).rejects.toThrow(/needs opts.inputPath/);
134
+ });
135
+ });