@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.1
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/CHANGELOG.md +42 -0
- package/LICENSE +661 -0
- package/README.en.md +110 -0
- package/README.md +102 -2
- package/dsh-memory-local/FORK-BASE.json +24 -0
- package/dsh-memory-local/README.md +113 -0
- package/dsh-memory-local/build.mjs +101 -0
- package/dsh-memory-local/cordis.patch.yml +20 -0
- package/dsh-memory-local/deploy.mjs +306 -0
- package/dsh-memory-local/entity-status.mjs +75 -0
- package/dsh-memory-local/lib/bridge.mjs +108 -0
- package/dsh-memory-local/lib/host-adapter.js +1171 -0
- package/dsh-memory-local/lib/index.d.ts +128 -0
- package/dsh-memory-local/lib/index.js +944 -0
- package/dsh-memory-local/lib/injector.mjs +400 -0
- package/dsh-memory-local/lib/memory-read.mjs +116 -0
- package/dsh-memory-local/lib/module-gate.mjs +68 -0
- package/dsh-memory-local/lib/pointer.mjs +152 -0
- package/dsh-memory-local/lib/prod-gate.mjs +121 -0
- package/dsh-memory-local/lib/recall-log.mjs +118 -0
- package/dsh-memory-local/lib/recall.mjs +192 -0
- package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
- package/dsh-memory-local/lib/user-message.mjs +32 -0
- package/dsh-memory-local/lib/vectors-read.mjs +110 -0
- package/dsh-memory-local/package.json +51 -0
- package/dsh-memory-local/restore.mjs +259 -0
- package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
- package/dsh-memory-local/src/bridge.mjs +108 -0
- package/dsh-memory-local/src/host-adapter.js +1171 -0
- package/dsh-memory-local/src/index.d.ts +128 -0
- package/dsh-memory-local/src/index.js +944 -0
- package/dsh-memory-local/src/injector.mjs +400 -0
- package/dsh-memory-local/src/memory-read.mjs +116 -0
- package/dsh-memory-local/src/module-gate.mjs +68 -0
- package/dsh-memory-local/src/pointer.mjs +152 -0
- package/dsh-memory-local/src/recall-log.mjs +118 -0
- package/dsh-memory-local/src/recall.mjs +192 -0
- package/dsh-memory-local/src/schema-migrate.mjs +365 -0
- package/dsh-memory-local/src/user-message.mjs +32 -0
- package/dsh-memory-local/src/vectors-read.mjs +110 -0
- package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
- package/dsh-memory-local/test/probe-entity.mjs +170 -0
- package/dsh-memory-local/test/regression.mjs +270 -0
- package/dsh-memory-local/test/s2b.test.mjs +568 -0
- package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
- package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
- package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
- package/dsh-memory-local/verify-after-restart.mjs +181 -0
- package/lib/deploy-guard.mjs +391 -0
- package/lib/guard-http.mjs +93 -0
- package/lib/pitfall-form-map.mjs +130 -0
- package/lib/pitfall-score.mjs +185 -0
- package/lib/session-file-name.mjs +160 -0
- package/lib/zstd-frames.mjs +78 -0
- package/memory-db-tools/README.md +68 -0
- package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
- package/memory-db-tools/audit-analyze.mjs +352 -0
- package/memory-db-tools/audit-dump-ids.mjs +245 -0
- package/memory-db-tools/audit-export.mjs +270 -0
- package/memory-db-tools/audit-hit-probe.mjs +227 -0
- package/memory-db-tools/audit-ledger-probe.mjs +236 -0
- package/memory-db-tools/audit-persona-overlap.mjs +277 -0
- package/memory-db-tools/audit-struct-probe.mjs +213 -0
- package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
- package/memory-db-tools/check-memory-db.mjs +8 -0
- package/memory-db-tools/dump-memory-db.mjs +12 -0
- package/memory-db-tools/lib/memory-scope.mjs +446 -0
- package/memory-db-tools/list-memory-summary.mjs +477 -0
- package/memory-db-tools/memory-identity-check.mjs +101 -0
- package/memory-db-tools/memory-schema-migrate.mjs +290 -0
- package/memory-db-tools/memory-scope-fixture.mjs +197 -0
- package/memory-db-tools/read-memories.mjs +26 -0
- package/memory-db-tools/read-memory-db.mjs +20 -0
- package/memory-db-tools/refine-memories.mjs +247 -0
- package/memory-db-tools/show-mem-times.mjs +22 -0
- package/memory-engine/README.md +70 -0
- package/memory-engine/bench/bench.mjs +221 -0
- package/memory-engine/cli/distill-rollback.mjs +86 -0
- package/memory-engine/cli/distill.mjs +156 -0
- package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
- package/memory-engine/cli/doc-index.mjs +48 -0
- package/memory-engine/cli/domain-tags.mjs +251 -0
- package/memory-engine/cli/pitfall-hook.mjs +91 -0
- package/memory-engine/cli/recall.mjs +132 -0
- package/memory-engine/cli/selfcheck.mjs +57 -0
- package/memory-engine/cli/serve-start.mjs +247 -0
- package/memory-engine/cli/sweep.mjs +58 -0
- package/memory-engine/cli/sync.mjs +69 -0
- package/memory-engine/lib/cli-util.mjs +197 -0
- package/memory-engine/lib/config.mjs +135 -0
- package/memory-engine/lib/dirty-ledger.mjs +174 -0
- package/memory-engine/lib/distill-rollback.mjs +178 -0
- package/memory-engine/lib/distill.mjs +898 -0
- package/memory-engine/lib/doc-index-store.mjs +325 -0
- package/memory-engine/lib/doc-index.mjs +529 -0
- package/memory-engine/lib/doc-scan.mjs +271 -0
- package/memory-engine/lib/doc-summarize.mjs +305 -0
- package/memory-engine/lib/domain-tags.mjs +429 -0
- package/memory-engine/lib/embedder.mjs +242 -0
- package/memory-engine/lib/hashes.mjs +12 -0
- package/memory-engine/lib/memory-source.mjs +127 -0
- package/memory-engine/lib/pitfall-source.mjs +170 -0
- package/memory-engine/lib/policy-source.mjs +371 -0
- package/memory-engine/lib/recall.mjs +331 -0
- package/memory-engine/lib/selfcheck.mjs +131 -0
- package/memory-engine/lib/sweep.mjs +159 -0
- package/memory-engine/lib/sync.mjs +180 -0
- package/memory-engine/lib/tick-hook.mjs +218 -0
- package/memory-engine/lib/vectors-store.mjs +292 -0
- package/memory-engine/package-lock.json +1086 -0
- package/memory-engine/package.json +22 -0
- package/memory-engine/serve.mjs +171 -0
- package/memory-engine/test/01-store.test.mjs +127 -0
- package/memory-engine/test/02-sync.test.mjs +196 -0
- package/memory-engine/test/03-sweep.test.mjs +145 -0
- package/memory-engine/test/04-recall.test.mjs +214 -0
- package/memory-engine/test/05-selfcheck.test.mjs +175 -0
- package/memory-engine/test/06-doc-index.test.mjs +928 -0
- package/memory-engine/test/07-domain-tags.test.mjs +298 -0
- package/memory-engine/test/08-distill.test.mjs +686 -0
- package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
- package/memory-engine/test/10-tick-hook.test.mjs +252 -0
- package/memory-engine/test/11-policy-source.test.mjs +224 -0
- package/memory-engine/test/_tickhook-worker.mjs +56 -0
- package/memory-engine/test/fixtures.mjs +205 -0
- package/memory-engine/test/harness.mjs +73 -0
- package/memory-engine/test/run.mjs +35 -0
- package/memory-engine/tools/bridge-smoke.mjs +80 -0
- package/memory-engine/tools/fetch-model.mjs +128 -0
- package/memory-engine/tools/smoke-recall.mjs +76 -0
- package/package.json +53 -4
- package/persona-memory-audit.mjs +217 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORK-EDIT(2026-10-09): 冲刺600 M1《注入去重修复》单测 —— `visibleInjections()` 取事件面兼容性。
|
|
3
|
+
*
|
|
4
|
+
* 背景:主包 0.1.5-rc.2 的 `Session` 已删 `get events()`(只剩 `eventAt(seq)` / `snapshotEvents()`),
|
|
5
|
+
* 老写法 `session.events[seq]` ⇒ `session.events` 恒 undefined ⇒ 该函数开头守卫直接 return {count:0}
|
|
6
|
+
* ⇒ `maxPerSession` / `surface-same-payload` 两道去重永不触发(每回合都注入)。
|
|
7
|
+
*
|
|
8
|
+
* 覆盖(本卡验收判据 ②):
|
|
9
|
+
* ① 新线:无 `session.events`、有 `eventAt` ⇒ 能数出注入过的 user 消息(count > 0 + lastText 正确)
|
|
10
|
+
* ② 老线回落:有 `session.events`、无 `eventAt` ⇒ 行为与改动前一致
|
|
11
|
+
* ③ 两者都不可用 ⇒ 安全返回 {count:0}(不抛错)
|
|
12
|
+
* ④ 回归断言:0.1.5-rc.2 形态下**不再恒 0**(本卡核心)
|
|
13
|
+
* ⑤ 真对照(可选,`M1_BEFORE_INJECTOR` 指向改动前快照):改动前实现对同一老夹具逐字段一致、
|
|
14
|
+
* 对 0.1.5 形态恒 0(复现缺陷)—— 拿不到快照即 skip,不计失败。
|
|
15
|
+
* ⑥ 主包真形态探针:直接 import `@deepseek-ai/dsh-session`,断言原型上有 `eventAt`、**没有** `events`
|
|
16
|
+
* getter(即缺陷成因的机器证据);import 不到即 skip。
|
|
17
|
+
*
|
|
18
|
+
* 家规:一切路径来自 `fs.mkdtempSync(os.tmpdir())`,**不写生产 / 不写任何 DSH home**。
|
|
19
|
+
* 跑法:node --test test/visible-injections.test.mjs
|
|
20
|
+
* (含真对照)M1_BEFORE_INJECTOR=<回滚点>\before\src\injector.mjs node --test test/visible-injections.test.mjs
|
|
21
|
+
*/
|
|
22
|
+
import test from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
import { visibleInjections, SOURCE_KIND } from '../src/injector.mjs';
|
|
30
|
+
import { EV } from '../src/host-adapter.js';
|
|
31
|
+
|
|
32
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const pkgRoot = path.join(here, '..');
|
|
34
|
+
const BEFORE_INJECTOR = process.env.M1_BEFORE_INJECTOR ?? null;
|
|
35
|
+
|
|
36
|
+
// ───────────────────────── 夹具(三种 session 形态) ─────────────────────────
|
|
37
|
+
|
|
38
|
+
/** 一条"本插件注入过"的 user 消息事件(形态与插件注入一致)。 */
|
|
39
|
+
function injectionEvent(text, ids = [], seq = 0) {
|
|
40
|
+
return {
|
|
41
|
+
seq,
|
|
42
|
+
type: EV.USER_MESSAGE,
|
|
43
|
+
data: { role: 'user', content: [{ type: 'text', text }], source: { kind: SOURCE_KIND, version: 1, items: ids } },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 一条"主人自己发"的 user 消息事件(无 source ⇒ 不该计入)。 */
|
|
48
|
+
function humanEvent(text, seq = 0) {
|
|
49
|
+
return { seq, type: EV.USER_MESSAGE, data: { role: 'user', content: [{ type: 'text', text }] } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 老线形态(≤ 0.1.4):`session.events` 是事件数组、**没有** `eventAt`。
|
|
54
|
+
* 与既有夹具 `fixtures-s2b.mjs#fakeAgent` 同形(本文件不重复导出、也不改那份夹具)。
|
|
55
|
+
*/
|
|
56
|
+
function legacyAgent(events, nodes) {
|
|
57
|
+
const list = events ?? [];
|
|
58
|
+
return { id: 'legacy-session', session: { events: list, surface: { nodes: nodes ?? list.map((_, i) => i) } } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 新线形态(0.1.5-rc.2):`eventAt(seq)` 是**原型方法**(照主包实现 `eventAt(seq) { return this.log[seq] }`),
|
|
63
|
+
* 而 `session.events` **根本不存在**(类上不定义该 getter ⇒ `session.events === undefined`)。
|
|
64
|
+
*/
|
|
65
|
+
class ModernSession {
|
|
66
|
+
constructor(log, nodes) {
|
|
67
|
+
this._log = log ?? [];
|
|
68
|
+
this.surface = { nodes: nodes ?? this._log.map((_, i) => i) };
|
|
69
|
+
}
|
|
70
|
+
get id() { return 'modern-session'; }
|
|
71
|
+
get seq() { return this._log.length; }
|
|
72
|
+
eventAt(seq) { return this._log[seq]; }
|
|
73
|
+
snapshotEvents(fromSeq = 0, toSeqExclusive = this._log.length) { return this._log.slice(fromSeq, toSeqExclusive); }
|
|
74
|
+
}
|
|
75
|
+
function modernAgent(events, nodes) {
|
|
76
|
+
return { id: 'modern-agent', session: new ModernSession(events, nodes) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 只读探针:记录某 agent 上「哪些取事件面被真读过」。 */
|
|
80
|
+
function probeAgent(session) {
|
|
81
|
+
return { id: 'probe-agent', session };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const THREE = [
|
|
85
|
+
injectionEvent('旧指针 A', [1], 0),
|
|
86
|
+
injectionEvent('旧指针 B', [2], 1),
|
|
87
|
+
injectionEvent('旧指针 C', [3], 2),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const idsOf = (r) => [...r.ids].sort();
|
|
91
|
+
|
|
92
|
+
// ───────────────────────── ① 新线(0.1.5 形态) ─────────────────────────
|
|
93
|
+
|
|
94
|
+
test('新线/正例:无 session.events、有 eventAt ⇒ 数得出注入过的 user 消息', () => {
|
|
95
|
+
const agent = modernAgent(THREE.map((e) => ({ ...e })));
|
|
96
|
+
assert.equal(agent.session.events, undefined, '新线夹具必须没有 session.events(0.1.5 形态)');
|
|
97
|
+
assert.equal(typeof agent.session.eventAt, 'function', '新线夹具必须有 eventAt(seq)');
|
|
98
|
+
|
|
99
|
+
const r = visibleInjections(agent);
|
|
100
|
+
assert.equal(r.count, 3, '三条可见注入应全部数出');
|
|
101
|
+
assert.equal(r.lastText, '旧指针 C', 'lastText = 最后一条可见注入的正文');
|
|
102
|
+
assert.deepEqual(idsOf(r), ['1', '2', '3'], 'ids 汇总所有可见注入的 items');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('新线/反例:主人自己的消息(无 source)与其它 kind 不计入', () => {
|
|
106
|
+
const log = [
|
|
107
|
+
injectionEvent('旧指针 A', [1], 0),
|
|
108
|
+
humanEvent('主人说话', 1),
|
|
109
|
+
{ seq: 2, type: EV.USER_MESSAGE, data: { role: 'user', content: [{ type: 'text', text: '别的插件' }], source: { kind: 'other-plugin', version: 1, items: [9] } } },
|
|
110
|
+
];
|
|
111
|
+
const r = visibleInjections(modernAgent(log));
|
|
112
|
+
assert.equal(r.count, 1);
|
|
113
|
+
assert.equal(r.lastText, '旧指针 A');
|
|
114
|
+
assert.deepEqual(idsOf(r), ['1'], '别的 kind 的 items 不得混入');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('新线/反例:被压缩遮蔽(不在 surface.nodes 内)的注入不计入', () => {
|
|
118
|
+
const log = THREE.map((e) => ({ ...e }));
|
|
119
|
+
const r = visibleInjections(modernAgent(log, [2])); // 只有 seq=2 还在可见 surface
|
|
120
|
+
assert.equal(r.count, 1, '遮蔽的注入不计入可见数(可见性口径)');
|
|
121
|
+
assert.equal(r.lastText, '旧指针 C');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ───────────────────────── ② 老线回落 ─────────────────────────
|
|
125
|
+
|
|
126
|
+
test('老线/正例:有 session.events、无 eventAt ⇒ 照 old 面取事件,行为不变', () => {
|
|
127
|
+
const agent = legacyAgent(THREE.map((e) => ({ ...e })));
|
|
128
|
+
assert.equal(typeof agent.session.eventAt, 'undefined', '老线夹具不得有 eventAt');
|
|
129
|
+
const r = visibleInjections(agent);
|
|
130
|
+
assert.equal(r.count, 3);
|
|
131
|
+
assert.equal(r.lastText, '旧指针 C');
|
|
132
|
+
assert.deepEqual(idsOf(r), ['1', '2', '3']);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('老线/反例:老面为空数组 ⇒ 照旧走 events 分支(不因 eventAt 缺失早退)', () => {
|
|
136
|
+
const r = visibleInjections(legacyAgent([]));
|
|
137
|
+
assert.equal(r.count, 0);
|
|
138
|
+
assert.equal(r.lastText, null);
|
|
139
|
+
assert.deepEqual(idsOf(r), []);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ───────────────────────── ③ 两面都不可用 ─────────────────────────
|
|
143
|
+
|
|
144
|
+
test('双缺/反例:既无 events 也无 eventAt ⇒ 安全返回 {count:0}(不抛错)', () => {
|
|
145
|
+
const bare = { id: 'bare', session: { surface: { nodes: [0, 1, 2] } } };
|
|
146
|
+
let r;
|
|
147
|
+
assert.doesNotThrow(() => { r = visibleInjections(bare); });
|
|
148
|
+
assert.equal(r.count, 0);
|
|
149
|
+
assert.equal(r.lastText, null);
|
|
150
|
+
assert.equal(r.ids.size, 0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('双缺/反例:session 缺失 / nodes 缺失 / agent 为 null ⇒ 一律 {count:0},不抛错', () => {
|
|
154
|
+
for (const bad of [null, undefined, {}, { session: null }, { session: {} }, { session: { surface: null } }, { session: { surface: { nodes: 'not-an-array' } } }]) {
|
|
155
|
+
let r;
|
|
156
|
+
assert.doesNotThrow(() => { r = visibleInjections(bad); }, `输入 ${JSON.stringify(bad)} 不得抛错`);
|
|
157
|
+
assert.equal(r.count, 0);
|
|
158
|
+
assert.equal(r.lastText, null);
|
|
159
|
+
assert.equal(r.ids.size, 0);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ───────────────────────── ④ 回归断言(本卡核心) ─────────────────────────
|
|
164
|
+
|
|
165
|
+
test('回归/核心:0.1.5-rc.2 形态(events 缺失)下 visibleInjections 不再恒 0', () => {
|
|
166
|
+
const agent = modernAgent(THREE.map((e) => ({ ...e })));
|
|
167
|
+
const r = visibleInjections(agent);
|
|
168
|
+
assert.ok(r.count > 0, `必须 > 0(改动前恒 0);实际 ${r.count}`);
|
|
169
|
+
assert.equal(r.count, 3);
|
|
170
|
+
assert.notEqual(r.lastText, null, 'lastText 必须取到(surface-same-payload 去重依赖它)');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('回归/核心:可由可见数触顶 maxPerSession=3 ⇒ 去重链真的能生效(decide 层联动)', async () => {
|
|
174
|
+
// 直接断言「去重判据的输入」= 可见数 —— decide 层用它判 surface-visible-limit(见 injector.mjs:226-228)
|
|
175
|
+
const seen = visibleInjections(modernAgent(THREE.map((e) => ({ ...e }))));
|
|
176
|
+
assert.ok(seen.count >= 3, `maxPerSession=3 时该值应触顶;实际 ${seen.count}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ───────────────────────── ⑤ 与改动前实现真对照 ─────────────────────────
|
|
180
|
+
|
|
181
|
+
test('对照/老线零回归 + 新线复现-修复(需 M1_BEFORE_INJECTOR)', { skip: BEFORE_INJECTOR ? false : '未提供 M1_BEFORE_INJECTOR(改动前快照路径)' }, async () => {
|
|
182
|
+
assert.ok(fs.existsSync(BEFORE_INJECTOR), `快照不存在:${BEFORE_INJECTOR}`);
|
|
183
|
+
// 把整份 src 拷到临时目录,再把改动前的 injector.mjs 覆盖回去 —— 让它照旧解析 ./recall.mjs 等同目录依赖
|
|
184
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'm1-before-'));
|
|
185
|
+
try {
|
|
186
|
+
fs.cpSync(path.join(pkgRoot, 'src'), path.join(tmp, 'src'), { recursive: true });
|
|
187
|
+
fs.copyFileSync(BEFORE_INJECTOR, path.join(tmp, 'src', 'injector.mjs'));
|
|
188
|
+
const oldMod = await import(pathToFileURL(path.join(tmp, 'src', 'injector.mjs')).href);
|
|
189
|
+
|
|
190
|
+
// (a) 老线:改动前 vs 改动后 —— 逐字段一致
|
|
191
|
+
const legacy = legacyAgent(THREE.map((e) => ({ ...e })));
|
|
192
|
+
const before = oldMod.visibleInjections(legacy);
|
|
193
|
+
const after = visibleInjections(legacy);
|
|
194
|
+
assert.deepEqual(
|
|
195
|
+
{ count: after.count, lastText: after.lastText, ids: idsOf(after) },
|
|
196
|
+
{ count: before.count, lastText: before.lastText, ids: idsOf(before) },
|
|
197
|
+
'老线回落必须与改动前逐字段一致',
|
|
198
|
+
);
|
|
199
|
+
assert.equal(after.count, 3);
|
|
200
|
+
|
|
201
|
+
// (b) 新线:改动前恒 0(复现缺陷)→ 改动后 3(修复)
|
|
202
|
+
const modernBefore = oldMod.visibleInjections(modernAgent(THREE.map((e) => ({ ...e }))));
|
|
203
|
+
const modernAfter = visibleInjections(modernAgent(THREE.map((e) => ({ ...e }))));
|
|
204
|
+
assert.equal(modernBefore.count, 0, '改动前实现必须在 0.1.5 形态下恒 0(缺陷复现)');
|
|
205
|
+
assert.equal(modernAfter.count, 3, '改动后必须数得出 3 条(缺陷修复)');
|
|
206
|
+
console.log(`[对照] 老线 before=${before.count} after=${after.count}|新线 before=${modernBefore.count} after=${modernAfter.count}`);
|
|
207
|
+
} finally {
|
|
208
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ───────────────────────── ⑥ 主包真形态探针 ─────────────────────────
|
|
213
|
+
|
|
214
|
+
test('主包探针:dsh-session 的 Session 原型有 eventAt、没有 events getter', { skip: process.env.M1_SKIP_MAINPKG_PROBE ? '显式跳过' : false }, async () => {
|
|
215
|
+
const candidates = [
|
|
216
|
+
process.env.M1_DSH_SESSION_ENTRY ?? null,
|
|
217
|
+
].filter(Boolean);
|
|
218
|
+
const entry = candidates.find((p) => fs.existsSync(p));
|
|
219
|
+
if (!entry) {
|
|
220
|
+
console.log('[探针] 跳过:本机找不到 @deepseek-ai/dsh-session 入口(可用 M1_DSH_SESSION_ENTRY 指定)');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let mod;
|
|
224
|
+
try {
|
|
225
|
+
mod = await import(pathToFileURL(entry).href);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.log(`[探针] 跳过:import 失败(peer 依赖不在该位置可解析)—— ${String(e && e.message ? e.message : e).slice(0, 160)}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const Session = mod.Session ?? mod.default?.Session ?? mod.default;
|
|
231
|
+
assert.ok(Session && typeof Session === 'function', '必须导出 Session 类');
|
|
232
|
+
assert.equal(typeof Session.prototype.eventAt, 'function', 'Session 原型必须有 eventAt(seq)');
|
|
233
|
+
const hasEventsGetter = Object.getOwnPropertyDescriptor(Session.prototype, 'events') !== undefined;
|
|
234
|
+
assert.equal(hasEventsGetter, false, 'Session 原型不得再有 events getter(0.1.5-rc.2 已删)');
|
|
235
|
+
// 顺带坐实 surface 仍是 { nodes } 形态的 getter
|
|
236
|
+
const surfaceDesc = Object.getOwnPropertyDescriptor(Session.prototype, 'surface');
|
|
237
|
+
assert.ok(surfaceDesc && typeof surfaceDesc.get === 'function', 'Session 原型必须有 get surface()');
|
|
238
|
+
console.log(`[探针] ${entry} ⇒ eventAt=function / events=getter? ${hasEventsGetter}`);
|
|
239
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-after-restart.mjs —— 部署后自检(**只读**,独立进程,不重启任何东西)。
|
|
3
|
+
*
|
|
4
|
+
* 断言五件事(每件都给 PASS/FAIL + 细节,退出码 0/1):
|
|
5
|
+
* ① 实体面:目标 profile 的 `dsh-memory` 实体存在、带 `fork` 段、`lib/index.js` 的 sha
|
|
6
|
+
* 与本包 `lib/index.js`(构建产物)逐字节一致;
|
|
7
|
+
* ② 源码↔产物:src/index.js 的 sha 与 lib/index.js 一致(即 lib 确实是当前源码的产物)、
|
|
8
|
+
* 且与 FORK-BASE.json 的上游基点 sha 对得上(版本与 sha 对齐);
|
|
9
|
+
* ③ 加载面:在**部署位置**跑 test/probe-entity.mjs —— 三工具 + memory:recall 段注册成功;
|
|
10
|
+
* ④ 实例面(--port 给出时):目标实例 HTTP 可达;
|
|
11
|
+
* ⑤ 日志面(--log-file 给出时):启动日志里没有崩溃特征与 patch 未命中告警。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* node verify-after-restart.mjs --target <DSH_HOME> [--profile web] [--port 3090]
|
|
15
|
+
* [--log-file <log>] [--expect fork|upstream] [--json]
|
|
16
|
+
* 退出码:0 = 全 PASS / 1 = 有 FAIL / 2 = 用法错误。
|
|
17
|
+
*
|
|
18
|
+
* 【卡 S14 · 2026-10-08 · token 门判活遗留同批修】④ 实例面判活改走**单一真源**
|
|
19
|
+
* `lib/guard-http.mjs#judgeHttp`(2xx/3xx/401/403 = 活;其它 4xx/5xx/无应答 = 不活)。
|
|
20
|
+
* 旧判据 `reachable = res.status === 200` 在 0.1.5 的 cookie token 门下,会把**活着的实例**
|
|
21
|
+
* 误报成"不可达"(本机实测 2026-10-08:`GET http://localhost:3080/` → 401 · 68 字节纯文本)。
|
|
22
|
+
* 接法 = 绝对 `file:///`(全仓只此一份码表;真源被移走/改名时本文件 import 阶段硬失败,不静默降级)。
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs'
|
|
25
|
+
import os from 'node:os'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import crypto from 'node:crypto'
|
|
28
|
+
import { spawnSync } from 'node:child_process'
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import { parseArgs, describeUnknown, EXIT } from '../lib/deploy-guard.mjs'
|
|
31
|
+
// 【卡 S14】HTTP 存活判据单一真源(H1 · 2026-09-22):2xx/3xx/401/403 = 活。
|
|
32
|
+
import { judgeHttp } from '../lib/guard-http.mjs'
|
|
33
|
+
|
|
34
|
+
const KNOWN_ARGS = [
|
|
35
|
+
'--json',
|
|
36
|
+
{ flag: '--target', takesValue: true },
|
|
37
|
+
{ flag: '--profile', takesValue: true, alias: '--p' },
|
|
38
|
+
{ flag: '--port', takesValue: true },
|
|
39
|
+
{ flag: '--log-file', takesValue: true },
|
|
40
|
+
{ flag: '--expect', takesValue: true },
|
|
41
|
+
]
|
|
42
|
+
const pa = parseArgs(process.argv.slice(2), KNOWN_ARGS)
|
|
43
|
+
if (!pa.ok) {
|
|
44
|
+
console.error(`[verify] ${describeUnknown(pa.unknown, KNOWN_ARGS)}`)
|
|
45
|
+
process.exit(EXIT.USAGE)
|
|
46
|
+
}
|
|
47
|
+
const rawTarget = pa.values['--target']
|
|
48
|
+
if (!rawTarget) {
|
|
49
|
+
console.error('[verify] 缺少 --target <DSH_HOME>')
|
|
50
|
+
process.exit(EXIT.USAGE)
|
|
51
|
+
}
|
|
52
|
+
const home = path.resolve(rawTarget)
|
|
53
|
+
const profileName = pa.values['--profile'] ?? 'web'
|
|
54
|
+
const expect = pa.values['--expect'] ?? 'fork'
|
|
55
|
+
const asJson = !!pa.flags['--json']
|
|
56
|
+
// 本脚本就住在包根(与 build.mjs / deploy.mjs 同级)——`here` 即包根。
|
|
57
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
58
|
+
const pkgRoot = here
|
|
59
|
+
|
|
60
|
+
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
|
|
61
|
+
const fileSha = (f) => (fs.existsSync(f) ? sha256(fs.readFileSync(f)) : null)
|
|
62
|
+
const checks = []
|
|
63
|
+
const check = (id, name, ok, detail = '') => {
|
|
64
|
+
checks.push({ id, name, ok: Boolean(ok), detail: String(detail) })
|
|
65
|
+
return Boolean(ok)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ① 实体面 -------------------------------------------------------------------
|
|
69
|
+
const target = path.join(home, 'profiles', profileName, 'node_modules', 'dsh-memory')
|
|
70
|
+
const entityPkgFile = path.join(target, 'package.json')
|
|
71
|
+
const entityEntry = path.join(target, 'lib', 'index.js')
|
|
72
|
+
const localLib = path.join(pkgRoot, 'lib', 'index.js')
|
|
73
|
+
const entityExists = fs.existsSync(target)
|
|
74
|
+
check('entity.exists', '实体目录存在', entityExists, target)
|
|
75
|
+
const entityPkg = entityExists && fs.existsSync(entityPkgFile) ? JSON.parse(fs.readFileSync(entityPkgFile, 'utf8')) : null
|
|
76
|
+
const entitySha = fileSha(entityEntry)
|
|
77
|
+
const localSha = fileSha(localLib)
|
|
78
|
+
if (expect === 'fork') {
|
|
79
|
+
check('entity.forkSection', '实体 package.json 带 `fork` 段(确认是 fork 版而非上游)',
|
|
80
|
+
Boolean(entityPkg?.fork), JSON.stringify(entityPkg?.fork?.name ?? null))
|
|
81
|
+
check('entity.sha', '实体 lib/index.js == 本包构建产物(逐字节)',
|
|
82
|
+
Boolean(entitySha && localSha && entitySha === localSha), `entity=${entitySha?.slice(0, 16)} local=${localSha?.slice(0, 16)}`)
|
|
83
|
+
check('entity.slot', '实体占用上游槽位名(bundle/patch/package-map 按 dsh-memory 解析)',
|
|
84
|
+
entityPkg?.name === 'dsh-memory', String(entityPkg?.name))
|
|
85
|
+
} else {
|
|
86
|
+
check('entity.upstream', '实体是未替换的上游包(无 fork 段)', !entityPkg?.fork, String(entityPkg?.name))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ② 源码 ↔ 产物 ↔ 基点 --------------------------------------------------------
|
|
90
|
+
const srcSha = fileSha(path.join(pkgRoot, 'src', 'index.js'))
|
|
91
|
+
const buildInfoFile = path.join(pkgRoot, 'lib', '.build-info.json')
|
|
92
|
+
const buildInfo = fs.existsSync(buildInfoFile) ? JSON.parse(fs.readFileSync(buildInfoFile, 'utf8')) : null
|
|
93
|
+
const forkBase = fs.existsSync(path.join(pkgRoot, 'FORK-BASE.json'))
|
|
94
|
+
? JSON.parse(fs.readFileSync(path.join(pkgRoot, 'FORK-BASE.json'), 'utf8')) : null
|
|
95
|
+
check('src.lib', 'lib 产物 == src 源码(构建新鲜)', Boolean(srcSha && localSha && srcSha === localSha),
|
|
96
|
+
`src=${srcSha?.slice(0, 16)} lib=${localSha?.slice(0, 16)}`)
|
|
97
|
+
check('src.buildInfo', 'lib/.build-info.json 记录的产物 sha 与实际一致',
|
|
98
|
+
buildInfo?.artifacts?.find((a) => a.file === 'lib/index.js')?.sha256 === localSha,
|
|
99
|
+
buildInfo?.artifacts?.find((a) => a.file === 'lib/index.js')?.sha256?.slice(0, 16) ?? 'missing')
|
|
100
|
+
check('base.sha', 'FORK-BASE.json 的上游基点 sha 与 FORK NOTICE 一致',
|
|
101
|
+
Boolean(forkBase?.source?.sha256) && fs.readFileSync(path.join(pkgRoot, 'src', 'index.js'), 'utf8').includes(forkBase.source.sha256),
|
|
102
|
+
forkBase?.source?.sha256?.slice(0, 16) ?? 'missing')
|
|
103
|
+
// 「部署态版本与基点对齐」只在期望 fork 时有意义(上游原物本来就没有 fork 段)
|
|
104
|
+
if (expect === 'fork') {
|
|
105
|
+
check('version.align', '部署态版本与基点版本对齐(package.json fork.base)',
|
|
106
|
+
entityPkg?.fork?.base === `${forkBase?.upstream?.name}@${forkBase?.upstream?.version}`,
|
|
107
|
+
`${entityPkg?.fork?.base} vs ${forkBase?.upstream?.name}@${forkBase?.upstream?.version}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ③ 加载面 -------------------------------------------------------------------
|
|
111
|
+
const probe = spawnSync(process.execPath, [path.join(here, 'test', 'probe-entity.mjs'), target, ...(expect === 'upstream' ? ['--upstream'] : [])],
|
|
112
|
+
{ encoding: 'utf8', timeout: 60000, windowsHide: true })
|
|
113
|
+
const probeLine = (probe.stdout ?? '').split(/\r?\n/).find((l) => l.startsWith('PROBE: ')) ?? ''
|
|
114
|
+
let probeJson = null
|
|
115
|
+
try { probeJson = JSON.parse(probeLine.slice('PROBE: '.length)) } catch { /* leave null */ }
|
|
116
|
+
check('load.tools', '部署位置加载后注册出三工具(write/search/forget)',
|
|
117
|
+
probeJson?.tools?.map((t) => t.name).sort().join(',') === 'memory_forget,memory_search,memory_write',
|
|
118
|
+
JSON.stringify(probeJson?.tools?.map((t) => t.name) ?? probe.stderr?.slice(0, 160)))
|
|
119
|
+
check('load.section', '注册 memory:recall 段(供 systemPrompt 注入)',
|
|
120
|
+
probeJson?.section === undefined ? false : true, `probe ok=${probeJson?.ok}`)
|
|
121
|
+
check('load.behavior', '三工具在临时库上真跑通(write→search→forget)',
|
|
122
|
+
probeJson?.behavior?.searchHits === 1 && probeJson?.behavior?.forgotten === true,
|
|
123
|
+
JSON.stringify(probeJson?.behavior ?? probeJson?.failures ?? null))
|
|
124
|
+
|
|
125
|
+
// ④ 实例面 -------------------------------------------------------------------
|
|
126
|
+
const port = pa.values['--port'] ? Number(pa.values['--port']) : null
|
|
127
|
+
if (port) {
|
|
128
|
+
let reachable = false
|
|
129
|
+
let status = null
|
|
130
|
+
let why = ''
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`http://localhost:${port}/`, { headers: { origin: `http://localhost:${port}` } })
|
|
133
|
+
status = res.status
|
|
134
|
+
// 【卡 S14】判活走单一真源(旧写法 `res.status === 200` 在 token 门下把 401 判死)
|
|
135
|
+
const j = judgeHttp(res.status)
|
|
136
|
+
reachable = j.alive
|
|
137
|
+
why = j.reason
|
|
138
|
+
} catch (error) {
|
|
139
|
+
reachable = false
|
|
140
|
+
status = error instanceof Error ? error.message : String(error)
|
|
141
|
+
why = 'no-answer(连不上/超时)'
|
|
142
|
+
}
|
|
143
|
+
check('instance.http', `目标实例(端口 ${port})HTTP 可达`, reachable, `status=${status}${why ? ` · ${why}` : ''}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ⑤ 日志面 -------------------------------------------------------------------
|
|
147
|
+
const logFile = pa.values['--log-file']
|
|
148
|
+
if (logFile) {
|
|
149
|
+
const exists = fs.existsSync(logFile)
|
|
150
|
+
const text = exists ? fs.readFileSync(logFile, 'utf8') : ''
|
|
151
|
+
const patterns = [
|
|
152
|
+
[/Cannot find module/i, 'Cannot find module'],
|
|
153
|
+
[/duplicate loader entry id/i, 'duplicate loader entry id'],
|
|
154
|
+
[/declares no dsh\.bundle/i, 'declares no dsh.bundle'],
|
|
155
|
+
[/did not activate/i, 'entry did not activate'],
|
|
156
|
+
[/duplicate exact route/i, 'duplicate exact route'],
|
|
157
|
+
[/patch[^\n]{0,80}(not found|unknown id|no match)/i, 'patch 未命中告警'],
|
|
158
|
+
[/^\s*Error:/m, 'Error:'],
|
|
159
|
+
]
|
|
160
|
+
const hits = []
|
|
161
|
+
for (const [re, name] of patterns) {
|
|
162
|
+
const m = re.exec(text)
|
|
163
|
+
if (m) hits.push(`${name} → ${m[0].trim().slice(0, 120)}`)
|
|
164
|
+
}
|
|
165
|
+
check('log.clean', `启动日志无崩溃/未命中特征(${path.basename(logFile)})`, exists && hits.length === 0,
|
|
166
|
+
exists ? (hits.join(' | ') || 'no crash signatures') : 'log file missing')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 汇总 -----------------------------------------------------------------------
|
|
170
|
+
const failed = checks.filter((c) => !c.ok)
|
|
171
|
+
if (asJson) {
|
|
172
|
+
console.log(JSON.stringify({
|
|
173
|
+
target: home, profile: profileName, expect, entitySha, localSha, srcSha,
|
|
174
|
+
checks, ok: failed.length === 0,
|
|
175
|
+
}, null, 2))
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`verify : ${target} (expect=${expect})`)
|
|
178
|
+
for (const c of checks) console.log(`${c.ok ? 'PASS' : 'FAIL'} ${c.id.padEnd(16)} ${c.name}${c.detail ? ` — ${c.detail}` : ''}`)
|
|
179
|
+
console.log(failed.length === 0 ? '\nverify: ALL CHECKS PASS' : `\nverify: ${failed.length} CHECK(S) FAILED`)
|
|
180
|
+
}
|
|
181
|
+
process.exitCode = failed.length === 0 ? EXIT.OK : EXIT.GATE
|