@modusensus/dsh-mneme 0.7.10 → 0.7.12
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/README.en.md +437 -0
- package/README.md +364 -166
- package/bin/cli.mjs +603 -0
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/lib/api-standalone.js +264 -0
- package/{dsh-mneme/lib → lib}/api.js +133 -289
- package/{dsh-mneme/lib → lib}/client.js +1008 -922
- package/{dsh-mneme/lib → lib}/config.js +54 -99
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +45 -42
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/src → lib}/settings.js +34 -64
- package/{dsh-mneme/lib → lib}/store.js +38 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +45 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/src/api-standalone.js +264 -0
- package/{dsh-mneme/src → src}/api.js +133 -289
- package/{dsh-mneme/src → src}/config.js +54 -99
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +45 -42
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/lib → src}/settings.js +34 -64
- package/{dsh-mneme/src → src}/store.js +38 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +45 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +16 -35
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/test/standalone-api.test.js +326 -0
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
computeHeat,
|
|
6
|
-
buildHeatSignals,
|
|
7
|
-
TYPE_DECAY_DEFAULTS,
|
|
8
|
-
} from '../src/heat.js';
|
|
9
|
-
|
|
10
|
-
const HOUR = 3600000;
|
|
11
|
-
|
|
12
|
-
describe('heat.js', () => {
|
|
13
|
-
test('未知类型使用默认 λ=0.002;72h 后 heat < 1 且 > 0.8,并随 Δt 单调递减', () => {
|
|
14
|
-
const now = Date.now();
|
|
15
|
-
const base = { type: 'episodic', last_accessed_at: now - 72 * HOUR };
|
|
16
|
-
|
|
17
|
-
const heat72 = computeHeat(base, now, {});
|
|
18
|
-
const heat144 = computeHeat(
|
|
19
|
-
{ ...base, last_accessed_at: now - 144 * HOUR },
|
|
20
|
-
now,
|
|
21
|
-
{}
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
assert(heat72 < 1.0, '72h 后热度应小于 1');
|
|
25
|
-
assert(heat72 > 0.8, '72h 后热度应仍大于 0.8');
|
|
26
|
-
assert(heat144 < heat72, 'Δt 越大,热度应越低');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('λ=0 的免疫类型任意 Δt 返回 1.0', () => {
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
const config = { heatTypeDecay: { preference: 0 } };
|
|
32
|
-
|
|
33
|
-
assert.strictEqual(
|
|
34
|
-
computeHeat(
|
|
35
|
-
{ type: 'preference', last_accessed_at: now - 999 * 24 * HOUR },
|
|
36
|
-
now,
|
|
37
|
-
config
|
|
38
|
-
),
|
|
39
|
-
1.0
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
assert.strictEqual(
|
|
43
|
-
computeHeat(
|
|
44
|
-
{ type: 'pattern', last_accessed_at: now - 365 * 24 * HOUR },
|
|
45
|
-
now,
|
|
46
|
-
{ heatTypeDecay: TYPE_DECAY_DEFAULTS }
|
|
47
|
-
),
|
|
48
|
-
1.0
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('ref 优先使用 last_accessed_at,缺失退 created_at,皆无返回 1.0', () => {
|
|
53
|
-
const now = Date.now();
|
|
54
|
-
const config = {
|
|
55
|
-
heatTypeDecay: { decision: 0.002 },
|
|
56
|
-
heatGlobalAlpha: 1.0,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const withLast = computeHeat(
|
|
60
|
-
{
|
|
61
|
-
type: 'decision',
|
|
62
|
-
last_accessed_at: now - 24 * HOUR,
|
|
63
|
-
created_at: now - 100 * HOUR,
|
|
64
|
-
},
|
|
65
|
-
now,
|
|
66
|
-
config
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const withCreated = computeHeat(
|
|
70
|
-
{ type: 'decision', created_at: now - 24 * HOUR },
|
|
71
|
-
now,
|
|
72
|
-
config
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
assert(withLast < 1.0);
|
|
76
|
-
assert.strictEqual(withLast, withCreated);
|
|
77
|
-
|
|
78
|
-
const noRef = computeHeat({ type: 'decision' }, now, config);
|
|
79
|
-
assert.strictEqual(noRef, 1.0);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('非法 ref 或 now < ref 时返回 1.0', () => {
|
|
83
|
-
const now = Date.now();
|
|
84
|
-
|
|
85
|
-
assert.strictEqual(
|
|
86
|
-
computeHeat(
|
|
87
|
-
{ type: 'decision', last_accessed_at: 'not-a-date' },
|
|
88
|
-
now,
|
|
89
|
-
{}
|
|
90
|
-
),
|
|
91
|
-
1.0
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
assert.strictEqual(
|
|
95
|
-
computeHeat(
|
|
96
|
-
{ type: 'decision', last_accessed_at: now + 1000 },
|
|
97
|
-
now,
|
|
98
|
-
{}
|
|
99
|
-
),
|
|
100
|
-
1.0
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('α 越大衰减越快(同一 Δt 下 α=2 的热度低于 α=1)', () => {
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
const base = {
|
|
107
|
-
type: 'decision',
|
|
108
|
-
last_accessed_at: now - 7 * 24 * HOUR,
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const h1 = computeHeat(base, now, {
|
|
112
|
-
heatTypeDecay: { decision: 0.002 },
|
|
113
|
-
heatGlobalAlpha: 1.0,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const h2 = computeHeat(base, now, {
|
|
117
|
-
heatTypeDecay: { decision: 0.002 },
|
|
118
|
-
heatGlobalAlpha: 2.0,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
assert(h2 < h1, 'α 更大时,同一 Δt 热度应更低');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('buildHeatSignals 返回字段齐全且 deltaHours 正确', () => {
|
|
125
|
-
const now = 1000000000000; // 固定毫秒时间戳
|
|
126
|
-
const ref = now - 12 * HOUR;
|
|
127
|
-
|
|
128
|
-
const signals = buildHeatSignals(
|
|
129
|
-
{ type: 'project', last_accessed_at: ref },
|
|
130
|
-
{ heatGlobalAlpha: 1.5 },
|
|
131
|
-
now
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
assert.deepStrictEqual(Object.keys(signals).sort(), [
|
|
135
|
-
'alpha',
|
|
136
|
-
'deltaHours',
|
|
137
|
-
'lambda',
|
|
138
|
-
'ref',
|
|
139
|
-
'type',
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
assert.strictEqual(signals.type, 'project');
|
|
143
|
-
assert.strictEqual(signals.lambda, 0.0008);
|
|
144
|
-
assert.strictEqual(signals.alpha, 1.5);
|
|
145
|
-
assert.strictEqual(signals.ref, ref);
|
|
146
|
-
assert.strictEqual(signals.deltaHours, 12);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { createStore } from "../src/store.js";
|
|
4
|
-
import { createService } from "../src/service.js";
|
|
5
|
-
import { createInjector } from "../src/inject.js";
|
|
6
|
-
import { createSettings } from "../src/settings.js";
|
|
7
|
-
|
|
8
|
-
function setup(over = {}) {
|
|
9
|
-
const store = createStore(":memory:");
|
|
10
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
11
|
-
const settings = createSettings(store.db);
|
|
12
|
-
const contexts = [];
|
|
13
|
-
const disposers = [];
|
|
14
|
-
const ctx = {
|
|
15
|
-
systemPrompt: {
|
|
16
|
-
context(def) {
|
|
17
|
-
contexts.push(def);
|
|
18
|
-
const dispose = () => disposers.push(def.name);
|
|
19
|
-
return dispose;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
|
|
24
|
-
const injector = createInjector(ctx, service, settings, config);
|
|
25
|
-
return { store, service, contexts, injector, settings };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
test("registers memory and user-settings contexts", () => {
|
|
29
|
-
const { contexts } = setup();
|
|
30
|
-
assert.equal(contexts.length, 2);
|
|
31
|
-
assert.equal(contexts[0].name, "memory");
|
|
32
|
-
assert.equal(contexts[1].name, "user-settings");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("context text renders injected memories as markdown block", () => {
|
|
36
|
-
const { contexts, service } = setup();
|
|
37
|
-
service.saveWithDedupe({ type: "preference", title: "语言", content: "用户用中文交流", importance: 5 });
|
|
38
|
-
service.saveWithDedupe({ type: "project", title: "记忆插件", content: "SQLite+Markdown", importance: 4 });
|
|
39
|
-
const text = contexts[0].text({});
|
|
40
|
-
assert.ok(text.includes("[记忆库]"), "has header");
|
|
41
|
-
assert.ok(text.includes("语言"), "includes preference");
|
|
42
|
-
assert.ok(text.includes("记忆插件"), "includes high-importance project");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("returns empty text when nothing qualifies", () => {
|
|
46
|
-
const { contexts } = setup();
|
|
47
|
-
const text = contexts[0].text({});
|
|
48
|
-
assert.equal(text, "");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("renders summary block first when summary candidate exists", () => {
|
|
52
|
-
const { contexts, service } = setup();
|
|
53
|
-
service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "这是总览摘要", importance: 5 });
|
|
54
|
-
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文", importance: 5 });
|
|
55
|
-
const text = contexts[0].text({});
|
|
56
|
-
const summaryIdx = text.indexOf("这是总览摘要");
|
|
57
|
-
const prefIdx = text.indexOf("语言");
|
|
58
|
-
assert.ok(summaryIdx !== -1, "summary present");
|
|
59
|
-
assert.ok(prefIdx !== -1, "preference present");
|
|
60
|
-
assert.ok(summaryIdx < prefIdx, "summary rendered first");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("user-settings context renders profile and rules, empty when unset", () => {
|
|
64
|
-
const { contexts, settings } = setup();
|
|
65
|
-
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
66
|
-
assert.ok(settingsCtx, "user-settings context registered");
|
|
67
|
-
assert.equal(settingsCtx.text({}), "", "empty when no profile/rules");
|
|
68
|
-
settings.setProfile("我叫小明,是一名前端开发者");
|
|
69
|
-
settings.setRules(["回答时先给结论", "使用简体中文"]);
|
|
70
|
-
const text = settingsCtx.text({});
|
|
71
|
-
assert.ok(text.includes("用户画像"), "has profile header");
|
|
72
|
-
assert.ok(text.includes("前端开发者"), "includes profile");
|
|
73
|
-
assert.ok(text.includes("先给结论"), "includes rule");
|
|
74
|
-
assert.ok(text.includes("简体中文"), "includes second rule");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("user-settings context precedes memory block (order 85 < 90)", () => {
|
|
78
|
-
const { contexts, settings } = setup();
|
|
79
|
-
settings.setProfile("画像");
|
|
80
|
-
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
81
|
-
assert.ok(settingsCtx.order < contexts.find((c) => c.name === "memory").order);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("Bug6: long content is truncated to ~300 chars with an ellipsis", () => {
|
|
85
|
-
const { contexts, service } = setup();
|
|
86
|
-
const longContent = "这是一段非常长的记忆正文".repeat(200); // ~2600 chars
|
|
87
|
-
service.saveWithDedupe({ type: "preference", title: "长记忆", content: longContent, importance: 5 });
|
|
88
|
-
const text = contexts[0].text({});
|
|
89
|
-
assert.ok(text.includes("长记忆"), "memory still rendered");
|
|
90
|
-
assert.ok(text.includes("…"), "ellipsis marks the truncation");
|
|
91
|
-
assert.ok(!text.includes(longContent.slice(300)), "full body not injected verbatim");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("Bug6: injected block stays within the ~1500 char budget, later entries collapse", () => {
|
|
95
|
-
const { contexts, service } = setup({ maxInjectedItems: 8 });
|
|
96
|
-
for (let i = 0; i < 8; i++) {
|
|
97
|
-
service.saveWithDedupe({ type: "preference", title: `长标题记忆${i}`, content: "这是一段".repeat(100), importance: 5 });
|
|
98
|
-
}
|
|
99
|
-
const text = contexts[0].text({});
|
|
100
|
-
assert.ok(text.length <= 1600, `memory block bounded near budget, got ${text.length} chars`);
|
|
101
|
-
// The first entries render full bodies; every entry is present by title.
|
|
102
|
-
for (let i = 0; i < 8; i++) assert.ok(text.includes(`长标题记忆${i}`), `entry ${i} present`);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("injectTimePrefix: off by default, no time prefix injected", () => {
|
|
106
|
-
const { contexts } = setup();
|
|
107
|
-
const text = contexts[0].text({ agent: { session: { id: "s1" } } });
|
|
108
|
-
assert.ok(!text.includes("[当前时间:"), "no time prefix when disabled");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("injectTimePrefix: enabled injects once at conversation start with correct format", () => {
|
|
112
|
-
const { contexts, service } = setup({ injectTimePrefix: true });
|
|
113
|
-
service.saveWithDedupe({ type: "preference", title: "语言", content: "用户用中文交流", importance: 5 });
|
|
114
|
-
const text = contexts[0].text({ agent: { session: { id: "s1" } } });
|
|
115
|
-
assert.match(
|
|
116
|
-
text,
|
|
117
|
-
/^\[当前时间: \d{4}-\d{2}-\d{2} 周[一二三四五六日] \d{2}:\d{2}\]\n\n/,
|
|
118
|
-
"prefix leads the injection with the documented format"
|
|
119
|
-
);
|
|
120
|
-
assert.ok(text.includes("语言"), "memory body still rendered after the prefix");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("injectTimePrefix: same session never re-injects, a new session injects again", () => {
|
|
124
|
-
const { contexts } = setup({ injectTimePrefix: true });
|
|
125
|
-
const first = contexts[0].text({ agent: { session: { id: "s1" } } });
|
|
126
|
-
assert.match(first, /^\[当前时间:/, "injects on the first render of s1");
|
|
127
|
-
const second = contexts[0].text({ agent: { session: { id: "s1" } } });
|
|
128
|
-
assert.ok(!second.includes("[当前时间:"), "no re-inject within the same session");
|
|
129
|
-
const third = contexts[0].text({ agent: { session: { id: "s2" } } });
|
|
130
|
-
assert.match(third, /^\[当前时间:/, "a new session injects the prefix again");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("issue #40: {{...}} in memory content is escaped at the injection boundary", () => {
|
|
134
|
-
const { contexts, service } = setup();
|
|
135
|
-
service.saveWithDedupe({ type: "preference", title: "模板记忆", content: "{{hl|highlight}} 和 {{挖空}} {{关键词}}", importance: 5 });
|
|
136
|
-
const text = contexts[0].text({});
|
|
137
|
-
assert.ok(text.includes("模板记忆"), "memory still rendered by title");
|
|
138
|
-
assert.ok(text.includes("{\\{hl|highlight}\\}"), "ASCII template braces escaped");
|
|
139
|
-
assert.ok(text.includes("{\\{挖空}\\}"), "CJK template braces escaped");
|
|
140
|
-
assert.ok(!text.includes("{{"), "no raw {{ survives into the prompt");
|
|
141
|
-
assert.ok(!text.includes("}}"), "no raw }} survives into the prompt");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("issue #40: odd brace runs like {{{a}}} are escaped too", () => {
|
|
145
|
-
const { contexts, service } = setup();
|
|
146
|
-
service.saveWithDedupe({ type: "preference", title: "三层模板", content: "{{{a}}} 和 }}} 结尾", importance: 5 });
|
|
147
|
-
const text = contexts[0].text({});
|
|
148
|
-
assert.ok(!text.includes("{{"), "no raw {{ for odd brace runs");
|
|
149
|
-
assert.ok(!text.includes("}}"), "no raw }} for odd brace runs");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("issue #40: {{...}} in user profile/rules is escaped", () => {
|
|
153
|
-
const { contexts, settings } = setup();
|
|
154
|
-
settings.setProfile("我叫{{名字}},前端开发者");
|
|
155
|
-
settings.setRules(["回答时用 {{hl|term}}", "以 }} 结尾的规则"]);
|
|
156
|
-
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
157
|
-
const text = settingsCtx.text({});
|
|
158
|
-
assert.ok(text.includes("{\\{名字}\\}"), "profile braces escaped");
|
|
159
|
-
assert.ok(text.includes("{\\{hl|term}\\}"), "rule braces escaped");
|
|
160
|
-
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
161
|
-
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("issue #40: hot-context rounds with {{...}} are escaped", () => {
|
|
165
|
-
const { contexts } = setup();
|
|
166
|
-
const text = contexts[0].text({
|
|
167
|
-
agent: {
|
|
168
|
-
session: {
|
|
169
|
-
id: "s1",
|
|
170
|
-
events: [
|
|
171
|
-
{ type: "user/message", data: { source: { kind: "user" }, content: ["用 {{hl|你好}} 测试"] } },
|
|
172
|
-
{ type: "assistant/message", data: { source: { kind: "assistant" }, content: ["返回 {{答案}}"] } }
|
|
173
|
-
]
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
assert.ok(text.includes("[短期上下文]"), "hot context rendered");
|
|
178
|
-
assert.ok(!text.includes("{{"), "no raw {{ in hot context");
|
|
179
|
-
assert.ok(!text.includes("}}"), "no raw }} in hot context");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("session with only snapshotEvents() (DSH 0.1.2-rc.1) still renders hot context", () => {
|
|
183
|
-
const { contexts } = setup();
|
|
184
|
-
const text = contexts[0].text({
|
|
185
|
-
agent: {
|
|
186
|
-
session: {
|
|
187
|
-
id: "s1",
|
|
188
|
-
snapshotEvents: () => [
|
|
189
|
-
{ type: "user/message", data: { source: { kind: "user" }, content: ["用快照接口提问"] } },
|
|
190
|
-
{ type: "assistant/message", data: { source: { kind: "assistant" }, content: ["快照返回的答复"] } }
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
assert.ok(text.includes("[短期上下文]"), "hot context rendered");
|
|
196
|
-
assert.ok(text.includes("用快照接口提问"), "user query picked up from snapshotEvents");
|
|
197
|
-
assert.ok(text.includes("快照返回的答复"), "assistant reply picked up from snapshotEvents");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("issue #40: escapePromptVariables=false passes {{...}} through verbatim", () => {
|
|
201
|
-
const { contexts, service } = setup({ escapePromptVariables: false });
|
|
202
|
-
service.saveWithDedupe({ type: "preference", title: "模板", content: "{{挖空}} 与 {{hl|term}}", importance: 5 });
|
|
203
|
-
const text = contexts[0].text({});
|
|
204
|
-
assert.ok(text.includes("{{挖空}}"), "raw braces preserved when escaping disabled");
|
|
205
|
-
assert.ok(text.includes("{{hl|term}}"), "raw ASCII braces preserved when escaping disabled");
|
|
206
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { EventEmitter } from "node:events";
|
|
4
|
-
import { createStore } from "../src/store.js";
|
|
5
|
-
import { createService } from "../src/service.js";
|
|
6
|
-
import { createApi } from "../src/api.js";
|
|
7
|
-
import { createSettings } from "../src/settings.js";
|
|
8
|
-
import { TYPE_FILE } from "../src/mirror.js";
|
|
9
|
-
import { parseSummaryJson } from "../src/summarize.js";
|
|
10
|
-
|
|
11
|
-
// v0.8.x: layered memory types (user / fact) + Web panel stats endpoint.
|
|
12
|
-
// user = user profile (background/identity), fact = atomic factual statement.
|
|
13
|
-
// Both ride the existing single-table `memories` design — only the type enum
|
|
14
|
-
// and downstream lists (mirror / tools / summarize / inject) were extended.
|
|
15
|
-
|
|
16
|
-
class FakeRes extends EventEmitter {
|
|
17
|
-
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
18
|
-
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
19
|
-
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function req(path, method = "GET", body = null) {
|
|
23
|
-
const r = new EventEmitter();
|
|
24
|
-
r.url = path;
|
|
25
|
-
r.method = method;
|
|
26
|
-
r.headers = {};
|
|
27
|
-
if (body !== null) {
|
|
28
|
-
process.nextTick(() => {
|
|
29
|
-
r.emit("data", Buffer.from(JSON.stringify(body)));
|
|
30
|
-
r.emit("end");
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
return r;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function setup() {
|
|
37
|
-
const store = createStore(":memory:");
|
|
38
|
-
const settings = createSettings(store.db);
|
|
39
|
-
const service = createService({ store, mirror: null, config: {}, settings });
|
|
40
|
-
const commands = { add: () => {}, remove: () => {}, list: () => [] };
|
|
41
|
-
const routes = [];
|
|
42
|
-
const ctx = {
|
|
43
|
-
webServer: {
|
|
44
|
-
register(route) { routes.push(route); return () => {}; }
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
createApi(ctx, service, settings, commands, undefined, undefined, "");
|
|
48
|
-
const handler = (path) => routes.find((r) => r.path === path)?.handler;
|
|
49
|
-
return { store, service, handler };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
test("user/fact are valid memory types for saveWithDedupe", () => {
|
|
53
|
-
const { service } = setup();
|
|
54
|
-
const a = service.saveWithDedupe({ type: "user", title: "桉桉", content: "湖南工业大学学生,考研公共管理学", importance: 5 });
|
|
55
|
-
const b = service.saveWithDedupe({ type: "fact", title: "服务器地址", content: "云服务器 Ubuntu 22.04", importance: 3 });
|
|
56
|
-
assert.equal(a.memory.type, "user");
|
|
57
|
-
assert.equal(b.memory.type, "fact");
|
|
58
|
-
assert.equal(service.count("user"), 1);
|
|
59
|
-
assert.equal(service.count("fact"), 1);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("mirror TYPE_FILE covers user/fact layers", () => {
|
|
63
|
-
assert.equal(TYPE_FILE.user, "user.md");
|
|
64
|
-
assert.equal(TYPE_FILE.fact, "facts.md");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("autoSummarize accepts user/fact types", () => {
|
|
68
|
-
const parsed = parseSummaryJson(
|
|
69
|
-
'[{"type":"user","title":"用户城市","content":"湖南株洲","importance":4},{"type":"fact","title":"端口","content":"23334","importance":3}]'
|
|
70
|
-
);
|
|
71
|
-
assert.equal(parsed.length, 2);
|
|
72
|
-
assert.deepEqual(parsed.map((i) => i.type), ["user", "fact"]);
|
|
73
|
-
// legacy types still pass, unknown types rejected
|
|
74
|
-
const mixed = parseSummaryJson(
|
|
75
|
-
'[{"type":"preference","title":"语言","content":"中文","importance":2},{"type":"nonsense","title":"x","content":"y","importance":1}]'
|
|
76
|
-
);
|
|
77
|
-
assert.equal(mixed.length, 1);
|
|
78
|
-
assert.equal(mixed[0].type, "preference");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("GET /api/dsh-mneme/list?type=user filters the new layer", async () => {
|
|
82
|
-
const { service, handler } = setup();
|
|
83
|
-
service.saveWithDedupe({ type: "user", title: "桉桉", content: "用户画像", importance: 5 });
|
|
84
|
-
service.saveWithDedupe({ type: "fact", title: "事实", content: "原子事实", importance: 3 });
|
|
85
|
-
const res = new FakeRes();
|
|
86
|
-
await handler("/api/dsh-mneme/list")?.({ url: "/api/dsh-mneme/list?type=user" }, res);
|
|
87
|
-
const data = JSON.parse(res.body);
|
|
88
|
-
assert.equal(res.statusCode, 200);
|
|
89
|
-
assert.equal(data.total, 1);
|
|
90
|
-
assert.equal(data.items[0].type, "user");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("GET /api/dsh-mneme/stats returns byType distribution + recent trend", async () => {
|
|
94
|
-
const { service, handler } = setup();
|
|
95
|
-
service.saveWithDedupe({ type: "user", title: "桉桉", content: "用户画像", importance: 5 });
|
|
96
|
-
service.saveWithDedupe({ type: "fact", title: "事实1", content: "原子事实", importance: 3 });
|
|
97
|
-
service.saveWithDedupe({ type: "fact", title: "事实2", content: "另一个事实", importance: 2 });
|
|
98
|
-
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文", importance: 2 });
|
|
99
|
-
const res = new FakeRes();
|
|
100
|
-
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=7" }, res);
|
|
101
|
-
const data = JSON.parse(res.body);
|
|
102
|
-
assert.equal(res.statusCode, 200);
|
|
103
|
-
assert.equal(data.byType.user, 1);
|
|
104
|
-
assert.equal(data.byType.fact, 2);
|
|
105
|
-
assert.equal(data.byType.preference, 1);
|
|
106
|
-
assert.equal(data.total, 4);
|
|
107
|
-
// 7-day trend covers today (the only day with live rows)
|
|
108
|
-
assert.equal(data.recent.length, 7);
|
|
109
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
110
|
-
assert.equal(data.recent[6].date, today);
|
|
111
|
-
assert.equal(data.recent[6].count, 4);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("stats excludes archived/forgotten memories", async () => {
|
|
115
|
-
const { service, handler } = setup();
|
|
116
|
-
const a = service.saveWithDedupe({ type: "user", title: "桉桉", content: "用户画像", importance: 5 });
|
|
117
|
-
const b = service.saveWithDedupe({ type: "fact", title: "事实", content: "原子事实", importance: 3 });
|
|
118
|
-
service.setArchived(a.memory.id, true);
|
|
119
|
-
service.setForget(b.memory.id, true);
|
|
120
|
-
const res = new FakeRes();
|
|
121
|
-
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=7" }, res);
|
|
122
|
-
const data = JSON.parse(res.body);
|
|
123
|
-
assert.equal(data.total, 0);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("stats clamps days to 1..30", async () => {
|
|
127
|
-
const { handler } = setup();
|
|
128
|
-
const res = new FakeRes();
|
|
129
|
-
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=999" }, res);
|
|
130
|
-
const data = JSON.parse(res.body);
|
|
131
|
-
assert.equal(res.statusCode, 200);
|
|
132
|
-
assert.equal(data.recent.length, 30);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("stats coerces fractional days to integer (kimi S2)", async () => {
|
|
136
|
-
const { service, handler } = setup();
|
|
137
|
-
service.saveWithDedupe({ type: "fact", title: "事实", content: "原子事实", importance: 3 });
|
|
138
|
-
const res = new FakeRes();
|
|
139
|
-
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=7.5" }, res);
|
|
140
|
-
const data = JSON.parse(res.body);
|
|
141
|
-
assert.equal(data.recent.length, 7); // floor(7.5) → 7, never a ragged 8
|
|
142
|
-
assert.equal(data.days, 7);
|
|
143
|
-
assert.equal(data.byType.fact, 1);
|
|
144
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// lib/ 是 npm 实际加载的产物(package main 指向 lib/index.js),而现有测试只引 src/,
|
|
2
|
-
// 覆盖不到发布产物——issue #65 就是 src 适配了、lib 没同步,npm 包静默跑旧代码。
|
|
3
|
-
// 本文件直接从 lib/ 导入,复跑 DSH 0.1.2-rc.1 snapshotEvents 关键用例,
|
|
4
|
-
// 并静态断言 src → lib 逐文件一致,防止同类回归再次发生。
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { join, relative } from "node:path";
|
|
10
|
-
import { createStore } from "../lib/store.js";
|
|
11
|
-
import { createService } from "../lib/service.js";
|
|
12
|
-
import { createInjector } from "../lib/inject.js";
|
|
13
|
-
import { createSettings } from "../lib/settings.js";
|
|
14
|
-
import { createSummarizer, parseSummaryJson } from "../lib/summarize.js";
|
|
15
|
-
|
|
16
|
-
function setup(over = {}) {
|
|
17
|
-
const store = createStore(":memory:");
|
|
18
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
19
|
-
const settings = createSettings(store.db);
|
|
20
|
-
const contexts = [];
|
|
21
|
-
const ctx = {
|
|
22
|
-
systemPrompt: {
|
|
23
|
-
context(def) {
|
|
24
|
-
contexts.push(def);
|
|
25
|
-
return () => {};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
|
|
30
|
-
const injector = createInjector(ctx, service, settings, config);
|
|
31
|
-
return { contexts, injector };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
test("lib: session with only snapshotEvents() (DSH 0.1.2-rc.1) still renders hot context", () => {
|
|
35
|
-
const { contexts } = setup();
|
|
36
|
-
const text = contexts[0].text({
|
|
37
|
-
agent: {
|
|
38
|
-
session: {
|
|
39
|
-
id: "s1",
|
|
40
|
-
snapshotEvents: () => [
|
|
41
|
-
{ type: "user/message", data: { source: { kind: "user" }, content: ["用快照接口提问"] } },
|
|
42
|
-
{ type: "assistant/message", data: { source: { kind: "assistant" }, content: ["快照返回的答复"] } }
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
assert.ok(text.includes("[短期上下文]"), "hot context rendered");
|
|
48
|
-
assert.ok(text.includes("用快照接口提问"), "user query picked up from snapshotEvents");
|
|
49
|
-
assert.ok(text.includes("快照返回的答复"), "assistant reply picked up from snapshotEvents");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("lib: session with only snapshotEvents() (DSH 0.1.2-rc.1) still summarizes", async () => {
|
|
53
|
-
const store = createStore(":memory:");
|
|
54
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
55
|
-
const events = [];
|
|
56
|
-
const ctx = {
|
|
57
|
-
on(name, fn) {
|
|
58
|
-
events.push({ name, fn });
|
|
59
|
-
return () => {};
|
|
60
|
-
},
|
|
61
|
-
llm: {
|
|
62
|
-
stream() {
|
|
63
|
-
const json = JSON.stringify([
|
|
64
|
-
{ type: "decision", title: "选型", content: "确定用 node:sqlite", importance: 4 },
|
|
65
|
-
{ type: "preference", title: "语言", content: "用户喜欢中文交流", importance: 5 }
|
|
66
|
-
]);
|
|
67
|
-
return (async function* () {
|
|
68
|
-
yield { type: "finish", kind: "ok" };
|
|
69
|
-
yield { type: "block-start", block: { type: "text" } };
|
|
70
|
-
yield { type: "text-delta", delta: json };
|
|
71
|
-
yield { type: "block-end", block: { type: "text" } };
|
|
72
|
-
})();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
const summarizer = createSummarizer(ctx, service, { autoSummarize: true });
|
|
77
|
-
const handler = events.find((e) => e.name === "session/event").fn;
|
|
78
|
-
const session = {
|
|
79
|
-
id: "s6",
|
|
80
|
-
requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
|
|
81
|
-
snapshotEvents: () => [{ type: "user/message", data: { content: ["用快照接口提问"] } }, { seq: 2, type: "turn/end" }]
|
|
82
|
-
};
|
|
83
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
84
|
-
assert.equal(store.count(), 2);
|
|
85
|
-
assert.ok(store.all().some((m) => m.type === "decision"));
|
|
86
|
-
assert.ok(store.all().some((m) => m.type === "preference"));
|
|
87
|
-
summarizer.dispose();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("lib/ mirrors src/ — every src file identical in lib (npm loads lib!)", () => {
|
|
91
|
-
const srcRoot = fileURLToPath(new URL("../src/", import.meta.url));
|
|
92
|
-
const libRoot = fileURLToPath(new URL("../lib/", import.meta.url));
|
|
93
|
-
const mismatches = [];
|
|
94
|
-
const walk = (dir) => {
|
|
95
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
96
|
-
const full = join(dir, entry.name);
|
|
97
|
-
if (entry.isDirectory()) walk(full);
|
|
98
|
-
else if (entry.isFile()) {
|
|
99
|
-
const rel = relative(srcRoot, full);
|
|
100
|
-
const counterpart = join(libRoot, rel);
|
|
101
|
-
if (!existsSync(counterpart) || !readFileSync(full).equals(readFileSync(counterpart))) {
|
|
102
|
-
mismatches.push(rel);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
walk(srcRoot);
|
|
108
|
-
assert.deepEqual(mismatches, [], "src 文件在 lib 缺失或内容不一致——请先 npm run sync 并提交");
|
|
109
|
-
});
|